1, Background
1.1 control effect
1.2. Analyze the function of this control. It has the following characteristics
- The randomly moving particles move from the circumference to the center of the circle and have an angle difference of plus or minus 30 ° with the tangent direction. The transparency, radius and speed of the particles are random, and the particles move beyond a certain distance or disappear in time
- The background circle has an inside out gradient
- In timing mode, the ring has a clockwise rotate animation with a color gradient
- The color of the whole background circle changes with the fan angle
- Pointer color change
- The number change is an animation that switches up and down
1.3 structural analysis
This control can be divided into two parts. It is a combined control composed of background circle and digital control. The reason why the digital control is separated separately is also to facilitate the animation of numbers jumping up and down. After all, it is inconvenient to realize the animation by controlling the position of drawText. It is better to realize it directly through the attribute animation of View
2, Background circle implementation
2.1. Realize particle movement
Use animpoint Java represents moving particles. It has attributes such as X and Y coordinates, radius, angle, motion speed and transparency. Through these attributes, you can draw a static particle
public class AnimPoint implements Cloneable { /** * x coordinate of particle origin */ private float mX; /** * Particle origin y coordinate */ 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 frames moved */ private int num = 0; /** * Transparency 0 ~ 255 */ private int alpha = 153; /** * Random offset angle */ private double randomAnger = 0; } Copy code
The initial position of the particle is located on the circumference of a random angle, and a particle has a random radius, transparency, velocity, etc. the initialization of the particle is realized through the init() method, 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 code
If you want the particles to move, you can use update to update these coordinate attributes of the particles. For example, if the coordinates of the particles are now (5,5), you can change the coordinates of the particles to (6,6) through update(). If you keep calling update() in combination with the attribute animation, you can constantly change the coordinates of X and y to realize the particle movement. Then when the particles move more than a certain distance, or call update more than a certain number of times, Then call init() again to make the particles move again from the circle to the next life cycle
public void updatePoint(Random random, float viewRadius) { //Pixel size of each frame offset 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 decreases gradually radius = radius - 0.02F * velocity; num++; //If the maximum value is reached, give the moving particles a track attribute again int maxDistance = 180; int maxNum = 400; if (velocity * num > maxDistance || num > maxNum) { num = 0; init(random, viewRadius); } } Copy code
In View, it is roughly implemented as follows
/** * Initialize animation */ private void initAnim() { //Paint moving particles AnimPoint mAnimPoint = new AnimPoint(); for (int i = 0; i < pointCount; i++) { //Create objects through clone to avoid duplicate creation AnimPoint cloneAnimPoint = mAnimPoint.clone(); //First initialize various attributes 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) { //Continuously calculate the next coordinate of the next particle through attribute 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 code
2.2. Realize gradient circle
To realize the gradient of circle from inside to outside, use RadialGradient. The general implementation method is as follows
float[] mRadialGradientStops = {0F, 0.69F, 0.86F, 0.94F, 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 code
2.3 sector area showing background circle
Originally, I wanted to achieve this effect through DrawArc, but DrawArc cannot achieve the area of the center of the circle. How to achieve such an irregular shape, you can use canvas Clippath() cuts irregular shapes, so as long as you get a fan-shaped path, you can close the path through a dot + arc
/** * Draw fan path * * @param r radius * @param startAngle Start angle * @param sweepAngle Swept angle */ 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 code
2.4. Realize pointer discoloration
The pointer is irregular and cannot be realized by drawing geometry, so drawBitmap is selected to realize the color change of bitmap pointer picture. The original scheme is to use AvoidXfermode to change the color within the specified pixel channel, but AvoidXfermode has been removed in API 24, Therefore, this scheme is invalid. Finally, the layer mixing mode is adopted to realize the color change of pointer image
Via porterduff Mode. Multi mode can realize bitmap color. The source image is the pointer color to be modified, and the target image is the white pointer. The color change can be realized by acquiring the overlapping part of the two images, which is roughly as follows
/** * Initialize Bitmap of pointer picture */ private void initBitmap() { float f = 130F / 656F; mBitmapDST = BitmapFactory.decodeResource(getResources(), R.drawable.indicator); float mBitmapDstHeight = width * f; float mBitmapDstWidth = mBitmapDstHeight * mBitmapDST.getWidth() / mBitmapDST.getHeight(); //Initializes the layer blending mode of 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); //Draw 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 code
2.5. Realize the change of background circle color with sector angle
Disassemble the circular control into 3600 °, each angle corresponds to a specific color value of the control, so how to calculate the specific color value of a specific angle? Refer to the color changing animation Android animation. Argbevaluator is implemented to calculate the color value of a specific point in two colors as follows
public Object evaluate(float fraction, Object startValue, Object endValue) { int startInt = (Integer) startValue; float startA = ((startInt >> 24) & 0xff) / 255.0f; float startR = ((startInt >> 16) & 0xff) / 255.0f; float startG = ((startInt >> 8) & 0xff) / 255.0f; float startB = ( startInt & 0xff) / 255.0f; int endInt = (Integer) endValue; float endA = ((endInt >> 24) & 0xff) / 255.0f; float endR = ((endInt >> 16) & 0xff) / 255.0f; float endG = ((endInt >> 8) & 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 code
There are four color segments in the control, 3600 / 4 = 900, so fraction = progressvalue% 900 / 900; The current angle of Android is used to determine the value in the current segment animation. ArgbEvaluator. Evaluate (float fraction, object startvalue, object endvalue) can go back. The specific color values are roughly implemented as follows
private ProgressParameter getProgressParameter(float progressValue) { float fraction = progressValue % 900 / 900; if (progressValue < 900) { //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) { //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) { //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 { //Fourth color segment 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 code
3, Realization of jumping digital animation
3.1. Attribute animation + 2 textviews to realize digital up-down switching animation
It was originally intended to use RecycleView to realize the digital switching animation, but considering that the dynamic effect may face various operations of the little sister of the UI in the future, it was finally decided to use two textviews to do the up and down translation animation, which is highly controllable and easy to perform the attribute animation on the View
NumberView uses FrameLayout to wrap two textviews and widgets_ progress_ number_ item_ layout. xml
<?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 code
Then control the up and down switching of the two textviews through attribute 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) { //The number gets bigger and moves down mTvFirst.setTranslationY(-mHeight * value); mTvSecond.setTranslationY(-mHeight * value); } else { //The number gets smaller and moves up mTvFirst.setTranslationY(mHeight * value); mTvSecond.setTranslationY(-2 * mHeight + mHeight * value); } }); Copy code
In this way, NumberView can realize the change of one digit, which is the animation of switching up and down. If there are ten hundred digits and clock colon, the container layout AnimNumberView can realize the combination layout to represent the time and ten hundred digits
4, Project source code
The blog just talks about the implementation idea. Please read the source code for the specific implementation github.com/DaLeiGe/And...
Author: chenyangqi
Link: https://juejin.cn/post/6984079687739768840
Source: Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.