Skip to content

Commit bdc10b5

Browse files
authored
Merge pull request #186 from stepstone-tech/feature/issue-160
Tab subtitles and error message
2 parents 51f9dfd + 4827ecc commit bdc10b5

32 files changed

Lines changed: 823 additions & 176 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [Unversioned]
8+
### Added
9+
- An option to show a subtitle in each tab
10+
- An option to display an error message below step title in tabbed stepper
11+
712
## [4.0.0]
813
### Added
914
- `setEndButtonVisible` and `setBackButtonVisible` methods in `StepViewModel.Builder` for toggling button visibility (issue #104)
@@ -18,4 +23,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1823
- **Breaking change:** Changed `setNextButtonLabel` methods in `StepViewModel.Builder` to `setEndButtonLabel` so that it works for both Next and Complete buttons (issue #107)
1924
- **Breaking change:** Split `content` stepper feedback type into `content_progress` and `content_fade`.
2025

21-
[4.0.0]: https://github.com/stepstone-tech/android-material-stepper/compare/v3.3.0...4.0.0
26+
[Unversioned]: https://github.com/stepstone-tech/android-material-stepper/compare/v4.0.0...develop
27+
[4.0.0]: https://github.com/stepstone-tech/android-material-stepper/compare/v3.3.0...v4.0.0

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Moreover, you can find there other examples, e.g. how to persist state on rotati
2020
- [Advanced usage](#advanced-usage)
2121
- [Making extra operations before going to the next step](#making-extra-operations-before-going-to-the-next-step)
2222
- [Changing button labels & compound drawables per step](#changing-button-labels--compound-drawables-per-step)
23+
- [Subtitles in tabs](#subtitles-in-tabs)
2324
- [Custom styling](#custom-styling)
2425
- [Using same stepper styling across the application](#using-same-stepper-styling-across-the-application)
2526
- [Showing a Back button on first step](#showing-a-back-button-on-first-step)
@@ -300,6 +301,11 @@ It is also possible to hide Back/Next/Complete buttons on each step if needed.
300301
To do so you need to call `setBackButtonVisible(false)` and/or `setEndButtonVisible(false)` on
301302
`StepViewModel.Builder` in your adapter.
302303

304+
### Subtitles in tabs
305+
You can set a subtitle for each step in stepper with tabs, e.g. to mark a step as optional.
306+
To do so you need to set the subtitle by calling `StepViewModel.Builder#setSubtitle(int)` or `StepViewModel.Builder#setSubtitle(CharSequence)`
307+
in your adapter's `getViewModel` method.
308+
303309
### Custom styling
304310
Basic styling can be done by choosing the active and inactive step colors.
305311
There are some additional properties which can be changed directly from StepperLayout's attributes e.g. the background of bottom navigation buttons (see [StepperLayout attributes](#stepperlayout-attributes))
@@ -352,6 +358,7 @@ To show an error in the tabbed stepper if step verification fails you need to se
352358
<p><img src ="./gifs/error-on-tabs.gif" width="640" /></p>
353359

354360
If you want to keep the error displayed when going back to the previous step you need to also set `ms_showErrorStateOnBackEnabled` to `true`.
361+
If you want display an error message below the step title you need to set `ms_showErrorMessageEnabled` to `true`. The message set in `VerificationError` will be then displayed.
355362

356363
### Stepper feedback
357364
It is possible to show stepper feedback for ongoing operations (see [Stepper feedback](https://material.io/guidelines/components/steppers.html#steppers-types-of-steppers)).
@@ -510,6 +517,7 @@ A list of `ms_stepperLayoutTheme` attributes responsible for styling of StepperL
510517
| *ms_stepTabDoneIndicatorStyle* | Used by ms_stepDoneIndicator in layout/ms_step_tab |
511518
| *ms_stepTabIconBackgroundStyle* | Used by ms_stepIconBackground in layout/ms_step_tab |
512519
| *ms_stepTabTitleStyle* | Used by ms_stepTitle in layout/ms_step_tab |
520+
| *ms_stepTabSubtitleStyle* | Used by ms_stepSubtitle in layout/ms_step_tab |
513521
| *ms_stepTabDividerStyle* | Used by ms_stepDivider in layout/ms_step_tab |
514522

515523
## Changelog

material-stepper/src/main/java/com/stepstone/stepper/Step.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.stepstone.stepper;
1818

1919
import android.support.annotation.NonNull;
20+
import android.support.annotation.Nullable;
2021

2122
/**
2223
* A base step interface which all {@link StepperLayout} steps must implement.
@@ -30,7 +31,7 @@ public interface Step {
3031
* he should handle this in {@link #onError(VerificationError)}.
3132
* @return the cause of the validation failure or <i>null</i> if step was validated successfully
3233
*/
33-
VerificationError verifyStep();
34+
@Nullable VerificationError verifyStep();
3435

3536
/**
3637
* Called when this step gets selected in the the stepper layout.

material-stepper/src/main/java/com/stepstone/stepper/StepperLayout.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ public void goToPrevStep() {
250250

251251
private boolean mShowErrorStateOnBackEnabled;
252252

253+
private boolean mShowErrorMessageEnabled;
254+
253255
private boolean mTabNavigationEnabled;
254256

255257
private boolean mInProgress;
@@ -551,6 +553,21 @@ public boolean isShowErrorStateOnBackEnabled() {
551553
return mShowErrorStateOnBackEnabled;
552554
}
553555

556+
/**
557+
* Set whether an error message below step title should appear when an error occurs
558+
* @param showErrorMessageEnabled true if an error message below step title should appear when an error occurs
559+
*/
560+
public void setShowErrorMessageEnabled(boolean showErrorMessageEnabled) {
561+
this.mShowErrorMessageEnabled = showErrorMessageEnabled;
562+
}
563+
564+
/**
565+
* @return true if an error message below step title should appear when an error occurs
566+
*/
567+
public boolean isShowErrorMessageEnabled() {
568+
return mShowErrorMessageEnabled;
569+
}
570+
554571
/**
555572
* @return true if step navigation is possible by clicking on the tabs directly, false otherwise
556573
*/
@@ -570,11 +587,11 @@ public void setTabNavigationEnabled(boolean tabNavigationEnabled) {
570587
* Updates the error state in the UI.
571588
* It does nothing if showing error state is disabled.
572589
* This is used internally to show the error on tabs.
573-
* @param hasError true if error should be shown, false otherwise
590+
* @param error not null if error should be shown, null otherwise
574591
* @see #setShowErrorStateEnabled(boolean)
575592
*/
576-
public void updateErrorState(boolean hasError) {
577-
updateErrorFlag(hasError);
593+
public void updateErrorState(@Nullable VerificationError error) {
594+
updateError(error);
578595
if (mShowErrorStateEnabled) {
579596
invalidateCurrentPosition();
580597
}
@@ -592,10 +609,6 @@ public void setOffscreenPageLimit(int limit) {
592609
mPager.setOffscreenPageLimit(limit);
593610
}
594611

595-
public void updateErrorFlag(boolean hasError) {
596-
mStepperType.setErrorFlag(mCurrentStepPosition, hasError);
597-
}
598-
599612
/**
600613
* Shows a progress indicator if not already shown. This does not have to be a progress bar and it depends on chosen stepper feedback types.
601614
* @param progressMessage optional progress message if supported by the selected types
@@ -827,6 +840,8 @@ private void extractValuesFromAttributes(AttributeSet attrs, @AttrRes int defSty
827840
mShowErrorStateOnBackEnabled = a.getBoolean(R.styleable.StepperLayout_ms_showErrorStateOnBack, false);
828841
mShowErrorStateOnBackEnabled = a.getBoolean(R.styleable.StepperLayout_ms_showErrorStateOnBackEnabled, mShowErrorStateOnBackEnabled);
829842

843+
mShowErrorMessageEnabled = a.getBoolean(R.styleable.StepperLayout_ms_showErrorMessageEnabled, false);
844+
830845
mTabNavigationEnabled = a.getBoolean(R.styleable.StepperLayout_ms_tabNavigationEnabled, true);
831846

832847
mStepperLayoutTheme = a.getResourceId(R.styleable.StepperLayout_ms_stepperLayoutTheme, R.style.MSDefaultStepperLayoutTheme);
@@ -855,7 +870,7 @@ private Step findCurrentStep() {
855870
}
856871

857872
private void updateErrorFlagWhenGoingBack() {
858-
updateErrorFlag(mShowErrorStateOnBackEnabled && mStepperType.getErrorAtPosition(mCurrentStepPosition));
873+
updateError(mShowErrorStateOnBackEnabled ? mStepperType.getErrorAtPosition(mCurrentStepPosition) : null);
859874
}
860875

861876
@UiThread
@@ -887,10 +902,14 @@ private boolean verifyCurrentStep(Step step) {
887902
result = true;
888903
}
889904

890-
updateErrorFlag(result);
905+
updateError(verificationError);
891906
return result;
892907
}
893908

909+
private void updateError(@Nullable VerificationError error) {
910+
mStepperType.setError(mCurrentStepPosition, error);
911+
}
912+
894913
private void onError(@NonNull VerificationError verificationError) {
895914
Step step = findCurrentStep();
896915
if (step != null) {

material-stepper/src/main/java/com/stepstone/stepper/VerificationError.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class VerificationError {
2525
/**
2626
* A message explaining the cause of the error.
2727
*/
28-
private String mErrorMessage;
28+
private final String mErrorMessage;
2929

3030
public VerificationError(String errorMessage) {
3131
this.mErrorMessage = errorMessage;

material-stepper/src/main/java/com/stepstone/stepper/internal/type/AbstractStepperType.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import android.support.annotation.CallSuper;
2020
import android.support.annotation.ColorInt;
2121
import android.support.annotation.NonNull;
22+
import android.support.annotation.Nullable;
2223
import android.support.annotation.RestrictTo;
23-
import android.util.SparseBooleanArray;
24+
import android.util.SparseArray;
2425

2526
import com.stepstone.stepper.StepperLayout;
27+
import com.stepstone.stepper.VerificationError;
2628
import com.stepstone.stepper.adapter.StepAdapter;
2729

2830
import static android.support.annotation.RestrictTo.Scope.LIBRARY;
@@ -55,7 +57,7 @@ public abstract class AbstractStepperType {
5557

5658
final StepperLayout mStepperLayout;
5759

58-
final SparseBooleanArray mStepErrors = new SparseBooleanArray();
60+
final SparseArray<VerificationError> mStepErrors = new SparseArray<>();
5961

6062
public AbstractStepperType(StepperLayout stepperLayout) {
6163
this.mStepperLayout = stepperLayout;
@@ -71,19 +73,20 @@ public AbstractStepperType(StepperLayout stepperLayout) {
7173
/**
7274
* Called to set whether the stepPosition has an error or not, changing it's appearance.
7375
* @param stepPosition the step to set the error
74-
* @param hasError whether it has error or not
76+
* @param error error instance or null if no error
7577
*/
76-
public void setErrorFlag(int stepPosition, boolean hasError) {
77-
mStepErrors.put(stepPosition, hasError);
78+
public void setError(int stepPosition, @Nullable VerificationError error) {
79+
mStepErrors.put(stepPosition, error);
7880
}
7981

8082
/**
8183
* Checks if there's an error for the step.
8284
*
8385
* @param stepPosition the step to check for error
84-
* @return true if there's an error for this step
86+
* @return an error for this step or null if no error
8587
*/
86-
public boolean getErrorAtPosition(int stepPosition) {
88+
@Nullable
89+
public VerificationError getErrorAtPosition(int stepPosition) {
8790
return mStepErrors.get(stepPosition);
8891
}
8992

material-stepper/src/main/java/com/stepstone/stepper/internal/type/TabsStepperType.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
package com.stepstone.stepper.internal.type;
1818

19+
import android.content.Context;
1920
import android.support.annotation.NonNull;
2021
import android.support.annotation.RestrictTo;
21-
import android.util.SparseBooleanArray;
22+
import android.util.SparseArray;
2223
import android.view.View;
2324

2425
import com.stepstone.stepper.R;
2526
import com.stepstone.stepper.StepperLayout;
27+
import com.stepstone.stepper.VerificationError;
2628
import com.stepstone.stepper.adapter.StepAdapter;
2729
import com.stepstone.stepper.internal.widget.TabsContainer;
2830
import com.stepstone.stepper.viewmodel.StepViewModel;
@@ -39,8 +41,6 @@
3941
@RestrictTo(LIBRARY)
4042
public class TabsStepperType extends AbstractStepperType {
4143

42-
private static final List<CharSequence> EDIT_MODE_STEP_TITLES = Arrays.<CharSequence>asList("Step 1", "Step 2");
43-
4444
private final TabsContainer mTabsContainer;
4545

4646
public TabsStepperType(StepperLayout stepperLayout) {
@@ -53,8 +53,12 @@ public TabsStepperType(StepperLayout stepperLayout) {
5353
mTabsContainer.setListener(stepperLayout);
5454

5555
if (stepperLayout.isInEditMode()) {
56-
mTabsContainer.setSteps(EDIT_MODE_STEP_TITLES);
57-
mTabsContainer.updateSteps(0, new SparseBooleanArray());
56+
Context context = stepperLayout.getContext();
57+
mTabsContainer.setSteps(Arrays.asList(
58+
new StepViewModel.Builder(context).setTitle("Step 1").create(),
59+
new StepViewModel.Builder(context).setTitle("Step 2").setSubtitle("Optional").create())
60+
);
61+
mTabsContainer.updateSteps(0, new SparseArray<VerificationError>(), false);
5862
mTabsContainer.setVisibility(View.VISIBLE);
5963
}
6064
}
@@ -67,7 +71,8 @@ public void onStepSelected(int newStepPosition, boolean userTriggeredChange) {
6771
if (!mStepperLayout.isShowErrorStateEnabled()) {
6872
mStepErrors.clear();
6973
}
70-
mTabsContainer.updateSteps(newStepPosition, mStepErrors);
74+
75+
mTabsContainer.updateSteps(newStepPosition, mStepErrors, mStepperLayout.isShowErrorMessageEnabled());
7176
}
7277

7378
/**
@@ -76,13 +81,12 @@ public void onStepSelected(int newStepPosition, boolean userTriggeredChange) {
7681
@Override
7782
public void onNewAdapter(@NonNull StepAdapter stepAdapter) {
7883
super.onNewAdapter(stepAdapter);
79-
List<CharSequence> titles = new ArrayList<>();
84+
List<StepViewModel> stepViewModels = new ArrayList<>();
8085
final int stepCount = stepAdapter.getCount();
8186
for (int i = 0; i < stepCount; i++) {
82-
final StepViewModel stepViewModel = stepAdapter.getViewModel(i);
83-
titles.add(stepViewModel.getTitle());
87+
stepViewModels.add(stepAdapter.getViewModel(i));
8488
}
85-
mTabsContainer.setSteps(titles);
89+
mTabsContainer.setSteps(stepViewModels);
8690
mTabsContainer.setVisibility(stepCount > 1 ? View.VISIBLE : View.GONE);
8791
}
8892
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.stepstone.stepper.internal.util;
2+
3+
import android.support.annotation.Nullable;
4+
5+
/**
6+
* This class consists of {@code static} utility methods for operating
7+
* on objects.
8+
*
9+
* This backports {@link java.util.Objects} which is available since API 19.
10+
*
11+
* @author Piotr Zawadzki
12+
*/
13+
public final class ObjectsCompat {
14+
15+
private ObjectsCompat() {
16+
}
17+
18+
/**
19+
* Returns {@code true} if the arguments are equal to each other
20+
* and {@code false} otherwise.
21+
* Consequently, if both arguments are {@code null}, {@code true}
22+
* is returned and if exactly one argument is {@code null}, {@code
23+
* false} is returned. Otherwise, equality is determined by using
24+
* the {@link Object#equals equals} method of the first
25+
* argument.
26+
*
27+
* @param a an object
28+
* @param b an object to be compared with {@code a} for equality
29+
* @return {@code true} if the arguments are equal to each other
30+
* and {@code false} otherwise
31+
* @see Object#equals(Object)
32+
*/
33+
@SuppressWarnings("PMD.SuspiciousEqualsMethodName")
34+
public static boolean equals(@Nullable Object a, @Nullable Object b) {
35+
return (a == b) || (a != null && a.equals(b));
36+
}
37+
}

material-stepper/src/main/java/com/stepstone/stepper/internal/widget/ColorableProgressBar.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import android.animation.ObjectAnimator;
2020
import android.content.Context;
21-
import android.content.res.TypedArray;
2221
import android.graphics.drawable.Drawable;
2322
import android.graphics.drawable.LayerDrawable;
2423
import android.support.annotation.ColorInt;
@@ -76,20 +75,6 @@ public ColorableProgressBar(Context context, AttributeSet attrs, int defStyleAtt
7675
mProgressColor = ContextCompat.getColor(context, R.color.ms_selectedColor);
7776
mProgressBackgroundColor = ContextCompat.getColor(context, R.color.ms_unselectedColor);
7877
super.setProgressDrawable(ContextCompat.getDrawable(context, R.drawable.ms_colorable_progress_bar));
79-
80-
if (attrs != null) {
81-
final TypedArray a = getContext().obtainStyledAttributes(
82-
attrs, R.styleable.ColorableProgressBar, defStyleAttr, 0);
83-
84-
if (a.hasValue(R.styleable.ColorableProgressBar_ms_progressPrimaryColor)) {
85-
mProgressColor = a.getColor(R.styleable.ColorableProgressBar_ms_progressPrimaryColor, mProgressColor);
86-
}
87-
if (a.hasValue(R.styleable.ColorableProgressBar_ms_progressBackgroundColor)) {
88-
mProgressBackgroundColor = a.getColor(R.styleable.ColorableProgressBar_ms_progressBackgroundColor, mProgressBackgroundColor);
89-
}
90-
91-
a.recycle();
92-
}
9378
updateProgressDrawable();
9479
}
9580

material-stepper/src/main/java/com/stepstone/stepper/internal/widget/DottedProgressBar.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.stepstone.stepper.internal.widget;
1818

1919
import android.content.Context;
20-
import android.content.res.TypedArray;
2120
import android.graphics.drawable.Drawable;
2221
import android.support.annotation.ColorInt;
2322
import android.support.annotation.RestrictTo;
@@ -68,19 +67,6 @@ public DottedProgressBar(Context context, AttributeSet attrs, int defStyleAttr)
6867
super(context, attrs, defStyleAttr);
6968
mSelectedColor = ContextCompat.getColor(context, R.color.ms_selectedColor);
7069
mUnselectedColor = ContextCompat.getColor(context, R.color.ms_unselectedColor);
71-
if (attrs != null) {
72-
final TypedArray a = getContext().obtainStyledAttributes(
73-
attrs, R.styleable.DottedProgressBar, defStyleAttr, 0);
74-
75-
if (a.hasValue(R.styleable.DottedProgressBar_ms_activeDotColor)) {
76-
mSelectedColor = a.getColor(R.styleable.DottedProgressBar_ms_activeDotColor, mSelectedColor);
77-
}
78-
if (a.hasValue(R.styleable.DottedProgressBar_ms_inactiveDotColor)) {
79-
mUnselectedColor = a.getColor(R.styleable.DottedProgressBar_ms_inactiveDotColor, mUnselectedColor);
80-
}
81-
82-
a.recycle();
83-
}
8470
}
8571

8672
public void setUnselectedColor(@ColorInt int unselectedColor) {

0 commit comments

Comments
 (0)