First, the effect picture (please see the explanation later if you are interested) :

1. Login effect display

1. [Draw rounded rectangles]

OnDraw: Define a property in the view: private RectF RectF = new RectF(); // load the control button area

rectf.left = current_left;
rectf.top = 0;      // (These 2 points define the upper left corner of the space area, current_left, for the later animated rectangle to become an equilateral rectangle, which you can view as 0)
rectf.right = width - current_left; 
rectf.bottom = height;       // (By changing the current_left size and updating the drawing, the animation is achieved)
// Draw rounded rectangles
// Parameter 1: region
// parameter 2,3: the rounded corner of the rectangle is actually the radius of the rounded corner of the rectangle
// Argument 4: brushCanvas. DrawRoundRect (rectf, circleAngle, circleAngle, paint);Copy the code

2. [Determine the size of the control]

I’m drawing rounded corners, so how do I get width and height by onMeasure of course;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    height = measuredHeight(heightMeasureSpec);  // Here is the measure control size
    width = measureWidth(widthMeasureSpec);  // We often see controls set wrap_content, match_content or fixed value
    setMeasuredDimension(width, height);
}
Copy the code

Take measureWidth as an example:


private int measureWidth(int widthMeasureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        // Here is the exact mode, such as match_content, or the size specified in your control
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            // Wrap_content is the default value
            If the user does not set the size, give him a fixed default value. It makes more sense to use the length of the font
            //result = (int) getContext().getResources().getDimension(R.dimen.dp_150);
            // Here is the length I set, of course you write custom controls can set the logic you want, according to your actual situation
            result = buttonString.length() * textSize + height * 5 / 3;
            if(specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); }}return result;
    }
Copy the code

3, 【 draw text】

I’m doing it my way here: when the text is longer than the control length, the text needs to scroll back and forth. So custom control because you need what kind of function can be their own to achieve (of course, this method is also in onDraw, why such a sequence, purpose HOPE I hope you can understand step by step, if you think onDraw side code is too miscellaneous, you can use a method to go out, You can use private void drawText(Canvas Canvas) {}) as the author does, // Draw the path of the text (if the text is too long, the text is needed to scroll back and forth).

Private Path textPath = new Path() :

textRect.left = 0;
textRect.top = 0;
textRect.right = width;
textRect.bottom = height; // This defines the text drawing area, which is actually the control area
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
// Here is the position of the Y-axis to get the text drawing
int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
< span style = "max-width: 100%; clear: both; min-height: 1em
// When the text content is larger than the control length, the rollback effect is enabled. It is recommended to first look at the normal case in the else below
if ((buttonString.length() * textSize) > (width - height * 5 / 3)) {textPath. Reset ();// Because we want to leave 2 spacing, heigh/3 spacing
    textPath.moveTo(height / 3, baseline);
    textPath.lineTo(width - height / 3, baseline);
    // Where does the text start, maybe center, right here
    textPaint.setTextAlign(Paint.Align.RIGHT);
    // Here is the path to draw text, scrollSize can be understood as the cheap amount of text on the X-axis, meanwhile, MY blending effect is by changing the scrollSize
    // Refresh drawing to implement
    canvas.drawTextOnPath(buttonString, textPath, scrollSize, 0, textPaint);
    if (isShowLongText) {
        // Here is draw occlusion, because there is no spacing method to draw path, so draw occlusion is similar to spacing method
        canvas.drawRect(new Rect(width - height / 2 - textSize / 3.0, width - height / 2, height),paintOval);
        canvas.drawRect(new Rect(height / 2.0, height / 2 + textSize / 3, height), paintOval);
        // There is a bug with a small dot -5 due to the thickness of the brush
        canvas.drawArc(new RectF(width - height, 0, width - 5, height), -90.180.true, paintOval);
        canvas.drawArc(new RectF(0.0, height, height), 90.180.true, paintOval);
    }
                                                                                                                          
    if (animator_text_scroll == null) { 
        // Calculate the range of distances to the right and left
        animator_text_scroll = ValueAnimator.ofInt(buttonString.length() * textSize - width + height * 2 / 3,-textSize);
        // Here is the animation time, scrollSpeed can be understood as each text scroll control outside the required time, can be provided as a control property
        animator_text_scroll.setDuration(buttonString.length() * scrollSpeed);
        // Set the animation mode, here is scroll back and forth
        animator_text_scroll.setRepeatMode(ValueAnimator.REVERSE);
        // Set the interpolator to make the animation flow smoothly
        animator_text_scroll.setInterpolator(new LinearInterpolator());
        // Here is the scroll number, -1 infinite scroll
        animator_text_scroll.setRepeatCount(-1);
        animator_text_scroll.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // Change the x offset of the text path
                scrollSize = (int) animation.getAnimatedValue(); postInvalidate(); }}); animator_text_scroll.start(); }}else {
    // This is the normal case, isShowLongText, is when I start the control animation, whether to start the text gradient effect flag,
    / / if it is a long word, start the gradual change effect, if the control is smaller, the text in the current control, can appear ugly, so according to this logo, closed, you can ignore (here at the same time, because according to path drawing text cannot have space effect, the logo is determine whether in control 2 times paint mask, it is the author of the solution, If you have a better way, leave a comment below)
    isShowLongText = false;
    /** * simply draw text, without considering text length over control length ** /
    // Here is the center display
    textPaint.setTextAlign(Paint.Align.CENTER);
    // Argument 1: text
    // Parameter 2: Draws the center of the text
    // Argument 4: brush
    canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
}
Copy the code

4. [Custom control properties]

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <declare-styleable name="SmartLoadingView">
        <attr name="textStr" format="string" />
        <attr name="errorStr" format="string" />
        <attr name="cannotclickBg" format="color" />
        <attr name="errorBg" format="color" />
        <attr name="normalBg" format="color" />
        <attr name="cornerRaius" format="dimension" />
        <attr name="textColor" format="color" />
        <attr name="textSize" format="dimension" />
        <attr name="scrollSpeed" format="integer" />
    </declare-styleable>
</resources>
Copy the code

So let’s take for example textStr. For example, if you use app for layout :txtStr=” copywriting content “. Get the following in the custom control:

public SmartLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // Attrs of the 3-argument method of the custom control is the key to setting the custom properties
    // For example, if we customize our attributes in attrs. XML,
    TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SmartLoadingView);
    // This is to get whether the user has set the entire attribute
    // Get the setup copy from the user
    String title = typedArray.getString(R.styleable.SmartLoadingView_textStr);
    if (TextUtils.isEmpty(title)){
       // If the obtained attribute is empty, then a default attribute can be obtained
       // (the author forgot to set it! It's embarrassing because it's already released post-optimization.)
       buttonString ="Default copy";
    }else{
       // If there is a setup copybuttonString = title; }}Copy the code

5. [Set click event to start animation]

In order to click events intuitive, can also be processed to prevent repeated click events encapsulated inside

// This is my custom login click interface
public interface LoginClickListener {
    void click(a);
}
 
public void setLoginClickListener(final LoginClickListener loginClickListener) {
    this.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if(loginClickListener ! =null) {
                // Prevent repeated clicks
                if(! isAnimRuning) { start(); loginClickListener.click(); }}}}); }Copy the code

6. [Animation Explanation]

6.1. The first animation, Rectangle to square and Rectangle to Rounded Rectangle (here are 2 animations, just at the same time)

Rectangle to square (for simplicity, I have removed some other attributes from the source code to make it easier to understand)

// where default_all_distance = (w-h) / 2; Divide by 2 because both of them are going to shrink in the middle
private void set_rect_to_circle_animation(a) {
    // This is a property animation where current_left changes uniformly from 0 to default_all_distance for duration
    // Interpolators can also be added if you want to add variety.
    animator_rect_to_square = ValueAnimator.ofInt(0, default_all_distance);
    animator_rect_to_square.setDuration(duration);
    animator_rect_to_square.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // Where current_left is related to onDraw, remember
            // The control area in onDraw
            Rectf. left = current_left;
            // Rectf. right = width-current_left;
            current_left = (int) animation.getAnimatedValue();
            // Refresh drawinginvalidate(); }});Copy the code

Rectangle to rounded rectangle. It’s going from a rectangle with no rounded corners to a fully rounded rectangle, and of course when I show it it’s only the third image, the last button that’s obvious.

Everything else I just set to the rounded corner button, because I made the rounded corner a property.

Remember canvas. DrawRoundRect (rectf, circleAngle, circleAngle, paint) from onDraw; The circleAngle is the radius of the rounded corner

You can imagine if they were all rounded, what would the circleAngle be, height over 2 of course; Right? So

Because I made rounded corners a property obtainCircleAngle is a property that I got from the XML file, and if I don’t set it, it’s 0, and I don’t have any rounded corners

animator_rect_to_angle = ValueAnimator.ofInt(obtainCircleAngle, height / 2);
animator_rect_to_angle.setDuration(duration);
animator_rect_to_angle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // If it is a square with a rounded corner, it is a circle
        circleAngle = (int) animation.getAnimatedValue();
        // Refresh the paintinginvalidate(); }});Copy the code

Private AnimatorSet AnimatorSet = new AnimatorSet(); I’m going to add the property animation, and I can set the sequence of the two animations to be done at the same time

animatorSet
        .play(animator_rect_to_square).with(animator_rect_to_angle);
Copy the code

6.2. After turning into a circle, there is a loading animation

This is to draw an arc, but it keeps changing. The starting point and ending point of the arc will eventually show loading state, which is also in onDraw

// Draw the loading progress
if (isLoading) {
    // Parameter 1: draw the arc region
    // Parameter 2: Draw the start and end points of the arc
    canvas.drawArc(new RectF(width / 2 - height / 2 + height / 4, height / 4, width / 2 + height / 2 - height / 4, height / 2 + height / 2 - height / 4), startAngle, progAngle, false, okPaint);
 
    // Here is my practice to achieve the best loading animation
    // Of course there are many ways, because I want to customize the view to put everything in this class, you can have your way too
    // If there is a better way, please leave a message and let me know
    startAngle += 6;
    if (progAngle >= 270) {
        progAngle -= 2;
        isAdd = false;
    } else if (progAngle <= 45) {
        progAngle += 6;
        isAdd = true;
    } else {
        if (isAdd) {
            progAngle += 6;
        } else {
            progAngle -= 2; }}// Refresh drawing, there is no need to worry about refresh drawing, will affect the performance
    //
    postInvalidate();
}
Copy the code

6.3 Loading state, to tick animation

IsLoading =false; isLoading=false; It won’t just launch the checkmark animation at the same time; Tick animation animation, which is more troublesome, is also what I learned in other people’s custom animation, through PathMeasure, path animation

/** * path -- the path to get the tick */
private Path path = new Path();
/** * The length of the path */
private PathMeasure pathMeasure;
Copy the code
// Initialize the tick animation path;
private void initOk(a) {
    // Check the path
    path.moveTo(default_all_distance + height / 8 * 3, height / 2);
    path.lineTo(default_all_distance + height / 2, height / 5 * 3);
    path.lineTo(default_all_distance + height / 3 * 2, height / 5 * 2);
    pathMeasure = new PathMeasure(path, true);
}
Copy the code
// Initialize the tick animation
private void set_draw_ok_animation(a) {
    animator_draw_ok = ValueAnimator.ofFloat(1.0);
    animator_draw_ok.setDuration(duration);
    animator_draw_ok.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            startDrawOk = true;
            isLoading = false;
            float value = (Float) animation.getAnimatedValue();
            effect = new DashPathEffect(new float[]{pathMeasure.getLength(), pathMeasure.getLength()}, value * pathMeasure.getLength()); okPaint.setPathEffect(effect); invalidate(); }}); }// To start the tick animation, you only need to call
animator_draw_ok.start();
Copy the code

Draw a check animation in onDraw

// Draw the check box, which is onDraw, startDrawOk is the flag to check whether the check box animation is enabled
if (startDrawOk) {
    canvas.drawPath(path, okPaint);
}
Copy the code

6.4 Returning to failure mode in Loading State (similar to networking failure)

Previous 6.1 mentioned rectangle-to-rounded rectangles and rectangle-to-square animation,

So this is just the reverse of the previous 2 animations, plus the text of the Internet failure, and the background of the Internet failure immediately

6.5 Start diffusion full-screen Animation in Loading State (key)

Here I enable different effects using the types of arguments in loginSuccess:

1Start diffuse full-screen animationpublic void loginSuccess(Animator.AnimatorListener endListener) {}
 
2, start the check box animationpublic void loginSuccess(AnimationOKListener animationOKListener) {}
Copy the code

Starting the diffusion full screen is the focus of this article, which also involves a custom view

CirclBigView, this control is full screen, and from a small circle constantly changing the radius of the animation into a large circle, so some people may ask, full screen must be bad, will affect the layout, but here, I put it in the view layer of the activity:  ViewGroup activityDecorView = (ViewGroup) ((Activity) getContext()).getWindow().getDecorView(); ViewGroup.LayoutParams layoutParams =new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);
Copy the code

This inspiration was also found in the idea of dragging and exiting wechat not long ago. The full code is as follows:

public void toBigCircle(Animator.AnimatorListener endListener) {
    // To zoom out to the radius of the circle, tell circlBigView
    circlBigView.setRadius(this.getMeasuredHeight() / 2);
    // Tell circlBigView the current background color
    circlBigView.setColorBg(normal_color);
    int[] location = new int[2];
    // Measure the screen coordinates x,y of the current control
    this.getLocationOnScreen(location);
    // Tell circlBigView the current coordinates, and circlBigView will calculate the maximum distance from the current point to the four points on the screen, that is, the radius to which the current control will spread
    // We recommend readers to download and play after reading this blog.
    circlBigView.setXY(location[0] + this.getMeasuredWidth() / 2, location[1]);
    if (circlBigView.getParent() == null) {
        ViewGroup activityDecorView = (ViewGroup) ((Activity) getContext()).getWindow().getDecorView();
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        activityDecorView.addView(circlBigView, layoutParams);
    }
    circlBigView.startShowAni(endListener);
    isAnimRuning = false;
}
Copy the code

Conclusion: Because the project is writing the previous functionality as controls, there are a lot of imperfections. I hope that if you have any suggestions, you can prompt me and make me better. thank you

Github address, see here to give a star

Another project address, shadow layout, no matter what control you are, put shadow layout into instantly enjoy the shadow you want. You can’t believe what you’re trying