Step indicator that can be used with (or without) a {@link ViewPager} to display current progress through an 43 | * Onboarding or any process in multiple steps.
44 | *The default main primary color if not specified in the XML attributes will use the theme primary color defined 45 | * via {@code colorPrimary} attribute.
46 | *If this view is used on a device below API 11, animations will not be used.
47 | *Usage of stepper custom attributes:
48 | *Name | 51 | *Description | 52 | *Default value | 53 | *
---|---|---|
stpi_animDuration | 57 | *duration of the line tracing animation | 58 | *250 ms | 59 | *
stpi_stepCount | 62 | *number of pages/steps | 63 | *64 | * |
stpi_circleColor | 67 | *color of the stroke circle | 68 | *#b3bdc2 (grey) | 69 | *
stpi_circleRadius | 72 | *radius of the circle | 73 | *10dp | 74 | *
stpi_circleStrokeWidth | 77 | *width of circle's radius | 78 | *4dp | 79 | *
stpi_indicatorColor | 82 | *color for the current page indicator | 83 | *#00b47c (green) | 84 | *
stpi_indicatorRadius | 87 | *radius for the circle of the current page indicator | 88 | *4dp | 89 | *
stpi_lineColor | 92 | *color of the line between indicators | 93 | *#b3bdc2 (grey) | 94 | *
stpi_lineDoneColor | 97 | *color of a line when step is done | 98 | *#00b47c (green) | 99 | *
stpi_lineStrokeWidth | 102 | *width of the line stroke | 103 | *2dp | 104 | *
stpi_lineMargin | 107 | *margin at each side of the line | 108 | *5dp | 109 | *
stpi_showDoneIcon | 112 | *show the done check icon or not | 113 | *true | 114 | *
stpi_showStepNumberInstead | 117 | *display text number for each step instead of bullets | 118 | *false | 119 | *
stpi_useBottomIndicator | 122 | *display the indicator for the current step at the bottom instead of inside bullet | 123 | *false | 124 | *
stpi_useBottomIndicatorWithStepColors | 127 | *use the same color for the bottom indicator as the step color | 128 | *false | 129 | *
stpi_bottomIndicatorHeight | 132 | *set the height for the bottom indicator component | 133 | *3dp | 134 | *
stpi_bottomIndicatorWidth | 137 | *set the width for the bottom indicator component | 138 | *50dp | 139 | *
stpi_bottomIndicatorMarginTop | 142 | *set the top margin for the bottom indicator component | 143 | *10dp | 144 | *
stpi_stepsCircleColors | 147 | *use multiple colors for each step (array of colors with the size at least the same size as the stpi_stepCount 148 | * value) | 149 | *150 | * |
stpi_stepsIndicatorColors | 153 | *use multiple colors for each step indicator (array of colors with the size at least the same size as the 154 | * stpi_stepCount value) | 155 | *156 | * |
stpi_labels | 159 | *supply an array of strings to show labels for every step indicator | 160 | *161 | * |
stpi_showLabels | 164 | *Show labels for each step indicator. Useful for timelines or checkpoints. | 165 | *false | 166 | *
stpi_labelMarginTop | 169 | *Top margin for the labels | 170 | *2dp | 171 | *
stpi_labelSize | 174 | *Size for the labels | 175 | *12sp | 176 | *
stpi_labelColor | 179 | *Color for the labels | 180 | *android:textColorSecondary defined in your project | 181 | *
184 | *
Updated by Ionut Negru on 08/08/16 to add the stepClickListener feature.
185 | *Updated by Ionut Negru on 09/08/16 to add support for customizations like: multiple colors, step text number, 186 | * bottom indicator.
187 | *Updated by Rakshak R.Hegde to add support for labels and it's customisations. Supports label too.
188 | */ 189 | @SuppressWarnings("unused") 190 | public class StepperIndicator extends View implements ViewPager.OnPageChangeListener { 191 | 192 | private static final String TAG = "StepperIndicator"; 193 | 194 | /** 195 | * Duration of the line drawing animation (ms) 196 | */ 197 | private static final int DEFAULT_ANIMATION_DURATION = 200; 198 | /** 199 | * Max multiplier of the radius when a step is being animated to the "done" state before going to it's normal radius 200 | */ 201 | private static final float EXPAND_MARK = 1.3f; 202 | 203 | private static final int STEP_INVALID = -1; 204 | 205 | /** 206 | * Paint used to draw circle 207 | */ 208 | private Paint circlePaint; 209 | /** 210 | * List of {@link Paint} objects used to draw the circle for each step. 211 | */ 212 | private List217 | * This is either declared via XML or default is used. 218 | *
219 | */ 220 | private float circleRadius; 221 | 222 | /** 223 | *224 | * Flag indicating if the steps should be displayed with an number instead of empty circles and current animated 225 | * with bullet. 226 | *
227 | */ 228 | private boolean showStepTextNumber; 229 | 230 | /** 231 | * Paint used to draw the number indicator for all steps. 232 | */ 233 | private Paint stepTextNumberPaint; 234 | 235 | /** 236 | * List of {@link Paint} objects used to draw the number indicator for each step. 237 | */ 238 | private ListIf this is set, it will override the default.
248 | */ 249 | private List291 | * This is useful if you want to use text number indicator for the steps as the bullet indicator will be 292 | * disabled for that flow. 293 | *
294 | */ 295 | private boolean useBottomIndicator; 296 | /** 297 | * The top margin of the bottom indicator. 298 | */ 299 | private float bottomIndicatorMarginTop = 0; 300 | /** 301 | * The width of the bottom indicator. 302 | */ 303 | private float bottomIndicatorWidth = 0; 304 | /** 305 | * The height of the bottom indicator. 306 | */ 307 | private float bottomIndicatorHeight = 0; 308 | /** 309 | * Flag indicating if the bottom indicator should use the same colors as the steps. 310 | */ 311 | private boolean useBottomIndicatorWithStepColors; 312 | 313 | /** 314 | * "Constant" size of the lines between steps 315 | */ 316 | private float lineLength; 317 | 318 | // Values retrieved from xml (or default values) 319 | private float checkRadius; 320 | private float indicatorRadius; 321 | private float lineMargin; 322 | private int animDuration; 323 | 324 | /** 325 | * Custom step click listener which will notify any component which sets an listener of any events (touch events) 326 | * that happen regarding the steps widget. 327 | */ 328 | private List368 | * The whole purpose of this listener is to correctly detect which step was touched by the user and notify 369 | * the component which registered to receive event updates through 370 | * {@link #addOnStepClickListener(OnStepClickListener)} 371 | *
372 | */ 373 | private GestureDetector.OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { 374 | @Override 375 | public boolean onSingleTapConfirmed(MotionEvent e) { 376 | int clickedStep = STEP_INVALID; 377 | if (isOnStepClickListenerAvailable()) { 378 | for (int i = 0; i < stepsClickAreas.size(); i++) { 379 | if (stepsClickAreas.get(i).contains(e.getX(), e.getY())) { 380 | clickedStep = i; 381 | // Stop as we found the step which was clicked 382 | break; 383 | } 384 | } 385 | } 386 | 387 | // If the clicked step is valid and an listener was setup - send the event 388 | if (clickedStep != STEP_INVALID) { 389 | for (OnStepClickListener listener : onStepClickListeners) { 390 | listener.onStepClicked(clickedStep); 391 | } 392 | } 393 | 394 | return super.onSingleTapConfirmed(e); 395 | } 396 | }; 397 | 398 | public StepperIndicator(Context context) { 399 | this(context, null); 400 | } 401 | 402 | public StepperIndicator(Context context, AttributeSet attrs) { 403 | this(context, attrs, 0); 404 | } 405 | 406 | public StepperIndicator(Context context, AttributeSet attrs, int defStyleAttr) { 407 | super(context, attrs, defStyleAttr); 408 | init(context, attrs, defStyleAttr); 409 | } 410 | 411 | @SuppressWarnings("unused") 412 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 413 | public StepperIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 414 | super(context, attrs, defStyleAttr, defStyleRes); 415 | init(context, attrs, defStyleAttr); 416 | } 417 | 418 | public static int getPrimaryColor(final Context context) { 419 | int color = context.getResources().getIdentifier("colorPrimary", "attr", context.getPackageName()); 420 | if (color != 0) { 421 | // If using support library v7 primaryColor 422 | TypedValue t = new TypedValue(); 423 | context.getTheme().resolveAttribute(color, t, true); 424 | color = t.data; 425 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 426 | // If using native primaryColor (SDK >21) 427 | TypedArray t = context.obtainStyledAttributes(new int[]{android.R.attr.colorPrimary}); 428 | color = t.getColor(0, ContextCompat.getColor(context, R.color.stpi_default_primary_color)); 429 | t.recycle(); 430 | } else { 431 | TypedArray t = context.obtainStyledAttributes(new int[]{R.attr.colorPrimary}); 432 | color = t.getColor(0, ContextCompat.getColor(context, R.color.stpi_default_primary_color)); 433 | t.recycle(); 434 | } 435 | 436 | return color; 437 | } 438 | 439 | public static int getTextColorSecondary(final Context context) { 440 | TypedArray t = context.obtainStyledAttributes(new int[]{android.R.attr.textColorSecondary}); 441 | int color = t.getColor(0, ContextCompat.getColor(context, R.color.stpi_default_text_color)); 442 | t.recycle(); 443 | return color; 444 | } 445 | 446 | private static PathEffect createPathEffect(float pathLength, float phase, float offset) { 447 | // Create a PathEffect to set on a Paint to only draw some part of the line 448 | return new DashPathEffect(new float[]{pathLength, pathLength}, Math.max(phase * pathLength, offset)); 449 | } 450 | 451 | private void init(Context context, AttributeSet attrs, int defStyleAttr) { 452 | final Resources resources = getResources(); 453 | 454 | // Default values 455 | int defaultPrimaryColor = getPrimaryColor(context); 456 | 457 | int defaultCircleColor = ContextCompat.getColor(context, R.color.stpi_default_circle_color); 458 | float defaultCircleRadius = resources.getDimension(R.dimen.stpi_default_circle_radius); 459 | float defaultCircleStrokeWidth = resources.getDimension(R.dimen.stpi_default_circle_stroke_width); 460 | 461 | //noinspection UnnecessaryLocalVariable 462 | int defaultIndicatorColor = defaultPrimaryColor; 463 | float defaultIndicatorRadius = resources.getDimension(R.dimen.stpi_default_indicator_radius); 464 | 465 | float defaultLineStrokeWidth = resources.getDimension(R.dimen.stpi_default_line_stroke_width); 466 | float defaultLineMargin = resources.getDimension(R.dimen.stpi_default_line_margin); 467 | int defaultLineColor = ContextCompat.getColor(context, R.color.stpi_default_line_color); 468 | //noinspection UnnecessaryLocalVariable 469 | int defaultLineDoneColor = defaultPrimaryColor; 470 | 471 | /* Customize the widget based on the properties set on XML, or use default if not provided */ 472 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StepperIndicator, defStyleAttr, 0); 473 | 474 | circlePaint = new Paint(); 475 | circlePaint.setStrokeWidth( 476 | a.getDimension(R.styleable.StepperIndicator_stpi_circleStrokeWidth, defaultCircleStrokeWidth)); 477 | circlePaint.setStyle(Paint.Style.STROKE); 478 | circlePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_circleColor, defaultCircleColor)); 479 | circlePaint.setAntiAlias(true); 480 | 481 | // Call this as early as possible as other properties are configured based on the number of steps 482 | setStepCount(a.getInteger(R.styleable.StepperIndicator_stpi_stepCount, 2)); 483 | 484 | final int stepsCircleColorsResId = a.getResourceId(R.styleable.StepperIndicator_stpi_stepsCircleColors, 0); 485 | if (stepsCircleColorsResId != 0) { 486 | stepsCirclePaintList = new ArrayList<>(stepCount); 487 | 488 | for (int i = 0; i < stepCount; i++) { 489 | // Based on the main indicator paint object, we create the customized one 490 | Paint circlePaint = new Paint(this.circlePaint); 491 | if (isInEditMode()) { 492 | // Fallback for edit mode - to show something in the preview 493 | circlePaint.setColor(getRandomColor()); 494 | } else { 495 | // Get the array of attributes for the colors 496 | TypedArray colorResValues = context.getResources().obtainTypedArray(stepsCircleColorsResId); 497 | 498 | if (stepCount > colorResValues.length()) { 499 | throw new IllegalArgumentException( 500 | "Invalid number of colors for the circles. Please provide a list " + 501 | "of colors with as many items as the number of steps required!"); 502 | } 503 | 504 | circlePaint.setColor(colorResValues.getColor(i, 0)); // specific color 505 | // No need for the array anymore, recycle it 506 | colorResValues.recycle(); 507 | } 508 | 509 | stepsCirclePaintList.add(circlePaint); 510 | } 511 | } 512 | 513 | indicatorPaint = new Paint(circlePaint); 514 | indicatorPaint.setStyle(Paint.Style.FILL); 515 | indicatorPaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_indicatorColor, defaultIndicatorColor)); 516 | indicatorPaint.setAntiAlias(true); 517 | 518 | stepTextNumberPaint = new Paint(indicatorPaint); 519 | stepTextNumberPaint.setTextSize(getResources().getDimension(R.dimen.stpi_default_text_size)); 520 | 521 | showStepTextNumber = a.getBoolean(R.styleable.StepperIndicator_stpi_showStepNumberInstead, false); 522 | 523 | // Get the resource from the context style properties 524 | final int stepsIndicatorColorsResId = a 525 | .getResourceId(R.styleable.StepperIndicator_stpi_stepsIndicatorColors, 0); 526 | if (stepsIndicatorColorsResId != 0) { 527 | // init the list of colors with the same size as the number of steps 528 | stepsIndicatorPaintList = new ArrayList<>(stepCount); 529 | if (showStepTextNumber) { 530 | stepsTextNumberPaintList = new ArrayList<>(stepCount); 531 | } 532 | 533 | for (int i = 0; i < stepCount; i++) { 534 | Paint indicatorPaint = new Paint(this.indicatorPaint); 535 | 536 | Paint textNumberPaint = showStepTextNumber ? new Paint(stepTextNumberPaint) : null; 537 | if (isInEditMode()) { 538 | // Fallback for edit mode - to show something in the preview 539 | 540 | indicatorPaint.setColor(getRandomColor()); // random color 541 | if (null != textNumberPaint) { 542 | textNumberPaint.setColor(indicatorPaint.getColor()); 543 | } 544 | } else { 545 | // Get the array of attributes for the colors 546 | TypedArray colorResValues = context.getResources().obtainTypedArray(stepsIndicatorColorsResId); 547 | 548 | if (stepCount > colorResValues.length()) { 549 | throw new IllegalArgumentException( 550 | "Invalid number of colors for the indicators. Please provide a list " + 551 | "of colors with as many items as the number of steps required!"); 552 | } 553 | 554 | indicatorPaint.setColor(colorResValues.getColor(i, 0)); // specific color 555 | if (null != textNumberPaint) { 556 | textNumberPaint.setColor(indicatorPaint.getColor()); 557 | } 558 | // No need for the array anymore, recycle it 559 | colorResValues.recycle(); 560 | } 561 | 562 | stepsIndicatorPaintList.add(indicatorPaint); 563 | if (showStepTextNumber && null != textNumberPaint) { 564 | stepsTextNumberPaintList.add(textNumberPaint); 565 | } 566 | } 567 | } 568 | 569 | linePaint = new Paint(); 570 | linePaint.setStrokeWidth( 571 | a.getDimension(R.styleable.StepperIndicator_stpi_lineStrokeWidth, defaultLineStrokeWidth)); 572 | linePaint.setStrokeCap(Paint.Cap.ROUND); 573 | linePaint.setStyle(Paint.Style.STROKE); 574 | linePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_lineColor, defaultLineColor)); 575 | linePaint.setAntiAlias(true); 576 | 577 | lineDonePaint = new Paint(linePaint); 578 | lineDonePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_lineDoneColor, defaultLineDoneColor)); 579 | 580 | lineDoneAnimatedPaint = new Paint(lineDonePaint); 581 | 582 | // Check if we should use the bottom indicator instead of the bullet one 583 | useBottomIndicator = a.getBoolean(R.styleable.StepperIndicator_stpi_useBottomIndicator, false); 584 | if (useBottomIndicator) { 585 | // Get the default height(stroke width) for the bottom indicator 586 | float defaultHeight = resources.getDimension(R.dimen.stpi_default_bottom_indicator_height); 587 | 588 | bottomIndicatorHeight = a 589 | .getDimension(R.styleable.StepperIndicator_stpi_bottomIndicatorHeight, defaultHeight); 590 | 591 | if (bottomIndicatorHeight <= 0) { 592 | Log.d(TAG, "init: Invalid indicator height, disabling bottom indicator feature! Please provide " + 593 | "a value greater than 0."); 594 | useBottomIndicator = false; 595 | } 596 | 597 | // Get the default width for the bottom indicator 598 | float defaultWidth = resources.getDimension(R.dimen.stpi_default_bottom_indicator_width); 599 | bottomIndicatorWidth = a.getDimension(R.styleable.StepperIndicator_stpi_bottomIndicatorWidth, defaultWidth); 600 | 601 | // Get the default top margin for the bottom indicator 602 | float defaultTopMargin = resources.getDimension(R.dimen.stpi_default_bottom_indicator_margin_top); 603 | bottomIndicatorMarginTop = a 604 | .getDimension(R.styleable.StepperIndicator_stpi_bottomIndicatorMarginTop, defaultTopMargin); 605 | 606 | useBottomIndicatorWithStepColors = a 607 | .getBoolean(R.styleable.StepperIndicator_stpi_useBottomIndicatorWithStepColors, false); 608 | } 609 | 610 | circleRadius = a.getDimension(R.styleable.StepperIndicator_stpi_circleRadius, defaultCircleRadius); 611 | checkRadius = circleRadius + circlePaint.getStrokeWidth() / 2f; 612 | indicatorRadius = a.getDimension(R.styleable.StepperIndicator_stpi_indicatorRadius, defaultIndicatorRadius); 613 | animIndicatorRadius = indicatorRadius; 614 | animCheckRadius = checkRadius; 615 | lineMargin = a.getDimension(R.styleable.StepperIndicator_stpi_lineMargin, defaultLineMargin); 616 | 617 | animDuration = a.getInteger(R.styleable.StepperIndicator_stpi_animDuration, DEFAULT_ANIMATION_DURATION); 618 | showDoneIcon = a.getBoolean(R.styleable.StepperIndicator_stpi_showDoneIcon, true); 619 | doneIcon = a.getDrawable(R.styleable.StepperIndicator_stpi_doneIconDrawable); 620 | 621 | // Labels Configuration 622 | labelPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 623 | labelPaint.setTextAlign(Paint.Align.CENTER); 624 | 625 | float defaultLabelSize = resources.getDimension(R.dimen.stpi_default_label_size); 626 | labelSize = a.getDimension(R.styleable.StepperIndicator_stpi_labelSize, defaultLabelSize); 627 | labelPaint.setTextSize(labelSize); 628 | 629 | float defaultLabelMarginTop = resources.getDimension(R.dimen.stpi_default_label_margin_top); 630 | labelMarginTop = a.getDimension(R.styleable.StepperIndicator_stpi_labelMarginTop, defaultLabelMarginTop); 631 | 632 | showLabels(a.getBoolean(R.styleable.StepperIndicator_stpi_showLabels, false)); 633 | setLabels(a.getTextArray(R.styleable.StepperIndicator_stpi_labels)); 634 | 635 | if (a.hasValue(R.styleable.StepperIndicator_stpi_labelColor)) { 636 | setLabelColor(a.getColor(R.styleable.StepperIndicator_stpi_labelColor, 0)); 637 | } else { 638 | setLabelColor(getTextColorSecondary(getContext())); 639 | } 640 | 641 | if (isInEditMode() && showLabels && labels == null) { 642 | labels = new CharSequence[]{"First", "Second", "Third", "Fourth", "Fifth"}; 643 | } 644 | 645 | if (!a.hasValue(R.styleable.StepperIndicator_stpi_stepCount) && labels != null) { 646 | setStepCount(labels.length); 647 | } 648 | 649 | a.recycle(); 650 | 651 | if (showDoneIcon && doneIcon == null) { 652 | doneIcon = ContextCompat.getDrawable(context, R.drawable.ic_done_white_18dp); 653 | } 654 | if (doneIcon != null) { 655 | int size = getContext().getResources().getDimensionPixelSize(R.dimen.stpi_done_icon_size); 656 | doneIcon.setBounds(0, 0, size, size); 657 | } 658 | 659 | // Display at least 1 cleared step for preview in XML editor 660 | if (isInEditMode()) { 661 | currentStep = Math.max((int) Math.ceil(stepCount / 2f), 1); 662 | } 663 | 664 | // Initialize the gesture detector, setup with our custom gesture listener 665 | gestureDetector = new GestureDetector(getContext(), gestureListener); 666 | } 667 | 668 | /** 669 | * Get an random color {@link Paint} object. 670 | * 671 | * @return {@link Paint} object with the same attributes as {@link #circlePaint} and with an random color. 672 | * @see #circlePaint 673 | * @see #getRandomColor() 674 | */ 675 | private Paint getRandomPaint() { 676 | Paint paint = new Paint(indicatorPaint); 677 | paint.setColor(getRandomColor()); 678 | 679 | return paint; 680 | } 681 | 682 | /** 683 | * Get an random color value. 684 | * 685 | * @return The color value as AARRGGBB 686 | */ 687 | private int getRandomColor() { 688 | Random rnd = new Random(); 689 | return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); 690 | } 691 | 692 | @Override 693 | public boolean onTouchEvent(MotionEvent event) { 694 | // Dispatch the touch events to our custom gesture detector. 695 | gestureDetector.onTouchEvent(event); 696 | return true; // we handle the event in the gesture detector 697 | } 698 | 699 | @Override 700 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 701 | compute(); // for setting up the indicator based on the new position 702 | } 703 | 704 | /** 705 | * Make calculations for establishing the exact positions of each step component, for the line dividers, for 706 | * bottom indicators, etc. 707 | *708 | * Call this whenever there is an layout change for the widget. 709 | *
710 | */ 711 | private void compute() { 712 | if (null == circlePaint) { 713 | throw new IllegalArgumentException("circlePaint is invalid! Make sure you setup the field circlePaint " + 714 | "before calling compute() method!"); 715 | } 716 | 717 | indicators = new float[stepCount]; 718 | linePathList.clear(); 719 | 720 | float startX = circleRadius * EXPAND_MARK + circlePaint.getStrokeWidth() / 2f; 721 | if (useBottomIndicator) { 722 | startX = bottomIndicatorWidth / 2F; 723 | } 724 | if (showLabels) { 725 | // gridWidth is the width of the grid assigned for the step indicator 726 | int gridWidth = getMeasuredWidth() / stepCount; 727 | startX = gridWidth / 2F; 728 | } 729 | 730 | // Compute position of indicators and line length 731 | float divider = (getMeasuredWidth() - startX * 2f) / (stepCount - 1); 732 | lineLength = divider - (circleRadius * 2f + circlePaint.getStrokeWidth()) - (lineMargin * 2); 733 | 734 | // Compute position of circles and lines once 735 | for (int i = 0; i < indicators.length; i++) { 736 | indicators[i] = startX + divider * i; 737 | } 738 | for (int i = 0; i < indicators.length - 1; i++) { 739 | float position = ((indicators[i] + indicators[i + 1]) / 2) - lineLength / 2; 740 | final Path linePath = new Path(); 741 | float lineY = getStepCenterY(); 742 | linePath.moveTo(position, lineY); 743 | linePath.lineTo(position + lineLength, lineY); 744 | linePathList.add(linePath); 745 | } 746 | 747 | computeStepsClickAreas(); // update the position of the steps click area also 748 | } 749 | 750 | /** 751 | *752 | * Calculate the area for each step. This ensure the correct step is detected when an click event is detected. 753 | *
754 | *755 | * Whenever {@link #compute()} method is called, make sure to call this method also so that the steps click 756 | * area is updated. 757 | *
758 | */ 759 | public void computeStepsClickAreas() { 760 | if (stepCount == STEP_INVALID) { 761 | throw new IllegalArgumentException("stepCount wasn't setup yet. Make sure you call setStepCount() " + 762 | "before computing the steps click area!"); 763 | } 764 | 765 | if (null == indicators) { 766 | throw new IllegalArgumentException("indicators wasn't setup yet. Make sure the indicators are " + 767 | "initialized and setup correctly before trying to compute the click " + 768 | "area for each step!"); 769 | } 770 | 771 | // Initialize the list for the steps click area 772 | stepsClickAreas = new ArrayList<>(stepCount); 773 | 774 | // Compute the clicked area for each step 775 | for (float indicator : indicators) { 776 | // Get the indicator position 777 | // Calculate the bounds for the step 778 | float left = indicator - circleRadius * 2; 779 | float right = indicator + circleRadius * 2; 780 | float top = getStepCenterY() - circleRadius * 2; 781 | float bottom = getStepCenterY() + circleRadius + getBottomIndicatorHeight(); 782 | 783 | // Store the click area for the step 784 | RectF area = new RectF(left, top, right, bottom); 785 | stepsClickAreas.add(area); 786 | } 787 | } 788 | 789 | /** 790 | * Get the height of the bottom indicator. 791 | *792 | * The height will include the height necessary for correctly drawing the bottom indicator plus the margin 793 | * set in XML (or the default one). 794 | *
795 | *796 | * If the widget isn't set to display the bottom indicator this will method will always return {@code 0} 797 | *
798 | * 799 | * @return The height of the bottom indicator in pixels or {@code 0}. 800 | */ 801 | private int getBottomIndicatorHeight() { 802 | if (useBottomIndicator) { 803 | return (int) (bottomIndicatorHeight + bottomIndicatorMarginTop); 804 | } else { 805 | return 0; 806 | } 807 | } 808 | 809 | private float getMaxLabelHeight() { 810 | return showLabels ? maxLabelHeight + labelMarginTop : 0; 811 | } 812 | 813 | private void calculateMaxLabelHeight(final int measuredWidth) { 814 | if (!showLabels) return; 815 | 816 | // gridWidth is the width of the grid assigned for the step indicator 817 | int twoDp = getContext().getResources().getDimensionPixelSize(R.dimen.stpi_two_dp); 818 | int gridWidth = measuredWidth / stepCount - twoDp; 819 | 820 | if (gridWidth <= 0) return; 821 | 822 | // Compute StaticLayout for the labels 823 | labelLayouts = new StaticLayout[labels.length]; 824 | maxLabelHeight = 0F; 825 | float labelSingleLineHeight = labelPaint.descent() - labelPaint.ascent(); 826 | for (int i = 0; i < labels.length; i++) { 827 | if (labels[i] == null) continue; 828 | 829 | labelLayouts[i] = new StaticLayout(labels[i], labelPaint, gridWidth, 830 | Layout.Alignment.ALIGN_NORMAL, 1, 0, false); 831 | maxLabelHeight = Math.max(maxLabelHeight, labelLayouts[i].getLineCount() * labelSingleLineHeight); 832 | } 833 | } 834 | 835 | private float getStepCenterY() { 836 | return (getMeasuredHeight() - getBottomIndicatorHeight() - getMaxLabelHeight()) / 2f; 837 | } 838 | 839 | @SuppressWarnings("ConstantConditions") 840 | @Override 841 | protected void onDraw(Canvas canvas) { 842 | float centerY = getStepCenterY(); 843 | 844 | // Currently Drawing animation from step n-1 to n, or back from n+1 to n 845 | boolean inAnimation = animatorSet != null && animatorSet.isRunning(); 846 | boolean inLineAnimation = lineAnimator != null && lineAnimator.isRunning(); 847 | boolean inIndicatorAnimation = indicatorAnimator != null && indicatorAnimator.isRunning(); 848 | boolean inCheckAnimation = checkAnimator != null && checkAnimator.isRunning(); 849 | 850 | boolean drawToNext = previousStep == currentStep - 1; 851 | boolean drawFromNext = previousStep == currentStep + 1; 852 | 853 | for (int i = 0; i < indicators.length; i++) { 854 | final float indicator = indicators[i]; 855 | 856 | // We draw the "done" check if previous step, or if we are going back (if going back, animated value will reduce radius to 0) 857 | boolean drawCheck = i < currentStep || (drawFromNext && i == currentStep); 858 | 859 | // Draw back circle 860 | canvas.drawCircle(indicator, centerY, circleRadius, getStepCirclePaint(i)); 861 | 862 | // Draw the step number inside the back circle if the flag for this is set to true 863 | if (showStepTextNumber) { 864 | final String stepLabel = String.valueOf(i + 1); 865 | 866 | stepAreaRect.set((int) (indicator - circleRadius), (int) (centerY - circleRadius), 867 | (int) (indicator + circleRadius), (int) (centerY + circleRadius)); 868 | stepAreaRectF.set(stepAreaRect); 869 | 870 | Paint stepTextNumberPaint = getStepTextNumberPaint(i); 871 | 872 | // measure text width 873 | stepAreaRectF.right = stepTextNumberPaint.measureText(stepLabel, 0, stepLabel.length()); 874 | // measure text height 875 | stepAreaRectF.bottom = stepTextNumberPaint.descent() - stepTextNumberPaint.ascent(); 876 | 877 | stepAreaRectF.left += (stepAreaRect.width() - stepAreaRectF.right) / 2.0f; 878 | stepAreaRectF.top += (stepAreaRect.height() - stepAreaRectF.bottom) / 2.0f; 879 | 880 | canvas.drawText(stepLabel, stepAreaRectF.left, stepAreaRectF.top - stepTextNumberPaint.ascent(), 881 | stepTextNumberPaint); 882 | } 883 | 884 | if (showLabels && labelLayouts != null && 885 | i < labelLayouts.length && labelLayouts[i] != null) { 886 | drawLayout(labelLayouts[i], 887 | indicator, getHeight() - getBottomIndicatorHeight() - maxLabelHeight, 888 | canvas, labelPaint); 889 | } 890 | 891 | if (useBottomIndicator) { 892 | // Show the current step indicator as bottom line 893 | if (i == currentStep) { 894 | // Draw custom indicator for current step only 895 | canvas.drawRect(indicator - bottomIndicatorWidth / 2, getHeight() - bottomIndicatorHeight, 896 | indicator + bottomIndicatorWidth / 2, getHeight(), 897 | useBottomIndicatorWithStepColors ? getStepIndicatorPaint(i) : indicatorPaint); 898 | } 899 | } else { 900 | // Show the current step indicator as bullet 901 | // If current step, or coming back from next step and still animating 902 | if ((i == currentStep && !drawFromNext) || (i == previousStep && drawFromNext && inAnimation)) { 903 | // Draw animated indicator 904 | canvas.drawCircle(indicator, centerY, animIndicatorRadius, getStepIndicatorPaint(i)); 905 | } 906 | } 907 | 908 | // Draw check mark 909 | if (drawCheck) { 910 | float radius = checkRadius; 911 | // Use animated radius value? 912 | if ((i == previousStep && drawToNext) || (i == currentStep && drawFromNext)) radius = animCheckRadius; 913 | canvas.drawCircle(indicator, centerY, radius, getStepIndicatorPaint(i)); 914 | 915 | // Draw check bitmap 916 | if (!isInEditMode() && showDoneIcon) { 917 | if ((i != previousStep && i != currentStep) || 918 | (!inCheckAnimation && !(i == currentStep && !inAnimation))) { 919 | canvas.save(); 920 | canvas.translate(indicator - (doneIcon.getIntrinsicWidth() / 2), 921 | centerY - (doneIcon.getIntrinsicHeight() / 2)); 922 | doneIcon.draw(canvas); 923 | canvas.restore(); 924 | } 925 | } 926 | } 927 | 928 | // Draw lines 929 | if (i < linePathList.size()) { 930 | if (i >= currentStep) { 931 | canvas.drawPath(linePathList.get(i), linePaint); 932 | if (i == currentStep && drawFromNext && (inLineAnimation || inIndicatorAnimation)) { 933 | // Coming back from n+1 934 | canvas.drawPath(linePathList.get(i), lineDoneAnimatedPaint); 935 | } 936 | } else { 937 | if (i == currentStep - 1 && drawToNext && inLineAnimation) { 938 | // Going to n+1 939 | canvas.drawPath(linePathList.get(i), linePaint); 940 | canvas.drawPath(linePathList.get(i), lineDoneAnimatedPaint); 941 | } else { 942 | canvas.drawPath(linePathList.get(i), lineDonePaint); 943 | } 944 | } 945 | } 946 | } 947 | } 948 | 949 | /** 950 | * x and y anchored to top-middle point of StaticLayout 951 | */ 952 | public static void drawLayout(Layout layout, float x, float y, 953 | Canvas canvas, TextPaint paint) { 954 | canvas.save(); 955 | canvas.translate(x, y); 956 | layout.draw(canvas); 957 | canvas.restore(); 958 | } 959 | 960 | /** 961 | * Get the {@link Paint} object which should be used for displaying the current step indicator. 962 | * 963 | * @param stepPosition The step position for which to retrieve the {@link Paint} object 964 | * @return The {@link Paint} object for the specified step position 965 | */ 966 | private Paint getStepIndicatorPaint(final int stepPosition) { 967 | return getPaint(stepPosition, stepsIndicatorPaintList, indicatorPaint); 968 | } 969 | 970 | /** 971 | * Get the {@link Paint} object which should be used for drawing the text number the current step. 972 | * 973 | * @param stepPosition The step position for which to retrieve the {@link Paint} object 974 | * @return The {@link Paint} object for the specified step position 975 | */ 976 | private Paint getStepTextNumberPaint(final int stepPosition) { 977 | return getPaint(stepPosition, stepsTextNumberPaintList, stepTextNumberPaint); 978 | } 979 | 980 | /** 981 | * Get the {@link Paint} object which should be used for drawing the circle for the step. 982 | * 983 | * @param stepPosition The step position for which to retrieve the {@link Paint} object 984 | * @return The {@link Paint} object for the specified step position 985 | */ 986 | private Paint getStepCirclePaint(final int stepPosition) { 987 | return getPaint(stepPosition, stepsCirclePaintList, circlePaint); 988 | } 989 | 990 | /** 991 | * Get the {@link Paint} object based on the step position and the source list of {@link Paint} objects. 992 | *993 | * If none found, will try to use the provided default. If not valid also, an random {@link Paint} object 994 | * will be returned instead. 995 | *
996 | * 997 | * @param stepPosition The step position for which the {@link Paint} object is needed 998 | * @param sourceList The source list of {@link Paint} object. 999 | * @param defaultPaint The default {@link Paint} object which will be returned if the source list does not 1000 | * contain the specified step. 1001 | * @return {@link Paint} object for the specified step position. 1002 | */ 1003 | private Paint getPaint(final int stepPosition, final List1033 | * This method ensured the widget doesn't try to use invalid steps. It will throw an exception whenever an 1034 | * invalid step is detected. Catch the exception if it is expected or it doesn't affect the flow. 1035 | *
1036 | * 1037 | * @param stepPos The step position to verify 1038 | * @return {@code true} if the step is valid, otherwise it will throw an exception. 1039 | */ 1040 | private boolean isStepValid(final int stepPos) { 1041 | if (stepPos < 0 || stepPos > stepCount - 1) { 1042 | throw new IllegalArgumentException("Invalid step position. " + stepPos + " is not a valid position! it " + 1043 | "should be between 0 and stepCount(" + stepCount + ")"); 1044 | } 1045 | 1046 | return true; 1047 | } 1048 | 1049 | @Override 1050 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1051 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1052 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1053 | 1054 | int width = widthMode == MeasureSpec.EXACTLY ? widthSize : getSuggestedMinimumWidth(); 1055 | 1056 | calculateMaxLabelHeight(width); 1057 | 1058 | // Compute the necessary height for the widget 1059 | int desiredHeight = (int) Math.ceil( 1060 | (circleRadius * EXPAND_MARK * 2) + 1061 | circlePaint.getStrokeWidth() + 1062 | getBottomIndicatorHeight() + 1063 | getMaxLabelHeight() 1064 | ); 1065 | 1066 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1067 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1068 | int height = heightMode == MeasureSpec.EXACTLY ? heightSize : desiredHeight; 1069 | 1070 | setMeasuredDimension(width, height); 1071 | } 1072 | 1073 | @SuppressWarnings("unused") 1074 | public int getStepCount() { 1075 | return stepCount; 1076 | } 1077 | 1078 | public void setStepCount(int stepCount) { 1079 | if (stepCount < 2) { 1080 | throw new IllegalArgumentException("stepCount must be >= 2"); 1081 | } 1082 | 1083 | this.stepCount = stepCount; 1084 | currentStep = 0; 1085 | compute(); 1086 | invalidate(); 1087 | } 1088 | 1089 | @SuppressWarnings("unused") 1090 | public int getCurrentStep() { 1091 | return currentStep; 1092 | } 1093 | 1094 | /** 1095 | * Sets the current step 1096 | * 1097 | * @param currentStep a value between 0 (inclusive) and stepCount (inclusive) 1098 | */ 1099 | @UiThread 1100 | public void setCurrentStep(int currentStep) { 1101 | if (currentStep < 0 || currentStep > stepCount) { 1102 | throw new IllegalArgumentException("Invalid step value " + currentStep); 1103 | } 1104 | 1105 | previousStep = this.currentStep; 1106 | this.currentStep = currentStep; 1107 | 1108 | // Cancel any running animations 1109 | if (animatorSet != null) { 1110 | animatorSet.cancel(); 1111 | } 1112 | 1113 | animatorSet = null; 1114 | lineAnimator = null; 1115 | indicatorAnimator = null; 1116 | 1117 | // TODO: 05/08/16 handle cases where steps are skipped - need to animate all of them 1118 | 1119 | if (currentStep == previousStep + 1) { 1120 | // Going to next step 1121 | animatorSet = new AnimatorSet(); 1122 | 1123 | // First, draw line to new 1124 | lineAnimator = ObjectAnimator.ofFloat(StepperIndicator.this, "animProgress", 1.0f, 0.0f); 1125 | 1126 | // Same time, pop check mark 1127 | checkAnimator = ObjectAnimator.ofFloat(StepperIndicator.this, "animCheckRadius", indicatorRadius, 1128 | checkRadius * EXPAND_MARK, checkRadius); 1129 | 1130 | // Finally, pop current step indicator 1131 | animIndicatorRadius = 0; 1132 | indicatorAnimator = ObjectAnimator.ofFloat(StepperIndicator.this, "animIndicatorRadius", 0f, 1133 | indicatorRadius * 1.4f, indicatorRadius); 1134 | 1135 | animatorSet.play(lineAnimator).with(checkAnimator).before(indicatorAnimator); 1136 | } else if (currentStep == previousStep - 1) { 1137 | // Going back to previous step 1138 | animatorSet = new AnimatorSet(); 1139 | 1140 | // First, pop out current step indicator 1141 | indicatorAnimator = ObjectAnimator 1142 | .ofFloat(StepperIndicator.this, "animIndicatorRadius", indicatorRadius, 0f); 1143 | 1144 | // Then delete line 1145 | animProgress = 1.0f; 1146 | lineDoneAnimatedPaint.setPathEffect(null); 1147 | lineAnimator = ObjectAnimator.ofFloat(StepperIndicator.this, "animProgress", 0.0f, 1.0f); 1148 | 1149 | // Finally, pop out check mark to display step indicator 1150 | animCheckRadius = checkRadius; 1151 | checkAnimator = ObjectAnimator 1152 | .ofFloat(StepperIndicator.this, "animCheckRadius", checkRadius, indicatorRadius); 1153 | 1154 | animatorSet.playSequentially(indicatorAnimator, lineAnimator, checkAnimator); 1155 | } 1156 | 1157 | if (animatorSet != null) { 1158 | // Max 500 ms for the animation 1159 | lineAnimator.setDuration(Math.min(500, animDuration)); 1160 | lineAnimator.setInterpolator(new DecelerateInterpolator()); 1161 | // Other animations will run 2 times faster that line animation 1162 | indicatorAnimator.setDuration(lineAnimator.getDuration() / 2); 1163 | checkAnimator.setDuration(lineAnimator.getDuration() / 2); 1164 | 1165 | animatorSet.start(); 1166 | } 1167 | 1168 | invalidate(); 1169 | } 1170 | 1171 | /** 1172 | *1173 | * Setter method for the animation progress. 1174 | *
1175 | * DO NOT CALL, DELETE OR RENAME: Will be used by animation. 1176 | */ 1177 | @SuppressWarnings("unused") 1178 | public void setAnimProgress(float animProgress) { 1179 | this.animProgress = animProgress; 1180 | lineDoneAnimatedPaint.setPathEffect(createPathEffect(lineLength, animProgress, 0.0f)); 1181 | invalidate(); 1182 | } 1183 | 1184 | /** 1185 | *1186 | * Setter method for the indicator radius animation. 1187 | *
1188 | * DO NOT CALL, DELETE OR RENAME: Will be used by animation. 1189 | */ 1190 | @SuppressWarnings("unused") 1191 | public void setAnimIndicatorRadius(float animIndicatorRadius) { 1192 | this.animIndicatorRadius = animIndicatorRadius; 1193 | invalidate(); 1194 | } 1195 | 1196 | /** 1197 | *1198 | * Setter method for the checkmark radius animation. 1199 | *
1200 | * DO NOT CALL, DELETE OR RENAME: Will be used by animation. 1201 | */ 1202 | @SuppressWarnings("unused") 1203 | public void setAnimCheckRadius(float animCheckRadius) { 1204 | this.animCheckRadius = animCheckRadius; 1205 | invalidate(); 1206 | } 1207 | 1208 | /** 1209 | * Set the {@link ViewPager} associated with this widget indicator. 1210 | * 1211 | * @param pager {@link ViewPager} to attach 1212 | */ 1213 | @SuppressWarnings("unused") 1214 | public void setViewPager(ViewPager pager) { 1215 | if (pager.getAdapter() == null) { 1216 | throw new IllegalStateException("ViewPager does not have adapter instance."); 1217 | } 1218 | setViewPager(pager, pager.getAdapter().getCount()); 1219 | } 1220 | 1221 | /** 1222 | * Set the {@link ViewPager} associated with this widget indicator. 1223 | * 1224 | * @param pager {@link ViewPager} to attach 1225 | * @param keepLastPage {@code true} if the widget should not create an indicator for the last page, to use it as 1226 | * the 1227 | * final page 1228 | */ 1229 | public void setViewPager(ViewPager pager, boolean keepLastPage) { 1230 | if (pager.getAdapter() == null) { 1231 | throw new IllegalStateException("ViewPager does not have adapter instance."); 1232 | } 1233 | setViewPager(pager, pager.getAdapter().getCount() - (keepLastPage ? 1 : 0)); 1234 | } 1235 | 1236 | /** 1237 | * Set the {@link ViewPager} associated with this widget indicator. 1238 | * 1239 | * @param pager {@link ViewPager} to attach 1240 | * @param stepCount The real page count to display (use this if you are using looped viewpager to indicate the real 1241 | * number 1242 | * of pages) 1243 | */ 1244 | public void setViewPager(ViewPager pager, int stepCount) { 1245 | if (this.pager == pager) { 1246 | return; 1247 | } 1248 | if (this.pager != null) { 1249 | pager.removeOnPageChangeListener(this); 1250 | } 1251 | if (pager.getAdapter() == null) { 1252 | throw new IllegalStateException("ViewPager does not have adapter instance."); 1253 | } 1254 | 1255 | this.pager = pager; 1256 | this.stepCount = stepCount; 1257 | currentStep = 0; 1258 | pager.addOnPageChangeListener(this); 1259 | 1260 | if (showLabels && labels == null) { 1261 | setLabelsUsingPageTitles(); 1262 | } 1263 | 1264 | requestLayout(); 1265 | invalidate(); 1266 | } 1267 | 1268 | private void setLabelsUsingPageTitles() { 1269 | PagerAdapter pagerAdapter = pager.getAdapter(); 1270 | int pagerCount = pagerAdapter.getCount(); 1271 | labels = new CharSequence[pagerCount]; 1272 | for (int i = 0; i < pagerCount; i++) { 1273 | labels[i] = pagerAdapter.getPageTitle(i); 1274 | } 1275 | } 1276 | 1277 | /** 1278 | * Pass a labels array of Charsequence that is greater than or equal to the {@code stepCount}. 1279 | * Never pass {@code null} to this manually. Call {@code showLabels(false)} to hide labels. 1280 | * 1281 | * @param labelsArray Non-null array of CharSequence 1282 | */ 1283 | public void setLabels(CharSequence[] labelsArray) { 1284 | if (labelsArray == null) { 1285 | labels = null; 1286 | return; 1287 | } 1288 | if (stepCount > labelsArray.length) { 1289 | throw new IllegalArgumentException( 1290 | "Invalid number of labels for the indicators. Please provide a list " + 1291 | "of labels with at least as many items as the number of steps required!"); 1292 | } 1293 | labels = labelsArray; 1294 | showLabels(true); 1295 | } 1296 | 1297 | public void setLabelColor(int color) { 1298 | labelPaint.setColor(color); 1299 | requestLayout(); 1300 | invalidate(); 1301 | } 1302 | 1303 | /** 1304 | * Shows the labels if true is passed. Else hides them. 1305 | * 1306 | * @param show Boolean to show or hide the labels 1307 | */ 1308 | public void showLabels(boolean show) { 1309 | showLabels = show; 1310 | requestLayout(); 1311 | invalidate(); 1312 | } 1313 | 1314 | /** 1315 | * Add the {@link OnStepClickListener} to the list of listeners which will receive events when an step is clicked. 1316 | * 1317 | * @param listener The {@link OnStepClickListener} which will be added 1318 | */ 1319 | public void addOnStepClickListener(OnStepClickListener listener) { 1320 | onStepClickListeners.add(listener); 1321 | } 1322 | 1323 | /** 1324 | * Remove the specified {@link OnStepClickListener} from the list of listeners which will receive events when an 1325 | * step is clicked. 1326 | * 1327 | * @param listener The {@link OnStepClickListener} which will be removed 1328 | */ 1329 | @SuppressWarnings("unused") 1330 | public void removeOnStepClickListener(OnStepClickListener listener) { 1331 | onStepClickListeners.remove(listener); 1332 | } 1333 | 1334 | /** 1335 | * Remove all {@link OnStepClickListener} listeners from the StepperIndicator widget. 1336 | * No more events will be propagated. 1337 | */ 1338 | @SuppressWarnings("unused") 1339 | public void clearOnStepClickListeners() { 1340 | onStepClickListeners.clear(); 1341 | } 1342 | 1343 | /** 1344 | * Check if the widget has any valid {@link OnStepClickListener} listener set for receiving events from the steps. 1345 | * 1346 | * @return {@code true} if there are listeners registered, {@code false} otherwise. 1347 | */ 1348 | public boolean isOnStepClickListenerAvailable() { 1349 | return null != onStepClickListeners && !onStepClickListeners.isEmpty(); 1350 | } 1351 | 1352 | public void setDoneIcon(@Nullable Drawable doneIcon) { 1353 | this.doneIcon = doneIcon; 1354 | if (doneIcon != null) { 1355 | showDoneIcon = true; 1356 | int size = getContext().getResources().getDimensionPixelSize(R.dimen.stpi_done_icon_size); 1357 | doneIcon.setBounds(0, 0, size, size); 1358 | } 1359 | invalidate(); 1360 | } 1361 | 1362 | public void setShowDoneIcon(boolean showDoneIcon) { 1363 | this.showDoneIcon = showDoneIcon; 1364 | invalidate(); 1365 | } 1366 | 1367 | @Override 1368 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1369 | /* no-op */ 1370 | } 1371 | 1372 | @Override 1373 | public void onPageSelected(int position) { 1374 | setCurrentStep(position); 1375 | } 1376 | 1377 | @Override 1378 | public void onPageScrollStateChanged(int state) { 1379 | /* no-op */ 1380 | } 1381 | 1382 | @Override 1383 | public void onRestoreInstanceState(Parcelable state) { 1384 | SavedState savedState = (SavedState) state; 1385 | super.onRestoreInstanceState(savedState.getSuperState()); 1386 | // Try to restore the current step 1387 | currentStep = savedState.mCurrentStep; 1388 | requestLayout(); 1389 | } 1390 | 1391 | @Override 1392 | public Parcelable onSaveInstanceState() { 1393 | Parcelable superState = super.onSaveInstanceState(); 1394 | SavedState savedState = new SavedState(superState); 1395 | // Store current stop so that it can be resumed when restored 1396 | savedState.mCurrentStep = currentStep; 1397 | return savedState; 1398 | } 1399 | 1400 | /** 1401 | * Contract used by the StepperIndicator widget to notify any listener of steps interaction events. 1402 | */ 1403 | public interface OnStepClickListener { 1404 | 1405 | /** 1406 | * Step was clicked 1407 | * 1408 | * @param step The step position which was clicked. (starts from 0, as the ViewPager bound to the widget) 1409 | */ 1410 | void onStepClicked(int step); 1411 | } 1412 | 1413 | /** 1414 | * Saved state in which information about the state of the widget is stored. 1415 | *1416 | * Use this whenever you want to store or restore some information about the state of the widget. 1417 | *
1418 | */ 1419 | private static class SavedState extends BaseSavedState { 1420 | 1421 | @SuppressWarnings("UnusedDeclaration") 1422 | public static final Parcelable.Creator