Android implements a cool custom View clock

Posted by phpprog on Sun, 16 Jan 2022 05:29:00 +0100

1, Background

1.1 control effect

*** github.com/DaLeiGe/And...

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.

Topics: Android