preface

In the App now design, shuffling basic became the “standard” for each application, a wheel, it needs to have a corresponding indicator, represent the current round of progress, the style of the indicator on the market now is mostly based on the form of dots, to implement the basic online also has a lot of wheels, the effect of this article is mainly on the basis of basic effect in the implementation, Add a sticky transition animation between switch dots.

Results the preview

Implementation approach

Draw the dot

Dot words based on the brush drawing, the control width is divided into N equal parts, and the selected dot radius is slightly larger.

The linkage between the dots

The maximum number of dots can be set to N. When the total number of dots exceeds N, they are not displayed in the visible range of the control temporarily. When the left/right scroll to the edge, all dots will be automatically translated, so that the newly selected dots will return to the middle position again. Implemented using property animation combined with abscissa offset.

Dot transition animation

If you simply switch between dots and dots, it will appear a little rigid, so we need to add some transitional animation effects for this process. Here we use a common “sticky” effect, similar to the effect of dragging the number of unread messages in the QQ contact list:

This is based on bezier curves. By calculating the position of the two dots preparing for the transition and the center point between them, the upper and lower Bezier curves can be drawn and then closed. Then move with the property animation to complete the final transition effect.

Implementation steps

1. Calculate the width and height of the control

The width and height of the control depends on the arrangement of the dots:

Control width = width of all dots visible on the screen * Number of dots visible + Spacing between dots * (Number of dots visible – 1)

Control height = height of the largest dot

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int count = Math.min(totalCount, showCount);
    int width = (int) ((count - 1) * smallDotWidth + (count - 1) * dotPadding + bigDotWidth);
    int height = (int) bigDotWidth;
    final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    final int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width, height);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, height);
    } else{ setMeasuredDimension(width, height); }}Copy the code

Int count = math.min (totalCount, showCount) If the current total number of dots exceeds the number of visible dots on the screen, the control’s width is calculated based on the maximum number of visible dots.

2. Draw small dots

Once you know the number of dots, you just need to walk through the drawing sequence. In consideration of the difference between the selected dot and other dot styles, we set the width bigDotWidth and the color selectColor separately for the currently selected dot as follows:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    float startX = curX;
    float selectX = 0;
    for (int i = 0; i < totalCount; i++) {
        if (curIndex == i) {
            // Draw the selected dot
            paint.setColor(selectColor);
            paint.setStyle(Paint.Style.FILL);
            
            selectRectF.left = startX;
            selectRectF.top = getHeight() / 2f - bigDotWidth / 2;
            selectRectF.right = startX + bigDotWidth;
            selectRectF.bottom = getHeight() / 2f + bigDotWidth / 2;
            canvas.drawCircle(startX + (bigDotWidth) / 2, bigDotWidth / 2, (bigDotWidth) / 2, paint);
            selectX = startX + bigDotWidth / 2;
            startX += (bigDotWidth + dotPadding);
        } else {
            // Draw other dots
            paint.setColor(defaultColor);
            paint.setStyle(Paint.Style.FILL);

            startX += smallDotWidth / 2;
            canvas.drawCircle(startX, bigDotWidth / 2, (smallDotWidth) / 2, paint);
            startX += (smallDotWidth / 2+ dotPadding); }}}Copy the code

3. Pan animations left and right

It can be seen from the renderings that the trigger time of the indicator translation lies in the left and right switching of each time, which needs to meet the following conditions:

The current number of dots exceeds the maximum number of visible dots. 2. The next dot to be switched is not in the middle of the screen

The first condition, the total number of dots is greater than the maximum number of visible dots that can be shifted, and that makes sense. The second is to switch the next dot to a non-middle position on the screen. This is a rule for panning, as shown in the following example:

Above before switching, the selected is 3, ready to switch to the four process, because the current total of 7, more than most visible number five, satisfy the first condition, at the same time as before to switch 4 is in the screen in the middle position, thus to meet the second condition, need translation left a whole unit, made after the switch, 4 became the center of the screen, The logic is as follows:

public void setCurIndex(int index) {
    if (index == curIndex) {
        return;
    }
    // The current total number of dots exceeds the maximum number of visible dots
    if (totalCount > showCount) {
        if (index > curIndex) {
            // Slide to the left
            int start = showCount % 2= =0 ? showCount/2 - 1 : showCount / 2;
            int end = totalCount - showCount / 2;
            // Determine if you need to scroll first
            if (index > start && index < end) {
                startScrollAnim(Duration.LEFT, () -> invalidateIndex(index));
            } else{ invalidateIndex(index); }}else {
            // Slide to the right
            int start = showCount / 2;
            int end = showCount % 2= =0 ? totalCount - showCount / 2 + 1 : totalCount - showCount / 2;
            // Determine if you need to scroll first
            if (index > start - 1 && index < end - 1) {
                startScrollAnim(Duration.RIGHT, () -> invalidateIndex(index));
            } else{ invalidateIndex(index); }}}else{ invalidateIndex(index); }}Copy the code

4. Dot transition animation

The viscous animation between dots is essentially that one previous dot is used as the reference position, and then the horizontal position of another dot is shifted, so that the closed curve between them gradually changes until it is shifted to coincide with the position of the next dot, as follows:

By A red dot, switch to the green dot in the process, to A point as the starting point, the connection point A and C point draw A bezier curve, also, the bottom between B and D also draw A bezier curve, and then connect the AB and CD, four forms A closed curve path mapped, form the basic shape. Then, in combination with the property animation, points C and D continue to move to the right until they completely coincide with the green circle. As follows:

Set the start and end values for the sticky properties animation:

// The horizontal center of the currently selected dot serves as the starting point of the sticky animation
float startValues = getCurIndexX() + bigDotWidth / 2;
// Set the end value of the animation according to the direction
if (index > curIndex) {
    stickAnimator.setFloatValues(startValues, startValues + dotPadding + smallDotWidth);
} else {
    stickAnimator.setFloatValues(startValues, startValues - dotPadding - smallDotWidth);
}
Copy the code

Listen to the animation constantly refresh the viscosity transition animation value:

ValueAnimator stickAnimator = new ValueAnimator();
stickAnimator.setDuration(animTime);
stickAnimator.addUpdateListener(animation -> {
    stickAnimX = (float) animation.getAnimatedValue();
    invalidate();
});
stickAnimator.removeAllListeners();
stickAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        isSwitchFinish = true; invalidate(); }}); stickAnimator.start();Copy the code

Draw viscosity curve:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    if (isSwitchFinish) {
        // Remember to reset the path after switching
        stickPath.reset();
    } else {
        paint.setColor(selectColor);
        // Draw with the currently selected dot as the starting point
        float quadStartX = selectX;
        float quadStartY = getHeight() / 2f - bigDotWidth / 2;
        stickPath.reset();
        // Connect 4 points
        stickPath.moveTo(quadStartX, quadStartY);
        stickPath.quadTo(quadStartX + (stickAnimX - quadStartX) / 2, bigDotWidth / 2, stickAnimX, quadStartY);
        stickPath.lineTo(stickAnimX, quadStartY + bigDotWidth);
        stickPath.quadTo(quadStartX + (stickAnimX - quadStartX) / 2, bigDotWidth / 2, quadStartX, quadStartY + bigDotWidth);
        // Form a closed curve
        stickPath.close();
        // Draw the transition circle
        canvas.drawCircle(stickAnimX, bigDotWidth / 2, (bigDotWidth) / 2, paint); canvas.drawPath(stickPath, paint); }}Copy the code

conclusion

If you specify the maximum number of dots to display, scroll left and right when the total number exceeds, or set to the maximum number of circles if you want non-scroll. This control is mainly through the Bezier curve to produce viscous effect, so that animation is more vivid, support to set whether to open viscous effect, viscous animation duration, small dots selected and not selected when the style, etc., will later expand other details according to demand. Full code has been uploaded to Github: a set of practical cool custom View library (including source code and demo) including common pay, scan, unlock animation, cool turntable menu effects, welcome issue and star~