preface

Hi everyone, I’m the Crimson Knight. I love to joke, I’m bad at technology and I love to study. This article is the last one of the year. Today I’m going to learn bezier curves, which I’ve always wanted to learn but didn’t have the time to do. What is a Bezier curve? I didn’t understand it at the beginning. After checking a lot of materials, I still didn’t know enough about it, and the derivation formula still couldn’t be thoroughly understood. In 1962, Pierre Bessel used Bessel curves to design the body of a car, Bessel curve was first developed by Paul de Casteljau in 1959 using de Casteljau algorithm, which is a stable reporting method to find Bessel curve. In fact, Bessel curve is in our daily life, such as some mature bitmap software: PhotoSHop, Flash5 and so on. Bezier curves are also ubiquitous in front end development: front end 2D or 3D graphics icon libraries use Bezier curves; It can be used to draw curves. In SVG and Canvas, curve drawing provided natively is implemented using Bezier curves. In the transition-timing-function property of the CSS, Bessel curves can be used to describe the slow calculation of transitions.

Bezier curve principle

Bezier curves control the state of the curve with a series of points, which I divide into three points: starting point, end point and control point. By changing these points, the Bezier curve changes.

  • Starting point: Determines the starting point of the curve
  • Endpoint: Determine the endpoint of a curve
  • Control point: Determine the control point of the curve

First order curve principle

A first order curve is a straight line, with only two points, the beginning and the end, and the final effect is a line segment. The above picture is more intuitive:

Second order curve principle

A second-order curve consists of two data points (starting point and end point) and one control point to describe the curve state, as shown in the figure below. Point A is the starting point, C is the end point, and B is the control point.

The starting point
At the end of

Third order curve principle

The third-order curve is actually described by two data points (starting point and end point) and two control points, as shown in the figure below. Below, A is the starting point, D is the end point, and B and C are the control points.

Bezier curves in Android

In Android, there are four methods in the Path class that are related to the Bezier curve, that is, functions that have been wrapped around the Bezier curve, and developers can call them directly:

    // Second order Bezier
    public void quadTo(float x1, float y1, float x2, float y2);
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
    // Third order Bessel
    public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
    public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
Copy the code

Among the above four functions, quadTo and rQuadTo are second-order Bessel curves, and cubicTo and rCubicTo are third-order Bessel curves. Because third-order Bezier curves are used in a similar way to second-order Bezier curves, they are also less useful, so I won’t go into details. The following is a detailed description of quadTo and rQuadTo Bessel curves of second order.

QuadTo principle

First look at the quadTo function definition:

/**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
Copy the code

(x1,y1) is the control point,(x2,y2) is the terminal point, why there is no starting point coordinates? As Android developers know, the starting point of a line segment is specified by path.move (x,y). If the quadTo function is called consecutively, the end point of the previous quadTo function is the start point of the next quadTo function. If path.moveto (x,y) is not called to specify the start point, the control view will start at the upper left corner (0,0). The following implementation draws the following renderings:

Pay attention to

The sample code

public class PathView extends View {


    / / brush
    private Paint paint;
    / / path
    private Path path;

    public PathView(Context context) {
        super(context);
    }

    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    // Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        // Line width
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        // Set the starting position to (200,400)
        path.moveTo(200.400);
        // line P0-P2 control point (300,300)
        path.quadTo(300.300.400.400);
        // Line P2-P4 control point (500,500) end position (600,400)
        path.quadTo(500.500.600.400);
        canvas.drawPath(path, paint);

    }


    private void init(a) {
        paint = new Paint();
        path = newPath(); }}Copy the code

The layout file is as follows:

<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000"> <Button android:id="@+id/btn_reset" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" Android: layout_marginTop = "10 dp" android: text = "empty path" / > < com. Example. Okhttpdemo. PathView android: id = "@ + id/path_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/btn_reset" android:background="#000000"/> </android.support.constraint.ConstraintLayout>Copy the code

The effect picture is as follows:

Path. The moveTo (200400);

  • When a quadTo function is called consecutively, the end point of the previous quadTo function is the start point of the next quadTo function.
  • The starting point of the bezier curve is specified by path.moveto (x,y). If path.move (x,y) is not called initially, the upper left corner (0,0) of the control is taken as the starting point.

Path.lineTo and path. quadTo

Path.lineTo is a line connecting the previous point to the current point. In order to draw the Path of your finger on the screen, add onTouchEvent to the above method. The code is as follows:

public class PathView extends View {


    / / brush
    private Paint paint;
    / / path
    private Path path;

    public PathView(Context context) {
        super(context);
    }

    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    // Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        // Line width
      // paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        canvas.drawPath(path, paint);

    }


    private void init(a) {
        paint = new Paint();
        path = new Path();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd"."Trigger press");
                path.moveTo(event.getX(), event.getY());
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd"."Trigger move");
                path.lineTo(event.getX(), event.getY());
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }


    public void reset(a) { path.reset(); invalidate(); }}Copy the code

Direct renderings:

MotionEvent.DOWN
path.move(event.getX(),event.getY())
path.lineTo(event.getX,event.getY())
invalidate
MotionEvent.ACTION_DOWN
return true
return true
ACTION_UP
ACTION_MOVE
case MotionEvent.ACTION_DOWN
return false
MOTION_MOVE
MMOTION_UP
ACTION_DOWN
ACTION_DOWN
ACTION_MOVE
ACTION_UP

The midpoints of the two lines are the starting and ending points respectively
The control points

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("ssd"."Trigger press");
                path.moveTo(event.getX(), event.getY());
                // Save the coordinates of this point
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                Log.d("ssd"."Trigger move");
                // Draw a second-order curve as you move
                // The endpoint is the midpoint of the segment
                endX = (mBeforeX + event.getX()) / 2;
                endY = (mBeforeY + event.getY()) / 2;
                // Draw a second-order curve
                path.quadTo(mBeforeX,mBeforeY,endX,endY);
                // Then update the coordinates of the previous point
                mBeforeX = event.getX();
                mBeforeY = event.getY();
                invalidate();
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }

Copy the code

ACTION_DOWN: path.moveto (event.getx (), event.gety ()); Set the initial position of the curve to be where the finger touches the screen. It is explained that if moveTo(event.getx (), event.gety ()) is not called, the drawing point will start at (0,0) of the control. Use mBeforeX and mBeforeY to record the first horizontal and vertical coordinates of the finger movement, which is the control point, and return true in order for ACTION_MOVE and ACTION_UP to be passed to the control. EndX = (mBeforeX + event.getx ()) / 2; endX = (mBeforeX + event.getx ()) / 2; And endY = (mBeforeY + event.gety ()) / 2; Find the horizontal and vertical coordinates of the middle position, and the control point is the position of the last finger touch screen, followed by the update of the previous finger coordinates. Note that when you call “quardTo” in a row, the first start point is set to path.moveto (x,y). The rest of the quadTo is the start point of the next quard, which is the middle point of the previous segment. The logic above is expressed in one sentence: The middle point of each line segment is used as the starting point and ending point, and the position of the previous finger is used as the control point. The final effect is as follows:

quadT

Path. RQuadTo principle

Look directly at the description of this function:

/**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
Copy the code
  • X1: The X coordinate of the control point, representing the displacement value relative to the X coordinate of the last endpoint, can be negative, positive value means addition, negative value means subtraction
  • X2: the Y coordinate of the control point, representing the displacement value relative to the Y coordinate of the last terminal point, which can be negative, positive value means addition, negative value means subtraction
  • X2: X coordinate of the end point, representing the displacement value relative to the X coordinate of the last end point, which can be negative, positive value means addition, negative value means subtraction
  • Y2: the y-coordinate of the end point, which represents the displacement value relative to the y-coordinate of the previous end point, can be negative, positive value means addition, negative value means subtraction. If the last endpoint coordinate is (100,200), if rQuardTo(100,-100,200,200) is called at this time, the control point coordinate obtained is (100 + 100,200-100), which is (200,100). The resulting endpoint coordinate is (100 + 200,200 + 200) which is (300,400). The following two segments are equal:
path.moveTo(100.200);
path.quadTo(200.100.300.400);
Copy the code
path.moveTo(100.200);
path.rQuadTo(100, -100.200.200);
Copy the code

In the above, quadTo implements a wavy line, as shown below:

quadTo

// Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        // Line width
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        // Set the starting position to (200,400)
        path.moveTo(200.400);
        // line P0-P2 control point (300,300)
        path.quadTo(300.300.400.400);
        // Line P2-P4 control point (500,500) end position (600,400)
        path.quadTo(500.500.600.400);
        canvas.drawPath(path, paint);

    }
Copy the code

Below, rQuadTo is used to realize this wavy line. First, the analysis figure is shown:

 // Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        // Line width
        paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        // Set the starting position to (200,400)
        path.moveTo(200.400);
        //线条p0-p2控制点(300,300) 终点坐标位置(400,400)
        path.rQuadTo(100, -100.200.0);
        //线条p2-p4控制点(500,500) 终点坐标位置(600,400)
        path.rQuadTo(100.100.200.0);
        canvas.drawPath(path, paint);

    }
Copy the code

First line: path.rquadto (100,-100,200,0); This line of code calculates the control points and endpoint coordinates of curves P0-P2 based on the point (200,400).

  • X-coordinate of control point = X-coordinate of last endpoint + x-displacement value of control point = 200 + 100 = 300;
  • Control point Y coordinate = last terminal point Y coordinate + control point Y displacement value = 400-100 = 300;
  • End point X coordinate = the X coordinate of the previous end point + the displacement value of end point X = 200 + 200 = 400;
  • End point Y coordinate = last end point Y coordinate + end point Y displacement value = 400 + 0 = 400; Path. quadTo(300,300,400,400)

Path. rQuadTo(100,100,200,0) calculates the control points and end points of the second curve based on this end point (400,400).

  • X-coordinate of control point = X-coordinate of last endpoint + x-displacement value of control point = 400 + 100 = 500;
  • Control point Y coordinate = last terminal point Y coordinate + control point Y displacement value = 400 + 100 = 500
  • End point X coordinate = the X coordinate of the previous end point + the displacement value of end point X = 400 + 200 = 600;
  • End point Y coordinate = last end point Y coordinate + end point Y displacement value = 400 + 0 = 400;

Path. rQuadTo(100,100,200,0); Is and path. The quadTo (500500600400); Similarly, the effect map of actual operation is the same as that drawn by quadTo method. Through this example, it can be known that the parameters of quadTo method are the coordinates of actual results, while the parameters of rQuadTo method are the displacement based on the above terminal position.

Realize closed wave

The following effects should be achieved:

Achieve static closed wave

The corresponding code is as follows:

  // Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        path.reset();
        // Set the fill drawing
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        // Line width
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        // Start with the initial state (-400,1200)
        path.moveTo(-waveLength,origY);
        // Because the width of the whole wave is the width of the View plus the left and right wavelengths
        for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
            path.rQuadTo(control / 2.70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
Copy the code

Here’s a line by line analysis:

        // Start with the initial state (-400,1200)
        path.moveTo(-waveLength,origY);
Copy the code

First move the starting position of the Path to the left by one wavelength, in order to animate the subsequent displacement, and then use the loop to draw all the waves in the screen:

 for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
            path.rQuadTo(control / 2.70,control,0);
        }
Copy the code

Path. rQuadTo(Control / 2,-70, Control,0); The first line in the loop shows the first half of a waveLength. This is easy to understand by plugging in the values below, because the waveLength is 400, so control = waveLength / 2 is 200. Path. rQuadTo(control / 2,-70,control,0) is path.rQuadTo(100,-70,200,0), Path. rQuadTo(control / 2,70,control,0) is path.rQuadTo(100,70,200,0). Coordinates of control points, and other waves are just drawn through cycles without analysis:

rQuadTo
paint.setStyle(Paint.Style.FILL_AND_STROKE);
path
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();

Realize the wave of displacement animation

The following implementation or displacement of the animation, it will feel a little bit of progress bar, my approach is very simple, because more than a start on the left side of the View drew a wave, that is to say, will move to the right starting point, and the length of a wave to move can make corrugated overlapping, then cycle, simply is, The distance the animation moves is the length of a wave, and when it moves to the maximum distance the setting is repeated to redraw the initial state of the wave.

    /** * Animation shift method */
    public void startAnim(a){
        // Create an animation instance
        ValueAnimator moveAnimator = ValueAnimator.ofInt(0,waveLength);
        // The animation time
        moveAnimator.setDuration(2500);
        // Set the number of animations to INFINITE to indicate an INFINITE loop
        moveAnimator.setRepeatCount(ValueAnimator.INFINITE);
        // Set the animation interpolation
        moveAnimator.setInterpolator(new LinearInterpolator());
        // Add a listener
        moveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveDistance = (int)animation.getAnimatedValue(); invalidate(); }});// Start animation
        moveAnimator.start();
    }
Copy the code

The distance moved by the animation is the length of a wave and is saved to moveDistance. Then, to start the animation, add this distance to moveTo. The complete code is as follows:

  // Override the onDraw method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Clear the path to redraw must be added or rectangle
        path.reset();
        // Set the fill drawing
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        // Line width
        //paint.setStrokeWidth(10);
        paint.setColor(Color.RED);
        int control = waveLength / 2;
        // Start with the initial state (-400,1200)
        path.moveTo(-waveLength + moveDistance,origY);
        for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
            path.rQuadTo(control / 2.70,control,0);
        }
        path.lineTo(getWidth(),getHeight());
        path.lineTo(0,getHeight());
        path.close();
        canvas.drawPath(path, paint);

    }
Copy the code

The effect is as follows:

path.moveTo(-waveLength + moveDistance,origY - moveDistance);

        pathView = findViewById(R.id.path_view);
        pathView.startAnim();
Copy the code

The effect is shown above. After the above, their initial understanding of Bessel curve, the following to achieve the wavy progress bar.

Implement wave progress bar

Once you have learned the basics above, here is a small example of how to implement the circular wavy progress bar. The final effect is at the bottom of the article.

Draw a wave

First draw a section of full screen wavy line, drawing principle will not be detailed, directly on the code:

/** * Describe: Created by Knight on 2019/2/1 * See the world by bit **/
public class CircleWaveProgressView extends View {

    // Draw a wave brush
    private Paint wavePaint;
    // Draw wave Path
    private Path wavePath;
    // Wave width
    private float waveLength;
    // Wave height
    private float waveHeight;
    public CircleWaveProgressView(Context context) {
        this(context,null);
    }

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

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /** * Initializes some brush path configuration *@param context
     */
    private void  init(Context context){
        // Set the wave width
        waveLength = Density.dip2px(context,25);
        // Set the wave height
        waveHeight = Density.dip2px(context,15);
        wavePath = new Path();
        wavePaint = new Paint();
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        // Set anti-aliasing
        wavePaint.setAntiAlias(true);
    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        // Draw wavy lines
        canvas.drawPath(paintWavePath(),wavePaint);
    }

    /** * draw wavy lines **@return* /
    private Path paintWavePath(a){
        Clear the route first
        wavePath.reset();
        // Start at (0,waveHeight)
        wavePath.moveTo(0,waveHeight);
        for(int i = 0; i < getWidth() ; i += waveLength){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        returnwavePath; }}Copy the code

XML layout file:

<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.progressbar.CircleWaveProgressView android:id="@+id/circle_progress"  android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> </android.support.constraint.ConstraintLayout>Copy the code

The actual effect is as follows:

Draw closed static waves

As the wave in the circular progress box rises with the increase of progress, the wave is a filler. Draw the wave first, and then use path.lineTo and path.close to connect and close to form a filling graph.

public class CircleWaveProgressView extends View {

    // Draw a wave brush
    private Paint wavePaint;
    // Draw wave Path
    private Path wavePath;
    // Wave width
    private float waveLength;
    // Wave height
    private float waveHeight;
    // The number of wave groups a wave is one low and one high
    private int waveNumber;
    // Customize the wave width and height of the View
    private int waveDefaultSize;
    // The maximum width and height of a custom View is higher than the wave
    private int waveMaxHeight;

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

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

    public CircleWaveProgressView(Context context,  @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    /** * Initializes some brush path configuration *@param context
     */
    private void  init(Context context){
        // Set the wave width
        waveLength = Density.dip2px(context,25);
        // Set the wave height
        waveHeight = Density.dip2px(context,15);
        // Set the width and height of the custom View
        waveDefaultSize = Density.dip2px(context,250);
        // Set the maximum width and height of the custom View
        waveMaxHeight = Density.dip2px(context,300);
        Math.ceil(a) returns the smallest integer not less than a
        // For example:
        / / Math. Ceil (125.9) = 126.0
        / / Math. Ceil (0.4873) = 1.0
        / / Math. Ceil (0.65) = 0.0
        Use ceil to make sure that the View is fully filled with waves to prepare for looping. The smaller the denominator is, the more accurate it is
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveDefaultSize / waveLength / 2)));
        wavePath = new Path();
        wavePaint = new Paint();
        // Set the color
        wavePaint.setColor(Color.parseColor("#ff7c9e"));
        // Set anti-aliasing
        wavePaint.setAntiAlias(true);

    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        // Draw wavy lines
        canvas.drawPath(paintWavePath(),wavePaint);
        Log.d("ssd",getWidth()+"");
    }

    /** * draw wavy lines **@return* /
    private Path paintWavePath(a){
        Clear the route first
        wavePath.reset();
        // Start at (0,waveHeight)
        wavePath.moveTo(0,waveMaxHeight - waveDefaultSize);
        // The maximum number of waves can be drawn
        I < getWidth(); I +=waveLength is not perfect
        // Draw p0-p1 draw wavy lines
        for(int i = 0; i < waveNumber ; i ++){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        // connect p1-p2
        wavePath.lineTo(waveDefaultSize,waveDefaultSize);
        // Connect p2-P3
        wavePath.lineTo(0,waveDefaultSize);
        // Connect p3-P0
        wavePath.lineTo(0,waveMaxHeight - waveDefaultSize);
        // Close and fill
        wavePath.close();
        return wavePath;
    }

Copy the code

Measure the width and height of the adaptive View

The width and height of a View are defined in an XML file or in a class file, so we need to rewrite the onMeasure method of a View:

 @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
         super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int height = measureSize(waveDefaultSize, heightMeasureSpec);
        int width = measureSize(waveDefaultSize, widthMeasureSpec);
        // Get the shortest edge of the View
        int minSize = Math.min(height,width);
        // Change View to a square
        setMeasuredDimension(minSize,minSize);
        //waveActualSize is the actual width and height
        waveActualSize = minSize;
        Math.ceil(a) returns the smallest integer not less than a
        // For example:
        / / Math. Ceil (125.9) = 126.0
        / / Math. Ceil (0.4873) = 1.0
        / / Math. Ceil (0.65) = 0.0
        Use ceil to make sure that the View is fully filled with waves to prepare for looping. The smaller the denominator is, the more accurate it is
        waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSize / waveLength / 2)));


    }

    /** * returns the specified value *@paramDefaultSize The default value *@paramMeasureSpec mode *@return* /
    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        / / the MeasureSpec. EXACTLY: if it is match_parent or setting fixed value
        / / the MeasureSpec. AT_MOST: wrap_content
        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }
Copy the code

Add a variable waveActualSize to the View’s actual width and height to make the code more scalable and accurate.

Plot wave rise

Implementation under the wave height changes along with the progress, when the progress increases, the wave height increases, when the schedule to reduce wave height decreases, actually very simple, namely p0 – p3, the height of the p1 and p2 changes according to the schedule, and increase the animation, increase code is as follows:

    // Ratio of current progress value to total progress value
    private float currentPercent;
    // Current progress value
    private float currentProgress;
    // Maximum progress
    private float maxProgress;
    // Animate objects
    private WaveProgressAnimat waveProgressAnimat;
    
     /** * Initializes some brush path configuration *@param context
     */
    private void  init(Context context){
        / /...
        // The ratio is initially set to 0
        currentPercent = 0;
        // Progress bar The progress is set to 0
        currentProgress = 0;
        // Set the maximum value of the progress bar to 100
        maxProgress = 100;
        // Animate instantiate
        waveProgressAnimat = new WaveProgressAnimat();

    }
    
       /** * draw wavy lines **@return* /
    private Path paintWavePath(a){
        Clear the route first
        wavePath.reset();
        // Start point moved to (0,waveHeight) p0-P1 height changes with progress
        wavePath.moveTo(0, (1 - currentPercent) * waveActualSize);
        // The maximum number of waves can be drawn
        I < getWidth(); I +=waveLength is not perfect
        // Draw p0-p1 draw wavy lines
        for(int i = 0; i < waveNumber ; i ++){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        // connect p1-p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        // Connect p2-P3
        wavePath.lineTo(0,waveActualSize);
        P3-p0 p3-P0d height changes as progress changes
        wavePath.lineTo(0, (1 - currentPercent) * waveActualSize);
        // Close and fill
        wavePath.close();
        return wavePath;
    }
    
      // Create a new animation class
    public class WaveProgressAnimat extends Animation{


        // applyTransformation is called repeatedly during animation drawing,
        // The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            // Update the ratio
            currentPercent = interpolatedTime * currentProgress / maxProgress;
            // Redrawinvalidate(); }}/** * Sets the progress bar value *@paramCurrentProgress currentProgress *@paramTime Animation duration */
    public void setProgress(float currentProgress,int time){
         this.currentProgress = currentProgress;
         // Change from 0
         currentPercent = 0;
         // Set the animation time
         waveProgressAnimat.setDuration(time);
         // Animation is enabled for the current view
         this.startAnimation(waveProgressAnimat);
    }

Copy the code

Finally, call some code in the Activity:

        // Progress of 50 is 2500 milliseconds
        circleWaveProgressView.setProgress(50.2500);
Copy the code

The final effect is shown below:

Draw wave translation left and right

Implements the above animation of the spike in the waves, the following implementation wave translational animation, add the effect of the shift to the left, here in front of the thought of also achieved the effect of translation, but this is implemented with the above is a little discrepancy, simply is the mobile p0 coordinates, but if p0 moving waves there will be not covered the whole View of the situation, A very common loop is used here. In the background scroll picture of aircraft war, two background pictures are splice together. When the plane starts from the bottom of the first background picture and moves up to the height of the first background picture, the role is put back to the bottom of the first background picture, so as to achieve the effect of background picture cycle. So you start drawing p0-P1, and then as you progress, P0 will move to the left, and the wave that wasn’t in the View at the beginning will move from the right to the left, and when you reach the maximum distance, you’ll redraw the original state, and you’ll have a loop. Let’s start with the diagram:

    // Wave translation distance
    private float moveDistance = 0;
    
     /** * draw wavy lines **@return* /
    private Path paintWavePath(a){
        Clear the route first
        wavePath.reset();
        // Start point moved to (0,waveHeight) p0-P1 height changes with progress
        wavePath.moveTo(-moveDistance,(1 - currentPercent) * waveActualSize);
        // The maximum number of waves can be drawn
        I < getWidth(); I +=waveLength is not perfect
        There is a segment that is out of View, to the right of the distance to the right of View, so * 2, for horizontal displacement
        for(int i = 0; i < waveNumber * 2 ; i ++){
             wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
             wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
        }
        // connect p1-p2
        wavePath.lineTo(waveActualSize,waveActualSize);
        // Connect p2-P3
        wavePath.lineTo(0,waveActualSize);
        P3-p0 p3-P0d height changes as progress changes
        wavePath.lineTo(0, (1 - currentPercent) * waveActualSize);
        // Close and fill
        wavePath.close();
        return wavePath;
    }
    
    
     /** * Sets the progress bar value *@paramCurrentProgress currentProgress *@paramTime Animation duration */
    public void setProgress(final float currentProgress, int time){
         this.currentProgress = currentProgress;
         // Change from 0
         currentPercent = 0;
         // Set the animation time
         waveProgressAnimat.setDuration(time);
         // Set the loop to play
         waveProgressAnimat.setRepeatCount(Animation.INFINITE);
         // Let the animation play at a uniform speed to avoid the phenomenon of waves moving and stopping
         waveProgressAnimat.setInterpolator(new LinearInterpolator());
         waveProgressAnimat.setAnimationListener(new Animation.AnimationListener() {
             @Override
             public void onAnimationStart(Animation animation) {}@Override
             public void onAnimationEnd(Animation animation) {}@Override
             public void onAnimationRepeat(Animation animation) {
                 // When the wave reaches its peak, the speed of movement changes. Set the monitor for the animation. When the animation ends, it runs at 7000 milliseconds, slowing down
                 if(currentPercent == currentProgress /maxProgress){
                     waveProgressAnimat.setDuration(7000); }}});// Animation is enabled for the current view
         this.startAnimation(waveProgressAnimat);
    }
     // Create a new animation class
    public class WaveProgressAnimat extends Animation{


        // applyTransformation is called repeatedly during animation drawing,
        // The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            // When the wave height reaches the maximum, there is no circulation, only translation
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
            }
            // The distance to the left changes according to the animation progress
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            // Redrawinvalidate(); }}Copy the code

The final effect is shown below:

Draws a circular frame background

Here to use the knowledge of PorterDuffXfermode, in fact, is not difficult, first on the porterduff. Mode various modes of effect:

Developers make a big mistake
Android Port DuffxferMode real effect test set (compared to the official demo)
PorterDuff.Mode.SRC_IN
PorterDuff.Mode.SRC_IN

    // Round background brush
    private Paint circlePaint;
    //bitmap
    private Bitmap circleBitmap;
    / / bitmap canvas
    private Canvas bitmapCanvas;
    
      /** * Initializes some brush path configuration *@param context
     */
    private void  init(Context context){
        / /...
        // Start by drawing a circular background
        wavePaint = new Paint();
        // Set the brush to take intersection mode
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        // The circular background is initialized
        circlePaint = new Paint();
        / / color
        circlePaint.setColor(Color.GRAY);
        // Set anti-aliasing
        circlePaint.setAntiAlias(true);
        
    }
    
     @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        Log.d("ssd",getWidth()+"");
        // The cache is used to create a new bitmap based on the parameters
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        // Create a canvas at the base of the bitmap
        bitmapCanvas = new Canvas(circleBitmap);
        // Drawing the center diameter of the circle is easy
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2, circlePaint);
        // Draw wave shapes
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        // Crop the image
        canvas.drawBitmap(circleBitmap, 0.0.null);
        // Draw wavy lines
      // canvas.drawPath(paintWavePath(),wavePaint);

    }

Copy the code

The actual effect is shown below:

res\vaules
attrs.xml
CircleWaveProgressView

<! <declare-styleable name="CircleWaveProgressView"> <! - the color of the wave - > < attr name = "wave_color format =" "color" > < / attr > <! <attr name="circlebg_color" format="color"></attr> <! <attr name="wave_length" format="dimension"></attr> <! <attr name="wave_height" format="dimension"></attr> <! <attr name="currentProgress" format="float"></attr> <! <attr name="maxProgress" format="float"></attr> </declare-styleable>Copy the code

Assign a property value to a custom View:

    // Wave color
    private int wave_color;
    // Round background progress box color
    private int circle_bgcolor;
    
    
    public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // Get attrs configuration attributes
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgressView);
        Dip2px (context,25); // If the XML does not specify wave_length, the default value will be used.
        waveLength = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_length,Density.dip2px(context,25));
        // Get the wave height
        waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_height,Density.dip2px(context,15));
        // Get the wave color
        wave_color = typedArray.getColor(R.styleable.CircleWaveProgressView_wave_color,Color.parseColor("#ff7c9e"));
        // Round background color
        circle_bgcolor = typedArray.getColor(R.styleable.CircleWaveProgressView_circlebg_color,Color.GRAY);
        // Current progress
        currentProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_currentProgress,50);
        // Maximum progress
        maxProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_maxProgress,100);
        // Remember to recycle TypedArray
        // A program maintains a Pool of TypedArray at runtime. When a program is called, it requests an instance from the pool and calls the Recycle () method to release the instance so that it can be reused by other modules.
        // Why use this mode? One of the scenarios for using TypedArray is the above custom View, which is created each time an Activity is created.
        // Therefore, the system needs to create arrays frequently, which is not a small cost to memory and performance. If you do not use pooling mode and let GC collect each time, it is likely to cause OutOfMemory.
        // This is the reason for using the pool + singleton mode, which is why the official documentation repeatedly emphasizes: after use must recycle,recycle,recycle
        typedArray.recycle();
        init(context);
    }
    
    /** * Initializes some brush path configuration *@param context
     */
    private void  init(Context context){
        // Set the width and height of the custom View
        waveDefaultSize = Density.dip2px(context,250);
        // Set the maximum width and height of the custom View
        waveMaxHeight = Density.dip2px(context,300);

        wavePath = new Path();
        wavePaint = new Paint();
        // Set the brush to take intersection mode
        wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        // The circular background is initialized
        circlePaint = new Paint();
        // Set the circular background color
        circlePaint.setColor(circle_bgcolor);
        // Set anti-aliasing
        circlePaint.setAntiAlias(true);
        // Set the wave color
        wavePaint.setColor(wave_color);
        // Set anti-aliasing
        wavePaint.setAntiAlias(true);
        // The ratio is initially set to 0
        currentPercent = 0;
        // Progress bar The progress is set to 0
        currentProgress = 0;
        // Set the maximum value of the progress bar to 100
        maxProgress = 100;
        // Animate instantiate
        waveProgressAnimat = new WaveProgressAnimat();


Copy the code

Here you can customize the wave color, height, width, and circular background color in the layout file:

<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.android.quard.CircleWaveProgressView android:id="@+id/circle_progress" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:wave_color="@color/colorPrimaryDark" app:circlebg_color="@android:color/black" /> </android.support.constraint.ConstraintLayout>Copy the code

The renderings will not be posted.

Draw text progress effect

The simplest is to draw text directly in the View. This is very simple. I used to implement custom View by putting logic in it, which makes the View look bloated and expansibility is not high, because you think, If I want to change font position and style now, I need to change it in this View. If this View can open the interface for processing text, that is, modify text styles only through this interface, so that the text and the progress bar View can be decoupled.

    // Progress displays TextView
    private TextView tv_progress;
    // Progress bar display value monitor interface
    private UpdateTextListener updateTextListener;
    
        // Create a new animation class
    public class WaveProgressAnimat extends Animation{


        // applyTransformation is called repeatedly during animation drawing,
        // The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t){
            super.applyTransformation(interpolatedTime, t);
            // When the wave height reaches the maximum, there is no circulation, only translation
            if(currentPercent < currentProgress / maxProgress){
                currentPercent = interpolatedTime * currentProgress / maxProgress;
                // This is displayed directly according to the progress value
                tv_progress.setText(updateTextListener.updateText(interpolatedTime,currentProgress,maxProgress));
            }
            // The left distance
            moveDistance = interpolatedTime * waveNumber * waveLength * 2;
            // Redrawinvalidate(); }}// Define a value listener
    public interface UpdateTextListener{
        /** * provides interface for external modification of numeric styles, etc. *@paramInterpolatedTime The value is animated from 0 to 1 *@paramCurrentProgress Indicates the value of the progress bar *@paramMaxProgress Indicates the maximum value of the progress bar *@return* /
        String updateText(float interpolatedTime,float currentProgress,float maxProgress);
    }
    // Set the listener
    public void setUpdateTextListener(UpdateTextListener updateTextListener){
        this.updateTextListener = updateTextListener;

    }

    /** ** Set the display *@paramTv_progress content values can be anything * */
    public void setTextViewVaule(TextView tv_progress){
        this.tv_progress = tv_progress;

    }
Copy the code

And then implement the Activity file CircleWaveProgressView UpdateTextListener interface, logic processing:

public class MainActivity extends AppCompatActivity {

    private CircleWaveProgressView circleWaveProgressView;
    private TextView tv_value;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        / / TextView widgets
        tv_value = findViewById(R.id.tv_value);
        // Progress bar control
        circleWaveProgressView = findViewById(R.id.circle_progress);
        // Set TextView to the progress bar
        circleWaveProgressView.setTextViewVaule(tv_value);
        // Set the font value display listener
        circleWaveProgressView.setUpdateTextListener(new CircleWaveProgressView.UpdateTextListener() {
            @Override
            public String updateText(float interpolatedTime, float currentProgress, float maxProgress) {
                // Take one integer and keep two decimal places
                DecimalFormat decimalFormat=new DecimalFormat("0.00");
                String text_value = decimalFormat.format(interpolatedTime * currentProgress / maxProgress * 100) +"%";
                // Finally bring the formatted content (value into the progress bar)
                returntext_value ; }});// Set the progress and animation time
        circleWaveProgressView.setProgress(50.2500); }}Copy the code

Layout file adds a TextView:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.android.quard.CircleWaveProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <TextView
        android:id="@+id/tv_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:textColor="#ffffff"
        android:textSize="24dp"
        />


</android.support.constraint.ConstraintLayout>
Copy the code

The final effect is shown below:

Draw double wave effect

To realize that the translation direction of the second layer wave is opposite to that of the first layer wave, the drawing sequence should be changed. Below:

    // Whether to draw double wavy lines
    private boolean isCanvasSecond_Wave;
    // The color of the second wave
    private int second_WaveColor;
    // The second layer of wave brushes
    private Paint secondWavePaint;
Copy the code

Attrs file adds a second layer of wave color:

<! <declare-styleable name="CircleWaveProgressView"> <! - the color of the wave - > < attr name = "wave_color format =" "color" > < / attr > <! <attr name="circlebg_color" format="color"></attr> <! <attr name="wave_length" format="dimension"></attr> <! <attr name="wave_height" format="dimension"></attr> <! <attr name="currentProgress" format="float"></attr> <! - the biggest progress - > < attr name = "maxProgress format =" float ">" < / attr > <! <attr name="second_color" format="color"></attr> </declare-styleable>Copy the code

Class files:

        // The color of the second wave
        second_WaveColor = typedArray.getColor(R.styleable.CircleWaveProgressView_second_color,Color.RED);
Copy the code

Add to init method:

 // Initialize the second layer of wave brushes
        secondWavePaint = new Paint();
        secondWavePaint.setColor(second_WaveColor);
        secondWavePaint.setAntiAlias(true);
        // To override the first layer wave, choose SRC_ATOP mode. The second layer wave is fully displayed, and the non-intersection part of the first layer is displayed. This pattern can be understood by looking at the image composition article above
        secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        // The initial state does not draw the second wave
        isCanvasSecond_Wave = false;
Copy the code

In the onDraw method add:

@Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

        Log.d("ssd",getWidth()+"");
        // The cache is used to create a new bitmap based on the parameters
        circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
        // Create a canvas at the base of the bitmap
        bitmapCanvas = new Canvas(circleBitmap);
        // Draw the circle with a smaller radius so that the waves fill the entire circle
        bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2 - Density.dip2px(getContext(),8), circlePaint);
        // Draw wave shapes
        bitmapCanvas.drawPath(paintWavePath(),wavePaint);
        // Whether to draw a second wave
        if(isCanvasSecond_Wave){
            bitmapCanvas.drawPath(cavasSecondPath(),secondWavePaint);

        }
        // Crop the image
        canvas.drawBitmap(circleBitmap, 0.0.null);
        // Draw wavy lines
      // canvas.drawPath(paintWavePath(),wavePaint);

    }
    
        // Whether to draw a second wave
    public void isSetCanvasSecondWave(boolean isCanvasSecond_Wave){
        this.isCanvasSecond_Wave = isCanvasSecond_Wave;
    }

    /** * Draw the second wave method *@return* /
    private Path cavasSecondPath(a){
        float secondWaveHeight = waveHeight;
        wavePath.reset();
        // Move to the upper right, at p1
        wavePath.moveTo(waveActualSize + moveDistance, (1 - currentPercent) * waveActualSize);
        //p1 - p0
        for(int i = 0; i < waveNumber * 2 ; i ++){
            wavePath.rQuadTo(-waveLength / 2,secondWaveHeight,-waveLength,0);
            wavePath.rQuadTo(-waveLength / 2,-secondWaveHeight,-waveLength,0);
        }
        // P0-p3-p0d height changes as the progress changes
        wavePath.lineTo(0, waveActualSize);
        // Connect p3-P2
        wavePath.lineTo(waveActualSize,waveActualSize);
        // connect p2-P1
        wavePath.lineTo(waveActualSize,(1 - currentPercent) * waveActualSize);
        // Close and fill
        wavePath.close();
        return wavePath;

    }
Copy the code

Finally set in Activty file:

        // Whether to draw a second wave
        circleWaveProgressView.isSetCanvasSecondWave(true);
Copy the code

The final effect is shown below:

conclusion

After the derivation of Bessel formula and the realization of small examples, there is a more profound impression. There are a lot of things does not look so touching, like when you get a development demand, technology assessment found themselves without before use technology is used, this time is how to realize the ideas and methods, with reference to others or the cheek to ask technical elite, learned is their own, more than just effort easier.

Example source code