Github address: TickView, a nice little checkbox animation github.com/ChengangFen…

1. Introduction

Recently, when I was reading light mang magazine, I saw an animation with a sense of delicacy.

I happen to be watching HenCoder’s custom View tutorial (which is very detailed, there are corresponding exercises and so on), so I will strike while the iron is hot and get familiar with the knowledge I have learned.

International practice, first on the light mount magazine to mark the read animation

qingmang.gif

Do you feel very delicate after reading it?


So let’s look at my own imitation

my.gif

Is it possible to imitate a bit similar, ha ha ~, let’s look at the idea of my implementation

2. The analysis

This animation is not complicated to achieve, master a few basic custom view methods.

The idea of implementation is divided into selected state and unselected state

2.1 Unselected Status

No choice. PNG

The unchecked state is simple and requires two graphs to be drawn

  • The ring
  • tick

2.2 Selected status

Drawing the selected animation is a little more complicated, mainly including

  1. Drawing the ring progress bar is simple and can be done directly using drawArc()

  2. Drawing an animation that shrinks to the center of the circle was originally intended to be achieved by drawArc() plus strokeWidth of the brush, but the change in width was outward expansion, so this idea was decisively abandoned. And then, my idea is this, look at the picture below

    Animation analysis of contraction towards the center of a circle


    I’m going toFirst draw a yellow background, then draw a white circle on top of this layer, decreasing the radius until it reaches zero, which in turn produces a shrinking animationIt’s called reverse thinking. I read a book recently about how sometimes reverse thinking can have a different effect.

  1. Show check

    As for this square root, I did a search on the Internet, and there is no clear indication of how to draw the standard, so you can play with it freely and feel good about it. You can use it directly heredrawLine()We can do it all at once.
  2. Finally, the ring zooms in and bounces back

    Zoom rebound can be useddrawArc(), and change the width of the brush to achieve

3. Implementation

3.1 Determine the position of the progress ring and hook

According to the above analysis, the position of the progress ring and hook is unchanged whether it is selected or not, so we first determine the position of the ring and hook

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); . Radius is the radius of the circle, centerX, Set (centerx-radius, centerY - radius, centerX + radius, centerY + RADIUS); // Draw a square root of 3 points // So I use one of them firstfloatArray to record the positions of three coordinate points, // Use canvas.drawLines(mPoints, MPoint [4]~mPoint[7] mPoints[0] = centerX  - tickRadius + tickRadiusOffset; mPoints[1] = (float) centerY;
    mPoints[2] = centerX - tickRadius / 2 + tickRadiusOffset;
    mPoints[3] = centerY + tickRadius / 2;
    mPoints[4] = centerX - tickRadius / 2 + tickRadiusOffset;
    mPoints[5] = centerY + tickRadius / 2;
    mPoints[6] = centerX + tickRadius * 2 / 4 + tickRadiusOffset;
    mPoints[7] = centerY - tickRadius * 2 / 4;
}Copy the code

3.2 Define variables and mark states

Since the sorting state and the unselected state, the drawing process must determine whether the current drawing is unselected or selected.

So in this case, I’ve defined a variable isChecked

Private Boolean isChecked =false; // Expose the external interface, change the drawing state public voidsetChecked(boolean checked) {
    if (this.isChecked != checked) {
        isChecked = checked;
        reset();
    }
}Copy the code

3.3 Drawing the unselected state

I’m not going to go into details about the brushes that you draw, but when you initialize the brushes you call them when you draw them at the end

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if(! IsChecked) {// Draw the circle, mRectF is the tangent rectangle. // Since it is static, set the Angle of sweep to 360 degrees canvas.drawArc(mRectF, 90, 360,false, mPaintRing); DrawLines (mPoints, mPaintTick);return; }}Copy the code

3.4 Drawing the selected state

The selected state is an animation, so we need to call postInvalidate() and keep redrawing until the animation is finished; In addition, I use a counter here to control the progress of the drawing.

3.4.1 Drawing a Progress bar in a Ring

Here, we define a counter ringCounter with a peak value of 360 (i.e. 360 degrees). Each time we execute the onDraw() method, we auto-increment ringCounter to simulate the progress.

Finally, remember to call postInvalidate() to redraw

// counter private int ringCounter = 0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas);if(! isChecked) { ...return; RingCounter += 12; ringCounter += 12; ringCounter += 12;if (ringCounter >= 360) {
        ringCounter = 360;
    }
    canvas.drawArc(mRectF, 90, ringCounter, false, mPaintRing); . // forcibly redraw postInvalidate(); }Copy the code

After this step, the effect is shown below

Draw a ring progress bar. GIF

3.4.2 Draw an animation for shrinking to the center of a circle

The contraction of the center of the circle will not be animated until the progress of the circle reaches 100%. Similarly, the method of the counter circleCounter is also used to control the drawing time and speed

// count private int circleCounter = 0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); . // Don't start drawing until the circle reaches 100%if(ringCounter == 360) {// Draw the background circle mpaintcirc.setcolor (checkBaseColor); canvas.drawCircle(centerX, centerY, radius, mPaintCircle); Mpaintcircles.setcolor (checkTickColor); // On the background circle layer, draw a white circle (radius shrinking). CircleCounter += 6; canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle); } // postInvalidate() must be redrawn; }Copy the code

After this step, the effect is shown below

Draw an animation that shrinks to the center of a circle. GIF

The rule 3.4.3 draw hook

When the radius of the white circle shrinks to 0, it is time to draw the checkbox.

Draw the check. This is not a problem because the three coordinate points of the check have been calculated in onMeasure() and can be drawn using drawLine().

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); . canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle); // When the radius of the white circle shrinks to 0, // when the counter circleCounter is larger than the radius of the background circle, it is time to display the check √if(circleCounter >= radius + 40) {// Show a tick (plus a transparent gradient) alphaCount += 20;if(alphaCount >= 255) alphaCount = 255; mPaintTick.setAlpha(alphaCount); DrawLines (mPoints, mPaintTick); drawLines(mPoints, mPaintTick) } postInvalidate(); }Copy the code

After this step, the effect is shown below

Draw hook after effect picture. GIF

3.4.4 Draw the effect of zoom and rebound

Zoom in and bounce back. The start time should also be after the end of the contraction animation, that is, at the same time as the tick animation

Because I want to zoom in and bounce back, I set the counter here to a non-zero value, set it to 45 (optional, that’s not standard), and subtract it by 4 without redrawing it.

Finally, the brush width is the key point. The brush width is added or subtracted according to the scaleCounter

// count private int scaleCounter = 45; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); .if(circleCounter >= radius + 40) {// show check... ScaleCounter -= 4; scaleCounter -= 4;if(scaleCounter <= -45) { scaleCounter = -45; } // Zoom in on the rebound, depending on the width of the brushfloat strokeWith = mPaintRing.getStrokeWidth() + 
            (scaleCounter > 0 ? dp2px(mContext, 1) : -dp2px(mContext, 1));
        mPaintRing.setStrokeWidth(strokeWith);
        canvas.drawArc(mRectF, 90, 360, false, mPaintRing); } // When the animation is finished, it needs to be redrawnif (scaleCounter != -45) {
        postInvalidate();
    }
}Copy the code

Finish the final rendering of the last step

The final rendering

3.5 Exposing External Interfaces

In order to have the flexibility to control the state of the drawing, we can expose an interface to whether the external Settings are selected or not

/** * Whether to select */ public voidsetChecked(boolean checked) {
    if (this.isChecked != checked) {
        isChecked = checked;
        reset();
    }
}

/**
 *  重置,并重绘
 */
private void reset() {// Brush reset... // counter reset ringCounter = 0; circleCounter = 0; scaleCounter = 45; alphaCount = 0; . invalidate(); }Copy the code

3.6 Adding click events

The control is pretty much ready at this point, but it’s not particularly polished.

Think of checkBox, which can be checked or unchecked by clicking on a control without exposing an external interface, so the next step is to add click events to the control

Define an interface OnCheckedChangeListener to listen for the listener events of this control

private OnCheckedChangeListener mOnCheckedChangeListener;

public interface OnCheckedChangeListener {
    void onCheckedChanged(TickView tickView, boolean isCheck);
}

public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    this.mOnCheckedChangeListener = listener;
}Copy the code

Next, initialize the click event for the controller

Public TickView(Context Context, @nullable AttributeSet attrs, int defStyleAttr) {super(Context, attrs, defStyleAttr); .setUpEvent(); } /** * Initializes click events */ private voidsetUpEvent() {
    this.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { isChecked = ! isChecked; reset();if(mOnCheckedChangeListener ! = null) {/ / callback mOnCheckedChangeListener here. OnCheckedChanged (TickView) view, isChecked (); }}}); }Copy the code

Look at the renderings

Add click event.gif

3.7 Customizing Configuration Items

<declare-styleable name="TickView"> <! -- No selected base color --> <attr name="uncheck_base_color" format="color"/ > <! -- Selected base color --> <attr name="check_base_color" format="color"/ > <! -- Select the color of the back hook --> <attr name="check_tick_color" format="color"/ > <! -- Circle radius --> <attr name="radius" format="dimension"/ > <! -- animation execution speed --> <attr name="rate">
        <enum name="slow" value="0"/>
        <enum name="normal" value="1"/>
        <enum name="fast" value="2"/>
    </attr>
</declare-styleable>Copy the code

Here is a brief description of the animation speed configuration, here I set 3 speed, I use enumeration to define three speed configuration items

Enum TickRateEnum {// SLOW(6, 4, 2), // NORMAL speed NORMAL(12, 6, 4), // High speed FAST(20, 14, 8); public static final int RATE_MODE_SLOW = 0; public static final int RATE_MODE_NORMAL = 1; public static final int RATE_MODE_FAST = 2; Private int ringCounterUnit; private int ringCounterUnit; Private int circleCounterUnit; Private int scaleCounterUnit; private int scaleCounterUnit; public static TickRateEnum getRateEnum(int rateMode) { TickRateEnum tickRateEnum; switch (rateMode) {case RATE_MODE_SLOW:
                tickRateEnum = TickRateEnum.SLOW;
                break;
            case RATE_MODE_NORMAL:
                tickRateEnum = TickRateEnum.NORMAL;
                break;
            case RATE_MODE_FAST:
                tickRateEnum = TickRateEnum.FAST;
                break;
            default:
                tickRateEnum = TickRateEnum.NORMAL;
                break;
        }
        returntickRateEnum; }... }Copy the code

Get the configuration of the XML, get the corresponding enumeration, and get some parameters for the animation speed

/** * constructor */ public TickView(Context Context, @nullable AttributeSet attrs, int defStyleAttr) {super(Context, attrs, defStyleAttr); . initAttrs(attrs); } private void initAttrs(AttributeSet attrs) {TypedArray TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.TickView); . Int rateMode = typeDarray. getInt(r.tyleable.tickView_rate, TickRateEnum.RATE_MODE_NORMAL); mTickRateEnum = TickRateEnum.getRateEnum(rateMode); typedArray.recycle(); }Copy the code

Final Result diagram

Final Result diagram

That’s all~ Thank you for reading, and finally, the github address of the project

Github address: TickView, a nice little checkbox animation github.com/ChengangFen…