The background,

1.1. Control effect

To achieve the custom control effect is roughly as follows, the implementation process used more custom View API, feel more representative, share out also as learning summary project code has been uploaded to github github.com/DaLeiGe/And…

1.2, from the functional analysis of this control, roughly has the following characteristics

  • Random motion Particles move from the circle to the center of the circle and have an Angle difference of plus or minus 30° from the tangential direction. The transparency, radius and velocity of particles are random and disappear when the movement exceeds a certain distance or time
  • The background circle has a gradient from inside to outside
  • The circle in timing mode has a clockwise rotate animation with a color gradient
  • The color of the whole background circle changes with the Angle of the fan
  • Pointer color change
  • Digital changes are animated by switching up and down

1.3 structural analysis

This control can be broken down into two parts, the background round + digital control the combination of the two parts control, the split of the digital control separately, and a number of jumping up and down in order to facilitate animation, after all, by the location of the control drawText animation feel inconvenient, directly by the attribute of the View animation better implementation

Second, the realization of background circle

2.1. Achieve particle motion

Using AnimPoint. Java to represent moving particles, it has x,y coordinates, radius, Angle, velocity, transparency and other properties, using these properties to draw a static particle

public class AnimPoint implements Cloneable {
    /** * the x coordinate of the particle origin */
    private float mX;
    /** * the y coordinate of the particle origin */
    private float mY;
    /** * Particle radius */
    private float radius;
    /** the Angle of the particle's initial position */
    private double anger;
    /** * The speed at which a frame moves */
    private float velocity;
    /** * Total number of frames moved */
    private int num = 0;

    /** * Transparency 0~255 */
    private int alpha = 153;

    /** * Random offset Angle */
    private double randomAnger = 0;
}
Copy the code

The initial position of the particle is located in the circle of random Angle, and a particle has random radius, transparency, speed, etc. Through init() method, the particle is initialized as follows

public void init(Random random, float viewRadius) {
        anger = Math.toRadians(random.nextInt(360));
        velocity = random.nextFloat() * 2F;
        radius = random.nextInt(6) + 5;
        mX = (float) (viewRadius * Math.cos(anger));
        mY = (float) (viewRadius * Math.sin(anger));
        // Random offset Angle -30°~30°
        randomAnger = Math.toRadians(30 - random.nextInt(60));
        alpha = 153 + random.nextInt(102);
    }
Copy the code

If you want to make the particle move, you can use update to update the coordinate properties of the particle. For example, the coordinate of the particle is now (5,5). You can change the coordinate of the particle to (6,6) by update(). Then when the particle moves beyond a certain distance, or update is called more than a certain number of times, init() is called again to start the particle again on the circle for the next lifetime

public void updatePoint(Random random, float viewRadius) {
        // The offset pixel size for each frame
        float distance = 1F;
        double moveAnger = anger + randomAnger;
        mX = (float) (mX - distance * Math.cos(moveAnger) * velocity);
        mY = (float) (mY - distance * Math.sin(moveAnger) * velocity);
        // The simulation radius gradually decreases
        radius = radius - 0.02 F * velocity;
        num++;
        // If the maximum value is reached, give the moving particle a new trajectory attribute
        int maxDistance = 180;
        int maxNum = 400;
        if (velocity * num > maxDistance || num > maxNum) {
            num = 0; init(random, viewRadius); }}Copy the code

The implementation in View is roughly as follows

/** * Initialize animation */
    private void initAnim(a) {
        // Draw moving particles
        AnimPoint mAnimPoint = new AnimPoint();
        for (int i = 0; i < pointCount; i++) {
            // Use Clone to create objects to avoid duplicate creation
            AnimPoint cloneAnimPoint = mAnimPoint.clone();
            // Initialize the properties for each particle
            cloneAnimPoint.init(mRandom, mRadius - mOutCircleStrokeWidth / 2F);
            mPointList.add(cloneAnimPoint);
        }
        // Draw moving particles
        mPointsAnimator = ValueAnimator.ofFloat(0F.1F);
        mPointsAnimator.setDuration(Integer.MAX_VALUE);
        mPointsAnimator.setRepeatMode(ValueAnimator.RESTART);
        mPointsAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mPointsAnimator.addUpdateListener(animation -> {
            for (AnimPoint point : mPointList) {
                // Calculate the next coordinate of the particle through the property animation
                point.updatePoint(mRandom, mRadius);
            }
            invalidate();
        });
        mPointsAnimator.start();
    }


    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mCenterX, mCenterY);
        // Draw moving particles
        for(AnimPoint animPoint : mPointList) { mPointPaint.setAlpha(animPoint.getAlpha()); canvas.drawCircle(animPoint.getmX(), animPoint.getmY(), animPoint.getRadius(), mPointPaint); }}Copy the code

2.2. Realize the gradient circle

Use RadialGradient to gradient the circle from inside to outside as follows

float[] mRadialGradientStops = {0F.0.69 F.0.86 F.0.94 F.0.98 F.1F};
mRadialGradientColors[0] = transparentColor;
mRadialGradientColors[1] = transparentColor;
mRadialGradientColors[2] = parameter.getInsideColor();
mRadialGradientColors[3] = parameter.getOutsizeColor();
mRadialGradientColors[4] = transparentColor;
mRadialGradientColors[5] = transparentColor;
mRadialGradient = new RadialGradient(
                    0.0, mCenterX, mRadialGradientColors, mRadialGradientStops, Shader.TileMode.CLAMP); mSweptPaint.setShader(mRadialGradient); ./ / ontouch ()
canvas.drawCircle(0.0, mCenterX, mSweptPaint);
Copy the code

2.3 Show the fan-shaped area with circle background

I originally wanted to use DrawArc to achieve this effect, but DrawArc can not achieve the center of the circle, so how to achieve such an irregular shape, you can use Canvas.clippath () to cut the irregular shape, so as long as the Path of the fan can be achieved. This can be done by closing the path with the dot + arc

/** * Draw fan path **@paramRadius of r *@paramStartAngle startAngle *@paramSweepAngle indicates the sweepAngle */
private void getSectorClip(float r, float startAngle, float sweepAngle) {
        mArcPath.reset();
        mArcPath.addArc(-r, -r, r, r, startAngle, sweepAngle);
        mArcPath.lineTo(0.0);
        mArcPath.close();
    }

// Then in onDraw(), crop the canvas
 canvas.clipPath(mArcPath);
Copy the code

2.4. Realize pointer color change

As for how to change the color of the bitmap pointer image, the original solution is to use AvoidXfermode to change the color of the specified pixel channel range. But AvoidXfermode has been removed in API 24, so this scheme doesn’t work. Finally, layer blend mode is used to color-change the pointer image

The bitmap color can be realized by porterduff.mode. MULTIPLY Mode. The source image is the pointer color to be modified, and the target image is the white pointer

/** * Initialize the pointer image Bitmap */
    private void initBitmap(a) {
        float f = 130F / 656F;
        mBitmapDST = BitmapFactory.decodeResource(getResources(), R.drawable.indicator);
        float mBitmapDstHeight = width * f;
        float mBitmapDstWidth = mBitmapDstHeight * mBitmapDST.getWidth() / mBitmapDST.getHeight();
        // Initialize the layer blend mode for the pointer
        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
        mPointerRectF = new RectF(0.0, mBitmapDstWidth, mBitmapDstHeight);
        mBitmapSRT = Bitmap.createBitmap((int) mBitmapDstWidth, (int) mBitmapDstHeight, Bitmap.Config.ARGB_8888);
        mBitmapSRT.eraseColor(mIndicatorColor);
    }

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        / / pointer
       canvas.translate(mCenterX, mCenterY);
       canvas.rotate(mCurrentAngle / 10F);
       canvas.translate(-mPointerRectF.width() / 2, -mCenterY);
       mPointerLayoutId = canvas.saveLayer(mPointerRectF, mBmpPaint);
       mBitmapSRT.eraseColor(mIndicatorColor);
       canvas.drawBitmap(mBitmapDST, null, mPointerRectF, mBmpPaint);
       mBmpPaint.setXfermode(mXfermode);
       canvas.drawBitmap(mBitmapSRT, null, mPointerRectF, mBmpPaint);
       mBmpPaint.setXfermode(null);
       canvas.restoreToCount(mPointerLayoutId);
    }
Copy the code

2.5. The color of the background circle changes with the Angle of the fan

The circular control is divided into 3600°, each Angle corresponds to a specific color value of the control, so how to calculate the specific Angle of his color value? Android reference attribute discoloration of animation animation. Animation. ArgbEvaluator implementation approach, the calculation of the two color one specific color value way points are as follows

public Object evaluate(float fraction, Object startValue, Object endValue) {
     int startInt = (Integer) startValue;
     float startA = ((startInt >> 24) & 0xff) / 255.0 f;
     float startR = ((startInt >> 16) & 0xff) / 255.0 f;
     float startG = ((startInt >>  8) & 0xff) / 255.0 f;
     float startB = ( startInt        & 0xff) / 255.0 f;

     int endInt = (Integer) endValue;
     float endA = ((endInt >> 24) & 0xff) / 255.0 f;
     float endR = ((endInt >> 16) & 0xff) / 255.0 f;
     float endG = ((endInt >>  8) & 0xff) / 255.0 f;
     float endB = ( endInt        & 0xff) / 255.0 f;

     // convert from sRGB to linear
     startR = (float) Math.pow(startR, 2.2);
     startG = (float) Math.pow(startG, 2.2);
     startB = (float) Math.pow(startB, 2.2);

     endR = (float) Math.pow(endR, 2.2);
     endG = (float) Math.pow(endG, 2.2);
     endB = (float) Math.pow(endB, 2.2);

     // compute the interpolated color in linear space
     float a = startA + fraction * (endA - startA);
     float r = startR + fraction * (endR - startR);
     float g = startG + fraction * (endG - startG);
     float b = startB + fraction * (endB - startB);

     // convert back to sRGB in the [0..255] range
     a = a * 255.0 f;
     r = (float) Math.pow(r, 1.0 / 2.2) * 255.0 f;
     g = (float) Math.pow(g, 1.0 / 2.2) * 255.0 f;
     b = (float) Math.pow(b, 1.0 / 2.2) * 255.0 f;

     return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
 }

Copy the code

Fraction = progressValue % 900/900; Then determine which color values the current Angle is in, Through the android. Animation. ArgbEvaluator. Evaluate (float fraction, Object startValue, Object endValue) would go back to the specific color values General realization process is as follows

private ProgressParameter getProgressParameter(float progressValue) {
        float fraction = progressValue % 900 / 900;
        if (progressValue < 900) {
            // The first color segment
            mParameter.setInsideColor(evaluate(fraction, insideColor1, insideColor2));
            mParameter.setOutsizeColor(evaluate(fraction, outsizeColor1, outsizeColor2));
            mParameter.setProgressColor(evaluate(fraction, progressColor1, progressColor2));
            mParameter.setPointColor(evaluate(fraction, pointColor1, pointColor2));
            mParameter.setBgCircleColor(evaluate(fraction, bgCircleColor1, bgCircleColor2));
            mParameter.setIndicatorColor(evaluate(fraction, indicatorColor1, indicatorColor2));
        } else if (progressValue < 1800) {
            // The second color segment
            mParameter.setInsideColor(evaluate(fraction, insideColor2, insideColor3));
            mParameter.setOutsizeColor(evaluate(fraction, outsizeColor2, outsizeColor3));
            mParameter.setProgressColor(evaluate(fraction, progressColor2, progressColor3));
            mParameter.setPointColor(evaluate(fraction, pointColor2, pointColor3));
            mParameter.setBgCircleColor(evaluate(fraction, bgCircleColor2, bgCircleColor3));
            mParameter.setIndicatorColor(evaluate(fraction, indicatorColor2, indicatorColor3));
        } else if (progressValue < 2700) {
            // The third color segment
            mParameter.setInsideColor(evaluate(fraction, insideColor3, insideColor4));
            mParameter.setOutsizeColor(evaluate(fraction, outsizeColor3, outsizeColor4));
            mParameter.setProgressColor(evaluate(fraction, progressColor3, progressColor4));
            mParameter.setPointColor(evaluate(fraction, pointColor3, pointColor4));
            mParameter.setBgCircleColor(evaluate(fraction, bgCircleColor3, bgCircleColor4));
            mParameter.setIndicatorColor(evaluate(fraction, indicatorColor3, indicatorColor4));
        } else {
            // The fourth color section
            mParameter.setInsideColor(evaluate(fraction, insideColor4, insideColor5));
            mParameter.setOutsizeColor(evaluate(fraction, outsizeColor4, outsizeColor5));
            mParameter.setProgressColor(evaluate(fraction, progressColor4, progressColor5));
            mParameter.setPointColor(evaluate(fraction, pointColor4, pointColor5));
            mParameter.setBgCircleColor(evaluate(fraction, bgCircleColor4, bgCircleColor5));
            mParameter.setIndicatorColor(evaluate(fraction, indicatorColor4, indicatorColor5));
        }
        return mParameter;
    }

Copy the code

Three, jump digital animation realization

3.1, property animation +2 TextView to achieve digital up and down switching animation

Realize digital switch animation, originally intended to use RecycleView to achieve, but considering the dynamic effect in the future may face UI little sister all kinds of call operation, so the final decision to use two TextView to do up and down translation animation, so high controllability, the View to perform attribute animation is simple

NumberView wraps two TextViews with FrameLayout, widget_progress_number_ITEM_layout.xml


      
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_number_one"
        style="@style/progress_text_font"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:padding="0dp"
        android:text="0"
        android:textColor="@android:color/white" />

    <TextView
        style="@style/progress_text_font"
        android:id="@+id/tv_number_tow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="1"
        android:textColor="@android:color/white" />
</FrameLayout>
Copy the code

We then use the property animation to control the two TextViews to switch up and down

mNumberAnim = ValueAnimator.ofFloat(0F.1F);
        mNumberAnim.setDuration(400);
        mNumberAnim.setInterpolator(new OvershootInterpolator());
        mNumberAnim.setRepeatCount(0);
        mNumberAnim.setRepeatMode(ValueAnimator.RESTART);
        mNumberAnim.addUpdateListener(animation -> {
            float value = (float) animation.getAnimatedValue();
            if (UP_OR_DOWN_MODE == UP_ANIMATOR_MODE) {
                // Make the number bigger and move it down
                mTvFirst.setTranslationY(-mHeight * value);
                mTvSecond.setTranslationY(-mHeight * value);
            } else {
                // Make the number smaller and move up
                mTvFirst.setTranslationY(mHeight * value);
                mTvSecond.setTranslationY(-2* mHeight + mHeight * value); }});Copy the code

So NumberView is able to change one digit by moving the animation up and down, with the ten hundred digits and the clock colon and then the container layout, the AnimNumberView combination layout is able to represent the time and the ten hundred digits

Four, the project source code

Blog just roughly about the realization of ideas, specific implementation please read the source github.com/DaLeiGe/And…

This article for the original blogger, reprint please indicate the source