preface

HenCoder’s custom View is very good. Do you have an idea? Don’t you want to do it? Without saying a word, I looked at my friend’s mobile phone effect and said to him: it is not difficult to implement, using the basic usage of displacement, zoom, gradient animation and custom View. Well, I will implement it, just to deepen the understanding of custom View.

Material preparation

After downloading jike APP and decompressing it in the way of decompression package, there are three pictures of thumbs-up effect, one is a picture of a small hand without thumbs-up, another is a picture of a red hand after thumbs-up, and the last one is a picture of four points on the finger after thumbs-up:

Practice thinking

rendering

A concrete analysis

View width = small hand image width + digital text width + 30px
View height = the height of the hand image (because finger height is higher than digital text height) + 20px

X = (15px)
Y = (whole image height – hand image height) / 2 – Highlight image height + 17px

TextX = width of the hand + 20px + width of the character on the right
TextY = Height of View + half height of text area

The specific implementation

Initialize the

Add the following attributes to the attrs file under values:

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <! --name is the set of declared attributes, can be arbitrarily selected, the best is the same as the name of the custom View, so convenient management -->
    <declare-styleable name="JiKeLikeView">
        <! Attribute like_number = like_number -->
        <attr name="like_number" format="integer"/>
    </declare-styleable>

</resources>
Copy the code

Because the likes only involve numbers, declare and define integers. Create a class that inherits View, and in the constructor, read attrs configuration properties:

public JiKeLikeView(Context context) {
        this(context, null);
    }

    public JiKeLikeView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public JiKeLikeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // Get attrs configuration attributes
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.JiKeLikeView);
        The first parameter is the fixed format of the property in the property set. R.styleable+ the custom property name
        // The second argument, if this property is not set, the default value is taken
        likeNumber = typedArray.getInt(R.styleable.JiKeLikeView_like_number, 1999);
        // Remember to recycle the TypedArray object
        typedArray.recycle();
        init();
    }
Copy the code

Init method is to initialize some brush, text display range

private void init(a) {
        // Create a text display range
        textRounds = new Rect();
        // The number of likes is currently 8
        widths = new float[8];
        //Paint.ANTI_ALIAS_FLAG is bitmap antialiasing
        //bitmapPaint is an image brush
        bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // This is the brush that draws the original number before it was liked, 45 before it was liked, 46 after it was liked, 45 before it was liked
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        oldTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // Text color size Set color gray font size to 14
        textPaint.setColor(Color.GRAY);
        textPaint.setTextSize(SystemUtil.sp2px(getContext(), 14));
        oldTextPaint.setColor(Color.GRAY);
        oldTextPaint.setTextSize(SystemUtil.sp2px(getContext(), 14));
        // Round brush initializes paint.style. STROKE to draw only the outline of the graph
        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setColor(Color.RED);
        circlePaint.setStyle(Paint.Style.STROKE);
        // Set the outline width
        circlePaint.setStrokeWidth(SystemUtil.dp2px(getContext(), 2));
        // The first parameter is the blur radius, the larger the blur, the second parameter is the horizontal offset distance of the shadow, positive offset downward offset negative offset upward
        // The third parameter is the vertical offset distance, positive offset downward, negative offset upward and the fourth parameter is the color of the brush
        circlePaint.setShadowLayer(SystemUtil.dp2px(getContext(), 1), SystemUtil.dp2px(getContext(), 1), SystemUtil.dp2px(getContext(), 1), Color.RED);

    }
Copy the code

Create a Bitmap object on the onAttachedToWindow method

    /** * This method is called when the Activity resumes. Each view is called only once when the window corresponding to the Activity is added. Some initialization operations can be performed */
    @Override
    protected void onAttachedToWindow(a) {
        super.onAttachedToWindow();
        Resources resources = getResources();
        // Construct a Bitmap object, using the static Bitmap decodeResource of the BitmapFactory class to parse into a Bitmap based on the given resource ID
        unLikeBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_unlike);
        likeBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_like);
        shiningBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_message_like_shining);
    }
Copy the code

The code above comes with an explanation of why you should build in this method instead of init. Also, the Bitmap is recycled in the onDetachedFromWindow method

/** * corresponds to onAttachedToWindow, which is called */ when destroying View
    @Override
    protected void onDetachedFromWindow(a) {
        super.onDetachedFromWindow();
        / / recycling bitmap
        unLikeBitmap.recycle();
        likeBitmap.recycle();
        shiningBitmap.recycle();
    }
Copy the code

Construct three Bitmap objects, the above analysis is very clear, one is the four points on the hand, one is the thumbs-up hand, the last one is not the thumbs-up hand.

Calculate wide high

    /** * Measure width and height * These two parameters are calculated by the parent and passed to the child *@paramWidthMeasureSpec width *@paramHeightMeasureSpec highly * /
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //MeasureSpec is composed of both specMode and specSize. The onMeasure parameters have different functions according to the specMode.
        // When specMode is EXACTLY, the size of the subview is set according to the size of the specSize, for match_parent or exact size values in the layout parameter
        // When specMode is AT_MOST, these two parameters only indicate the maximum size that the subview can currently use. The actual size of the subview is not necessarily specSize. So when we customize the View, we override the onMeasure method mainly to set a default size for the subview in AT_MOST mode, for the layout parameter wrAP_content.
        // The default height is the bitmap height plus the margin of 10dp
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(unLikeBitmap.getHeight() + SystemUtil.dp2px(getContext(), 20), MeasureSpec.EXACTLY);
        // The default width is the bitmap width plus the left and right margin of 10dp and the text width and the right side of the text of 10dp likeNumber is the text number
        String textnum = String.valueOf(likeNumber);
        // Get the width of the text
        float textWidth = textPaint.measureText(textnum, 0, textnum.length());
        // Calculate the width of the entire View hand width + text width + 30px
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(((int) (unLikeBitmap.getWidth() + textWidth + SystemUtil.dp2px(getContext(), 30))), MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
Copy the code

EXACTLY as MeasureSpec.EXACTLY as MeasureSpec.

Draw the ontouch

Draw the hands

        super.onDraw(canvas);
        // Get the height of the View
        int height = getHeight();
        / / center
        int centerY = height / 2;
        // The hand changes depending on whether or not it has been liked
        Bitmap handBitmap = isLike ? likeBitmap : unLikeBitmap;
        // Get the image width
        int handBitmapWidth = handBitmap.getWidth();
        // Get the image height
        int handBitmapHeight = handBitmap.getHeight();

        / / drawing hands
        int handTop = (height - handBitmapHeight) / 2;
        // Save the state of the canvas
        canvas.save();
        // Scale according to the bitmap center
        canvas.scale(handScale, handScale, handBitmapWidth / 2, centerY);
        The first parameter is the corresponding bitmap, the second parameter is the top left coordinate, the third parameter is the top coordinate, and the fourth parameter is the brush
        canvas.drawBitmap(handBitmap, SystemUtil.dp2px(getContext(), 10), handTop, bitmapPaint);
        // The state of the canvas without scaling before reading
        canvas.restore();
Copy the code

Here’s why we use canvas.save() and canvas.restore(), because the entire like effect is animated, and scaling the canvas is not what you want if you don’t save the original state of the canvas and continue to draw other images after scaling.

Draw four highlights on the hand

// Draw four bright points on the top
        // Set the top
        int shiningTop = handTop - shiningBitmap.getHeight() + SystemUtil.dp2px(getContext(), 17);
        // Set the opacity of the light according to the hiding coefficient
        bitmapPaint.setAlpha((int) (255 * shiningAlpha));
        // Save the canvas state
        canvas.save();
        // The canvas is scaled according to the lit scaling factor
        canvas.scale(shiningScale, shiningScale, handBitmapWidth / 2, handTop);
        // Draw a lit bitmap
        canvas.drawBitmap(shiningBitmap, SystemUtil.dp2px(getContext(), 15), shiningTop, bitmapPaint);
        // Restore the previous state of the brush
        canvas.restore();
        // And restore the bitmapPaint opacity
        bitmapPaint.setAlpha(255);
Copy the code

Note that only the bitmappaint.setalpha () method is used to set the four points to appear and disappear. All the points are on the canvas, and setAlpha(255) is set to appear after the “like”. Otherwise, it will change depending on the transparency.

Draw a digital text area and draw a like when the circle spreads out

Here is divided into two big cases, one is different digit number change, the other is the same digit number change

    / / draw text
        String textValue = String.valueOf(likeNumber);
        // If the "like" is selected, the previous value is -1. If the "like" is unselected, the previous value is now displayed
        String textCancelValue;
        if (isLike) {
            textCancelValue = String.valueOf(likeNumber - 1);
        } else {
            if (isFirst) {
                textCancelValue = String.valueOf(likeNumber + 1);
            } else {
                isFirst = !isFirst;
                textCancelValue = String.valueOf(likeNumber);
            }
        }
        // The length of text
        int textLength = textValue.length();
        GetTextBounds returns the joint bounds of all the text
        textPaint.getTextBounds(textValue, 0, textValue.length(), textRounds);
        // Set the X coordinate distance to 10dp
        int textX = handBitmapWidth + SystemUtil.dp2px(getContext(), 20);
        GetTextBounds' rect parameter is derived by subtracting the height of the text area from the distance of half of the larger image.
        // Top is a negative number; Sometimes bottom is 0, sometimes it's positive. It's easy to understand with the first point, because the baseline is the origin (0,0),
        // Top is a negative number, bottom is a negative number, and bottom is a negative number. Lowercase letters like j, G, y, etc., all have positive bounds bottom,
        // Because they both have a falling part.
        int textY = height / 2 - (textRounds.top + textRounds.bottom) / 2;
        // Draw text in this case for different digit changes such as 99 to 100 999 to 10000
        if(textLength ! = textCancelValue.length() || textMaxMove ==0) {
            // The first parameter is the text content, the second parameter is the text's X coordinate, and the third parameter is the text's Y coordinate
            // It is not the upper left corner of the text, but closer to the lower left corner
            //canvas.drawText(textValue, textX, textY, textPaint);
            / / thumb up
            if (isLike) {
                // The round brush changes according to the opacity set
                circlePaint.setAlpha((int) (255 * shingCircleAlpha));
                / / draw circles
                canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
                // Change according to transparency
                oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                // Draw the previous number
                canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
                // Set the transparency of the new number
                textPaint.setAlpha((int) (255 * textAlpha));
                // Draw a new number (like or unlike)
                canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);

            } else {
                oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                canvas.drawText(textCancelValue, textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
                textPaint.setAlpha((int) (255 * textAlpha));
                canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
            }
            return;
        }
        // The following case is different from 99999 9999, which is the same digit change
        // To break the text into characters, get the width of each character in the string, and fill the result into the parameter widths
        MeasureText () = measureText(); measureText() = measureText()
        // Their results are filled with different widths elements
        textPaint.getTextWidths(textValue, widths);
        // Convert a string to an array of characters
        char[] chars = textValue.toCharArray();
        char[] oldChars = textCancelValue.toCharArray();

        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == oldChars[i]) {
                textPaint.setAlpha(255);
                canvas.drawText(String.valueOf(chars[i]), textX, textY, textPaint);

            } else {
                / / thumb up
                if (isLike) {
                    circlePaint.setAlpha((int) (255 * shingCircleAlpha));
                    canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
                    oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                    canvas.drawText(String.valueOf(oldChars[i]), textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
                    textPaint.setAlpha((int) (255 * textAlpha));
                    canvas.drawText(String.valueOf(chars[i]), textX, textY + textMoveDistance, textPaint);
                } else {
                    oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                    canvas.drawText(String.valueOf(oldChars[i]), textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
                    textPaint.setAlpha((int) (255* textAlpha)); canvas.drawText(String.valueOf(chars[i]), textX, textY + textMoveDistance, textPaint); }}// Add the width of the previous digit to the next digit
            textX += widths[i];


        }

Copy the code

Here I use textValue and textCancelValue to record the number before and after the change respectively.

        int textY = height / 2 - (textRounds.top + textRounds.bottom) / 2;  
Copy the code

Textrounds. top is a negative value, and the origin of the coordinates is not in the upper left corner, but in the text baseline. I won’t go into details about the transparency changes, but here’s how far they move:

/ / thumb up
            if (isLike) {
                // The round brush changes according to the opacity set
                circlePaint.setAlpha((int) (255 * shingCircleAlpha));
                / / draw circles
                canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
                // Change according to transparency
                oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                // Draw the previous number
                canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
                // Set the transparency of the new number
                textPaint.setAlpha((int) (255 * textAlpha));
                // Draw a new number (like or unlike)
                canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);

            } else {
                oldTextPaint.setAlpha((int) (255 * (1 - textAlpha)));
                canvas.drawText(textCancelValue, textX, textY + textMaxMove + textMoveDistance, oldTextPaint);
                textPaint.setAlpha((int) (255 * textAlpha));
                canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
            }
Copy the code

TextMaxMove is set to 20px and textMoveDistance is set to 14px

                // Draw the previous number
                canvas.drawText(textCancelValue, textX, textY - textMaxMove + textMoveDistance, oldTextPaint);
                // Draw a new number (like or unlike)
                canvas.drawText(textValue, textX, textY + textMoveDistance, textPaint);
Copy the code

So these two lines are going to draw a new number, and the main thing is the change in the y coordinate, so for example, if I’m 104 now, and I want to like 105 now, textCancelValue is 104, textValue is 105. Since textMoveDistance is decreasing from 20 to 0, the first formula is to draw 105, texty-TextMaxMove + textMoveDistance, and the y coordinate is getting smaller and smaller, so the 5 will move up, Similarly, the textY + textMoveDistance moves up according to formula 4, because it’s getting smaller and smaller, and because it’s converting the number to a string that’s easy to understand. The main purpose of drawing circle diffusion is to determine the center point of the circle, and the radius should be roughly determined:

canvas.drawCircle(handBitmapWidth / 2 + SystemUtil.dp2px(getContext(), 10), handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 10), ((handBitmapHeight / 2 + SystemUtil.dp2px(getContext(), 2)) * shingCircleScale), circlePaint);
Copy the code

The first two parameters are to determine the center of the circle, which I set to the center of the hand image.

Touch handles onTouchEvent

I set touch to trigger the like event:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                jump();
                break;
        }
        return super.onTouchEvent(event);
    }
Copy the code

Jump method is as follows:

/** * The "like" event triggers */
    private void jump(a) { isLike = ! isLike;if (isLike) {
            ++likeNumber;
            setLikeNum();
            // A custom attribute in ObjectAnimator is first assembled into the corresponding set function name based on the attribute value, such as the following handScale assembly method
            // The first letter of the property is forcibly capitalized and concatenated with set, so it is setHandScale, and then reflection finds the corresponding control's setHandScale(float handScale) function
            // Passing the current numeric value as an argument to the setHandScale (float handScale) call to the set function is used every ten milliseconds
            //ObjectAnimator just passes the value of the current motion animation to the set function
            ObjectAnimator handScaleAnim = ObjectAnimator.ofFloat(this."handScale".1f.0.8 f.1f);
            // Set the animation time
            handScaleAnim.setDuration(duration);

            // Animate the four points of the finger from 0-1
            ObjectAnimator shingAlphaAnim = ObjectAnimator.ofFloat(this."shingAlpha".0f.1f);
            // shingAlphaAnim.setDuration(duration);

            // Zoom in and light the four points of your finger
            ObjectAnimator shingScaleAnim = ObjectAnimator.ofFloat(this."shingScale".0f.1f);

            // Draw a central circle with inner to outer diffusion
            ObjectAnimator shingClicleAnim = ObjectAnimator.ofFloat(this."shingCircleScale".0.6 f.1f);
            // Draw a circle with 1 to 0 missing
            ObjectAnimator shingCircleAlphaAnim = ObjectAnimator.ofFloat(this."shingCircleAlpha".0.3 f.0f);


            // Play the animation together
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(handScaleAnim, shingAlphaAnim, shingScaleAnim, shingClicleAnim, shingCircleAlphaAnim);
            animatorSet.start();


        } else {
            // Unlike
            --likeNumber;
            setLikeNum();
            ObjectAnimator handScaleAnim = ObjectAnimator.ofFloat(this."handScale".1f.0.8 f.1f);
            handScaleAnim.setDuration(duration);
            handScaleAnim.start();

            // The four points on the finger disappear and the opacity is set to 0
            setShingAlpha(0); }}Copy the code

The above animation functions are used, and the code above explains clearly that the animation will trigger the corresponding setXXXX () method below

/** * Finger scaling method **@param handScale
     */
    public void setHandScale(float handScale) {
        // Pass the scaling factor
        this.handScale = handScale;
        // Redraw the View tree, i.e. the draw procedure. The layout procedure will not be called if the View size does not change, and the views that "need redrawing" will be redrawn
        // If it is a view, draw the view; if it is a ViewGroup, draw the entire ViewGroup
        invalidate();
    }


    /** * four points on the finger from 0 to 1 appear **@param shingAlpha
     */

    public void setShingAlpha(float shingAlpha) {
        this.shiningAlpha = shingAlpha;
        invalidate();
    }

    /** * Finger four point zoom method **@param shingScale
     */
    @Keep
    public void setShingScale(float shingScale) {
        this.shiningScale = shingScale;
        invalidate();
    }


    /** * sets the number change */
    public void setLikeNum(a) {
        // Start moving the Y coordinate
        float startY;
        // Maximum height to move
        textMaxMove = SystemUtil.dp2px(getContext(), 20);
        // If you like it, move it up
        if (isLike) {
            startY = textMaxMove;
        } else {
            startY = -textMaxMove;
        }
        ObjectAnimator textInAlphaAnim = ObjectAnimator.ofFloat(this."textAlpha".0f.1f);
        textInAlphaAnim.setDuration(duration);
        ObjectAnimator textMoveAnim = ObjectAnimator.ofFloat(this."textTranslate", startY, 0);
        textMoveAnim.setDuration(duration);


        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(textInAlphaAnim, textMoveAnim);
        animatorSet.start();
    }


    /** * sets the transparency of the value */

    public void setTextAlpha(float textAlpha) {
        this.textAlpha = textAlpha;
        invalidate();

    }

    /** * sets the value to move */

    public void setTextTranslate(float textTranslate) {
        textMoveDistance = textTranslate;
        invalidate();
    }

    /** * draw a circle of ripples **@param shingCircleScale
     */
    public void setShingCircleScale(float shingCircleScale) {
        this.shingCircleScale = shingCircleScale;
        invalidate();
    }

    /** * Circular transparency set **@param shingCircleAlpha
     */
    public void setShingCircleAlpha(float shingCircleAlpha) {
        this.shingCircleAlpha = shingCircleAlpha;
        invalidate();

    }
Copy the code

The effect is as follows:

conclusion

This simple example involves the basic use of some custom View, such as drawing, some basic uses of Canvas, etc. And instant “like” effect is still different, can be added under the animation differentiator optimization. Project code: copy instant like effect