preface

Let’s review what we have covered in the previous five articles, first from View basics, event distribution, View workflow, and then Paint and Canvas. This article is divided into two parts. First Path, then PathMeaure, if you want to make a more dazzling animation, then it is indispensable to leave these two classes, the following is still to the use of API and demo actual combat to explain.

Path

Path is a very important concept. It’s almost as important in custom views as Paint, so what can you do with it? It’s not too much of an overstatement to say that Path can do everything, because if you give me any Path, I’ll be able to plot it. Let’s first familiarize ourselves with the API, see the following table:

API function instructions
moveTo The starting point of mobile Moves the starting position of the next operation
setLastPoint Set the end point Resets the position of the last point in the current path. If called before drawing, it has the same effect as moveTo
lineTo Connecting line Adds a point to the current point
close A closed path Connect the first point to the last point to form a closed interval
addRect,addRoundRect,addOval,addCircle,addPath,addArc,arcTo Add content Add (rectangle, rounded rectangle, ellipse, circle, Path, arc) to the current Path
isEmpty Whether is empty Check whether the current Path is empty
isRect Rectangle or not Check whether Path is a rectangle
set Replace the path Replace everything in the current path with the new path
offset Migration path Offsets previous operations on the current path (does not affect subsequent operations)
quadTo,cubicTo Bessel curve Methods for quadratic and cubic Bezier cancellations respectively
rMoveTo, rLineTo,rQuadTo,rCubicTo RXXX method The method without R is based on the far point coordinate system (offset), while the rXXX method is based on the current point coordinate system (offset).
setFillType, getFillType,isInverseFilltype,toggleInverseFilltype Fill mode Set, get, judge and toggle fill modes
incReserve Prompt method Indicates how many points Path has left to join
op Boolean operations Boolean operation on 2 paths (take intersection union)
computeBounds Compute the Path boundary Calculate the boundary
reset,rewind Reset the path Clear the contents of Path. Reset does not retain the internal data structure but retains Filltype. Rewind retains the internal data structure but does not retain Filltype
transform Matrix operations Matrix transformation
  1. moveTo,lineTo,setLastPoint,close

    // Connect from 0.0 to 400,600
    mPath.lineTo(400f,600f)
    // resets the last point of the previous operation from 0,0 to 600,200.
    //mPath.setLastPoint(600f,200f)
    // Connect 900,100 from 400,600
    mPath.lineTo(900f,100f)
    // Start drawinganvas!! .drawPath(mPath,mPathPaint)Copy the code

    Let’s uncomment the above comment with the following code:

    // Connect from 0.0 to 400,600
    mPath.lineTo(400f,600f)
    // resets the last point of the previous operation from 0,0 to 600,200.
    mPath.setLastPoint(600f,200f)
    // Connect 900,100 from 600,200
    mPath.lineTo(900f,100f)
    // Start drawinganvas!! .drawPath(mPath,mPathPaint)Copy the code

    The implementation effect is as follows:

    In the figure above, we can see that setLastPoint is set and then changes the last coordinate point, which is to update the last coordinate point. We can see that every time we draw from the coordinate Angle (0,0), is there a way to specify where to draw from? You can try moveTo, which specifies the starting point of your path, as follows:

            //moveTo sets the starting point
            mPath.moveTo(600f,200f)
            // Connect from 0.0 to 400,600
            mPath.lineTo(400f,600f)
            mPath.lineTo(800f,300f)
            // The last point and the starting point are closed
            mPath.close()
    Copy the code

    Using Path#moveTo to start drawing with a Path starting point of (400,600), we call Path# Close to close the closed line.

  2. AddXxx series

    We draw it in the order of rectangle, rounded rectangle, ellipse, circle and arc

    // path.direction. CW/CCW clockwise/counterclockwise
    
    //1. Add rectangle to Path
    void addRect (float left, float top, float right, float bottom, Path.Direction dir)
    //2. Add rounded rectangle to Path
    void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
    void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
    //3. Add ellipse to Path
    void addOval (RectF oval, Path.Direction dir)
    //4. Add circles to Path
    void addCircle (float x, float y, float radius, Path.Direction dir)
    //5. Add an arc to Path
    void addArc (RectF oval, float startAngle, float sweepAngle)
    // Add an arc to the path. If the arc's starting point is not the same as the last coordinate point, connect the two points
    void arcTo (RectF oval, float startAngle, float sweepAngle)
    void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
    Copy the code
            mPathPaint.textSize = 50f
            //1. Add rectangle to Path
            mPath.addRect(100f,300f,400f,700f,Path.Direction.CW)/ / clockwisecanvas!! .drawText("1".200f,500f,mPathPaint)
    
            //2. Add rounded rectangle to Path
            mPath.addRoundRect(100f + 500.300f,1000f ,700f,30f,30f,Path.Direction.CCW)/ / counterclockwisecanvas!! .drawText("2".800f,500f,mPathPaint)
    
            //3. Add ellipse to Path
            mPath.addOval(100f,1300f,600f ,1000f,Path.Direction.CCW)/ / counterclockwisecanvas!! .drawText("3".300f,1150f,mPathPaint)
    
            //4. Add circles to Path
            mPath.addCircle(850f,1200f ,150f,Path.Direction.CCW)/ / counterclockwisecanvas!! .drawText("4".850f,1200f,mPathPaint)
    
            //5. Add an arc to Path
            // Add an arc to the path. If the arc's starting point is not the same as the last coordinate point, connect the two points
            mPath.addArc(100f,1500f,600f,1800f,0f,300f) canvas!! .drawText("5".300f,1550f,mPathPaint)
    				
            mPath.arcTo(650f,1500f,800f,1800f,0f,180f,true) canvas!! .drawText("6".750f,1550f,mPathPaint) canvas!! .drawPath(mPath, mPathPaint)Copy the code

    Note that the last parameter of addTo, forceMoveTo, means “whether to forceMoveTo”, that is, whether to use moveTo to move variables to the beginning of the arc, which means:

    forceMoveTo meaning Equivalent method
    true Move the last point to the beginning of the arc, that is, do not connect the last point to the beginning of the arc public void addArc (RectF oval, float startAngle, float sweepAngle)
    false Instead of moving, connect the last point to the beginning of the arc public void arcTo (RectF oval, float startAngle, float sweepAngle)
  3. computeBounds,set,setPath

    1. Start by drawing a circle and rectangle

      mPath.addCircle(500f, 500f, 150f, Path.Direction.CW) canvas!! .drawPath(mPath, mPathPaint)/ / draw the Path
      
      
      canvas.drawRect(300f,800f,800f,1300f ,mPathPaint)   // Draw a rectangle
      Copy the code
    2. Rewrite onTouchEvent to implement the click event

      override fun onTouchEvent(event: MotionEvent): Boolean {
          when (event.action) {
            	// Events need to be pressed
              MotionEvent.ACTION_DOWN -> return true
              MotionEvent.ACTION_UP -> {
                  val rectF = RectF()
                	// Compute the Path boundary
                  mPath.computeBounds(rectF, true)
                	// Put the boundary into the rectangle area
                  region.setPath(
                      mPath,
                      Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt())
                  )
                  if (region.contains(event.x.toInt(), event.y.toInt())) {
                      Toast.makeText(context, "Click on the circle", Toast.LENGTH_SHORT).show()
                  }
      						// Replace everything in the current path with the new path
                  region.set(300.800.800.1300)
                  if (region.contains(event.x.toInt(), event.y.toInt())) {
                      Toast.makeText(context, "Hit the rectangle", Toast.LENGTH_SHORT).show()
                  }
              }
          }
          return super.onTouchEvent(event)
      }
      Copy the code
    3. The effect

That’s all we need to learn about Path. Later in this article, we will learn about Path in action.

PathMeasure

In the last part of the article, we looked at paths. Now, it seems that Path has no function except to draw graphics. Of course, if I only display the graphics that Path draws, I will not introduce the main point of this article. The Android SDK provides a very useful API to help developers implement a Path tracking. This API is called PathMeasure, which can achieve complex and beautiful effects.

concept

PathMeasure is similar to a calculator, which can calculate some information about a specified path, such as the total length of the path and the coordinate points corresponding to the specified length.

Use the API

A constructor

/ / 1. Empty the cords
 public PathMeasure()
//2. Path represents a completed path, and forceClosed represents whether the path is finally closed
 public PathMeasure(Path path, boolean forceClosed)
Copy the code

Simple function use

  1. GetLength () function

    The PathMeasure#getLength() function is widely used to obtain a calculated path length. Here is an example of its use.

    Effect:

    Code:

        override fun draw(canvas: Canvas) {
            super.draw(canvas)
            /** * 1. getLength */
            // Move the starting point to position 100,100
            mPath.moveTo(100f,100f)
            // Draw the connection line
            mPath.lineTo(100f,450f)
            mPath.lineTo(450f,500f)
            mPath.lineTo(500f,100f)
            mPathMeasure.setPath(mPath,false)// Not closed
            mPathMeasure2.setPath(mPath,true)/ / closed
            println("forceClosed false pathLength =${mPathMeasure.length}")
            println("forceClosed true pathLength =${mPathMeasure2.length}")
            canvas.drawPath(mPath,mPathPaint)
        }
    Copy the code

    Output:

    System.out: forceClosed false pathLength =1106.6663
    System.out: forceClosed true pathLength =1506.6663
    Copy the code

    As you can see, if forceClosed is set to true/false, the respective paths are measured.

  2. IsClosed () function

    This function is used to determine whether closure is calculated when measuring Path. Therefore, if forceClosed is set to true when Path is associated, this function must also return true.

  3. NextContour () function

    We know that a Path can be composed of multiple curves, but getLength (), getSegment(), or any other function only evaluates the first line segment. NextContour is the function used to jump to the next curve. If the jump succeeds, it will return true. If the jump fails, return false. Here is an example of creating three closed paths and using PathMeasure to measure each one in turn.

    Effect:

    Code:

            /** * 2. nextContour */
            mPath.addCircle(500f,500f,10f,Path.Direction.CW)
            mPath.addCircle(500f,500f,80f,Path.Direction.CW)
            mPath.addCircle(500f,500f,150f,Path.Direction.CW)
            mPath.addCircle(500f,500f,200f,Path.Direction.CW)
    
            mPathMeasure.setPath(mPath,false)// Not closed
    
            canvas.drawPath(mPath,mPathPaint)
    
            do {
                println("forceClosed  pathLength =${mPathMeasure.length}")}while (mPathMeasure.nextContour())
    Copy the code

    Output:

    2019- 12- 03 22:37:22.340 18501- 18501./? I/System.out: forceClosed  pathLength =62.42697
    2019- 12- 03 22:37:22.341 18501- 18501./? I/System.out: forceClosed  pathLength =501.84265
    2019- 12- 03 22:37:22.341 18501- 18501./? I/System.out: forceClosed  pathLength =942.0967
    2019- 12- 03 22:37:22.341 18501- 18501./? I/System.out: forceClosed  pathLength =1256.1292
    Copy the code

    Here we go through do… Combine the while loop with the measure.nextcontour () function to get all the curves in the Path in turn

    From this example, we can see that the curves obtained by the PathMeasure#nextContour function will be in the same order as the Path was added

GetSegment () function

//startD: The length from the start interception position to the start Path
//stopD: Stops the length between the interception position and the Path start point
// DST: the intercepted Path will be added to DST. Note that it is added, not replaced
//startWithMoveTo: Whether the start point uses moveTo
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
Copy the code

GetSegment Is used to capture a segment of the entire Path. Parameters startD and stopD control the length of the truncated Path, and save the truncated Path to parameter DST. The final parameter, startWithMoveTo, indicates whether to use moveTo to move the new start point of a Path to the start point of the resulting Path. This parameter is usually set to true to ensure that the Path is properly and completely truncated each time. This parameter is used with DST. Because the Path stored in DST is continuously added, rather than overwritten each time; If set to false, new fragments are evaluated from the end of the previous Path, which ensures that truncated Path fragments are contiguous.

Note:

  • If startD, stopD values are not in the range of values, or startD == stopD, false is returned and DST has no Path data.
  • When hardware acceleration is enabled, drawing will have problems, so getSegment needs to be called in the constructorsetLayerType(LAYER_TYPE_SOFTWARE,null)Function to disable hardware acceleration

GetSegment example:

        /** * 3. getSegment */
        mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
        mPathMeasure.setPath(mPath,false)// Not closed
        val segment = mPathMeasure.getSegment(50f, 500f, mTempPath, true)
        println("Whether interception was successful:$segment")
        canvas.drawPath(mTempPath,mPathPaint)
Copy the code

Effect:

Note:

If startWithMoveTo is true, the truncated path fragment remains unchanged. If startWithMoveTo is false, the start point of the truncated Path fragment is moved to the last point in DST to ensure continuity of the DST Path.

Implement a real-time capture of an animation:

Code implementation:

  1. Define a value animation

            val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
            valueAnimator.addUpdateListener {
                animation -> stopValues = animation.animatedValue as Float
                invalidate()
            }
            valueAnimator.repeatCount  = ValueAnimator.INFINITE
            valueAnimator.setDuration(1500)
            valueAnimator.start()
    Copy the code
  2. Real-time interception drawing

            mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
            mPathMeasure.setPath(mPath,false)// Not closed
            mTempPath.rewind()
            stop =   mPathMeasure.length * stopValues
            val start = (stop - (0.5 - Math.abs(stopValues - 0.5)) * mPathMeasure.length).toFloat()
            val segment = mPathMeasure.getSegment(start, stop, mTempPath, true)
            println("Total length:${mPathMeasure.length}Whether interception is successful:$segment + start:$start  stop:$stop")
            canvas.drawPath(mTempPath,mPathPaint)
    Copy the code

getPosTan

This method is used to find the position of a length along the path and the value of the tangent of that position

//distance: distance from the start of the Path, range: 0 <= distance <= getLength
//pos: the position of the current point on the canvas, with two values, respectively x and y.
Math.atan2(tan[1], tan[0]);
boolean getPosTan (floatdistancefloat[] pos, float[] tan)
Copy the code

GetPosTan: getPosTan: getPosTan: getPosTan: getPosTan: getPosTan

Doesn’t it feel cool, but how did we do it? Let’s take a look at the core code, as follows:

    override fun onDraw(canvas: Canvas?). {
        super.onDraw(canvas)
        // Clear the path data
        mTempPath.rewind()
        // Draw a simulated road
        addLineToPath()
        // Measure path, closemPathMeasure!! .setPath(mTempPath,true)
        // Dynamically changing values
        mCurValues += 0.002f
        if (mCurValues >= 1) mCurValues = 0f
        // Get the sine coordinate of the current pointmPathMeasure!! .getPosTan(mPathMeasure!! .length * mCurValues, pos, tan)// Use sine to get the current Angle
        valy = tan!! [1].toDouble()
        valx = tan!! [0].toDouble()
        var degrees = (Math.atan2(y, x) * 180f / Math.PI).toFloat()
        println("Point of view:$degrees") mMatrix!! .reset()// Get the bitmap to the desired rotation Angle, then rotate the matrixmMatrix!! .postRotate(degrees, mBitmap!! .width /2.toFloat(), mBitmap!! .height /2.toFloat())
      	// Get the pos point on path and move along with the pointmMatrix!! .postTranslate(pos!! [0] - mBitmap!! .getWidth() /2, pos!! [1] - mBitmap!! .getHeight() /2)
        // Draw the Bitmap and pathcanvas!! .drawPath(mTempPath, mTempPaint) canvas!! .drawBitmap(mBitmap!! , mMatrix!! , mTempPaint)/ / redraw
        postInvalidate()
    }
Copy the code

We can get the tangent of the current point according to PathMeasure#getPosTan [], Then figure out the tan according to Math#atan, and finally figure out the Angle according to the degrees * 180 / π formula. And then the matrix rotates to get a rotated CAR that keeps redrawing so that’s what it looks like. Again, it’s pretty simple.

So let’s just briefly review the trig functions

You can refer to this article to calculate the sine, cosine and tangent values

getMatrix

This method is used to obtain a matrix of positions along a length along the path and the tangent value of that position:

parameter role note
The return value (Boolean) Check whether the command is successfully obtained True indicates success and data will be saved into matrix, false fails and matrix content will not be changed
distance Distance from the start of the Path Value range: 0 <= distance <= getLength
matrix According to matrix encapsulated by Falgs Different things are saved depending on the flags Settings
flags Specify what content is stored in the matrix Optional POSITION_MATRIX_FLAG(position) ANGENT_MATRIX_FLAG(tangent)

In fact, this method is equivalent to the process we encapsulated matrix in the previous example by getMatrix for us, we can directly get a package to matrix, not too fast.

But we see that at the end of the flags option we can choose position or tangent, what if we want to choose both?

If both options to choose, can connect with a | between two options, as follows:

measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
Copy the code

We can replace getPosTan with getMatrix in the above examples to see if it is much simpler:

    override fun onDraw(canvas: Canvas?). {
        super.onDraw(canvas)

        // Clear path data
        mTempPath.rewind()
        // Draw a simulated road
        addLineToPath()
        // Measure path, closemPathMeasure!! .setPath(mTempPath,true)
        // Dynamically changing values
        mCurValues += 0.002f
        if (mCurValues >= 1) mCurValues = 0f


        // Get the current position and trend matrixmPathMeasure!! .getMatrix(mPathMeasure!! .getLength() * mCurValues, mMatrix!! , (PathMeasure.TANGENT_MATRIX_FLAG or PathMeasure.POSITION_MATRIX_FLAG))// Adjust the center of the image to match the current point (offset + rotation)mMatrix!! .preTranslate(-mBitmap!! .getWidth() /2f, -mBitmap!! .getHeight() /2f);



        // Draw the Bitmap and pathcanvas!! .drawPath(mTempPath, mTempPaint) canvas!! .drawBitmap(mBitmap!! , mMatrix!! , mTempPaint)/ / redraw
        postInvalidate()

    }
Copy the code

The realization effect here is the same as the picture, there is no mapping, there is no need to calculate the tan Angle and so on, it looks easier than the first one, the specific use of which depends on the actual requirements of the scene.

In actual combat

A spider’s web

Spiderwebview. kt please go to GitHub

Smiley face loading progress

Implementation principle:

The above effect is achieved by using the Path Path to draw the eyes and mouth, then taking the RectF rectangle bounds through the Path#computeBounds and drawing them, and then animating the constantly redrawn intercepts.

(Ps: The above effect is just an exercise demo and is not recommended for direct use in the project.)

For details of FaceLoadingView, please go to GitHub

The car follows the path

For details CarRotate, go to GitHub

reference

  • www.gcssloop.com/customview/…
  • Custom View – Let’s pull a spider web