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, to share out also as learning summary project code has been uploaded to Github :github.com/DaLeiGe/And…

1.2, from the function of the analysis of this control, roughly have the following characteristics
  • Random motion Particles move from the circle to the center of the circle, and there is an Angle difference of plus or minus 30° with the tangent direction. Particle transparency, radius and movement speed are random, and the movement disappears after a certain distance or time
  • The background circle has a gradient from the inside out
  • The circle in timing mode has a clockwise rotate animation with a color gradient
  • The color of the entire background circle varies with the Angle of the sector
  • 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, background circle realization

2.1. Realize particle motion

Using animpoint.java to represent moving particles, it has x and Y coordinates, radius, Angle, speed of motion, transparency, etc., from which it is possible to draw a static particle

Public class implements Cloneable {/** * private float mX; /** * private float mY; /** * private float radius; /** * private double anger; /** * private float velocity; /** * private int num = 0; Private int alpha = 1; private int alpha = 2; /** * private double randomAnger = 0; }Copy the code

The initial position of a particle is at a random Angle of the circle, and a particle has a random radius, transparency, velocity, etc. Init () is used to initialize the particle 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)); // -30°~30° randomAnger = math.toradians (30-random.nextint (60)); alpha = 153 + random.nextInt(102); }Copy the code

For example, the particle’s coordinates are now at (5,5) ‘ ‘. Change the particle’s coordinates to (6,6) by using update(). Change the particle’s coordinates to (6,6) by using update(). Then, when the particle moves beyond a certain distance, or when update is called more than a certain number of times, init() is called again to restart the particle from the circle for the next life cycle

Public void updatePoint(Random Random, float viewRadius) {float distance = 1F; double moveAnger = anger + randomAnger; mX = (float) (mX - distance * Math.cos(moveAnger) * velocity); mY = (float) (mY - distance * Math.sin(moveAnger) * velocity); Radius = radius-0.02f * velocity; num++; Int maxDistance = 180; int maxNum = 400; if (velocity * num > maxDistance || num > maxNum) { num = 0; init(random, viewRadius); }}Copy the code

The general implementation in View is as follows

Private void initAnim() {// Render the moving particle AnimPoint mAnimPoint = new AnimPoint(); for (int i = 0; i < pointCount; I++) {cloneAnimPoint = manimpoint.clone (); Cloneanimpoint. init(mRandom, radius-moutcircleStrokeWidth / 2F); mPointList.add(cloneAnimPoint); } // mPointsAnimator = valueanimator.offloat (0F, 1F); mPointsAnimator.setDuration(Integer.MAX_VALUE); mPointsAnimator.setRepeatMode(ValueAnimator.RESTART); mPointsAnimator.setRepeatCount(ValueAnimator.INFINITE); mPointsAnimator.addUpdateListener(animation -> { for (AnimPoint point : UpdatePoint (mRandom, mRadius); mPointList (mPointList, mRadius); } invalidate(); }); mPointsAnimator.start(); } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCenterX, mCenterY); For (AnimPoint AnimPoint: mPointList) {mpointpaint.setalpha (animpoint.getalpha ()); canvas.drawCircle(animPoint.getmX(), animPoint.getmY(), animPoint.getRadius(), mPointPaint); }}Copy the code
2.2. Realize asymptotic circle

To achieve circle gradient from inside to outside using RadialGradient is roughly implemented as follows

Float [] mRadialGradientStops = {0F, 0.69f, 0.86f, 0.94f, 0.98f, 1F}; float[] mRadialGradientStops = {0F, 0.69f, 0.86f, 0.98f, 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); . //onDraw() draw canvas.drawCircle(0, 0, mCenterX, mSweptPaint);Copy the code
2.3. Display the fan-shaped area of the background circle

We wanted to do this with DrawArc, but DrawArc couldn’t reach the center of the circle

You can use Canvas.clippath () to cut the irregular shape, so as long as you get the fan-shaped Path, by dot + arc and then closed Path can be achieved

Path ** @param r radius * @param startAngle * @param sweepAngle */ private void getSectorClip(float r, 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(), cut the canvas canvas.clippath (mArcPath);Copy the code
2.4 achieve pointer color change

Pointer is irregular shape and cannot be realized by drawing geometry, so drawBitmap is used to achieve it

As for how to implement color change on bitmap pointer images, the original plan was to use AvoidXfermode to change color within the specified pixel channel, but AvoidXfermode has been removed in API 24, so this doesn’t work

Finally, layer blending mode is adopted to realize pointer image color change

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

Private void initBitmap() {float f = 130F / 656F; float f = 130F / 656F; mBitmapDST = BitmapFactory.decodeResource(getResources(), R.drawable.indicator); float mBitmapDstHeight = width * f; float mBitmapDstWidth = mBitmapDstHeight * mBitmapDST.getWidth() / mBitmapDST.getHeight(); MXfermode = new PorterDuffXfermode(porterduff.mode.multiply); mXfermode = new 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); // 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 sector

The circular control is divided into 3600 degrees, and each Angle corresponds to a specific color value of the control. Then how to calculate the specific color value of a specific Angle?

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.0f; float startA = ((startInt >> 24) & 0xff) / 255.0f; Float startR = ((startInt >> 16) & 0xff) / 255.0f; float startR = ((startInt >> 16) & 0xff) / 255.0f; Float startG = ((startInt >> 8) & 0xff) / 255.0f; float startG = ((startInt >> 8) & 0xff) / 255.0f; Float startB = (startInt & 0xff) / 255.0f; float startB = (startInt & 0xff) / 255.0f; int endInt = (Integer) endValue; Float endA = ((endInt >> 24) & 0xff) / 255.0f; float endA = (endInt >> 24) & 0xff) / 255.0f; Float endR = ((endInt >> 16) & 0xff) / 255.0f; float endR = ((endInt >> 16) & 0xff) / 255.0f; Float endG = ((endInt >> 8) &0xff) / 255.0f; float endG = ((endInt >> 8) &0xff) / 255.0f; Float endB = (endInt & 0xff) / 255.0f; float endB = (endInt & 0xff) / 255.0f; // 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.0f; R = (float) math.pow (r, 1.0/2.2) * 255.0f; G = (float) math.pow (g, 1.0/2.2) * 255.0f; B = (float) math.pow (b, 1.0/2.2) * 255.0f; return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b); }Copy the code

Fraction = progressValue % 900/900; progressValue % 900/900; Then determine which color value 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

The general implementation process is as follows

private ProgressParameter getProgressParameter(float progressValue) { float fraction = progressValue % 900 / 900; If (progressValue < 900) {/ / the first color segment mParameter setInsideColor (the 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 (the 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 (the 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 {/ / fourth color segment mParameter setInsideColor (the 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, dance digital animation

3.1. Attribute animation +2 TextView to realize digital up-down switching animation

Realize digital switching animation, originally intended to use RecycleView to achieve, but considering the dynamic effect in the future may face various UI sister operations, so the final decision to use two TextView to do up and down translation animation, so high controllability, the View property animation is also simple

NumberView uses FrameLayout to wrap two TextViews, widgeT_Progress_number_item_layout.xml

<? The XML version = "1.0" encoding = "utf-8"? > <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

The two TextViews are then toggled up and down using property animation

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) {mtvfirst.settranslationy (-mheight * value); mTvSecond.setTranslationY(-mHeight * value); } else {// The number is smaller, move up mtvfirst.settranslationy (mHeight * value); mTvSecond.setTranslationY(-2 * mHeight + mHeight * value); }});Copy the code

So NumberView is able to do one digit change up and down animation that has a hundred digits and a colon on the clock through the container layout of the AnimNumberView combination layout that represents time and hundreds of digits

4. Project source code

Blog just about the implementation ideas, specific implementation please read the source github.com/DaLeiGe/And… Original link: juejin.cn/post/698407…

At the end of the article

Your likes collection is the biggest encouragement to me! Welcome to follow me, share Android dry goods, exchange Android technology. If you have any comments or technical questions about this article, please leave a comment in the comments section.