“This is my 12th day of the November Gwen Challenge.The final text challenge in 2021”


preface

Path plays an important role in the UI architecture, both in custom views and animation. Path can be drawn through the API provided by Android, or bezier curves, mathematical functions, graph combinations, etc. To obtain the coordinates of each component point on the Path, it is generally necessary to know the function method of Path, such as the De Casteljau algorithm to solve the points on the Bezier curve. However, for general Path, it is difficult to calculate by simple function method. Therefore, what we need to know today is PathMeasure, which is about the application of Path measurement

PathMeasure

The API you need to know today is very simple, and for measuring Path, let’s first look at some effects

We often see this load effect in projects, and some of it is implemented by measuring Path

First of all, let’s look at the class Of PathMeasure, and I will list the specific API details below. Today, the most important thing is to master the skills of using this class, rather than rigid API, so let’s take a look at the API of this class first

Public methods

Return value method namevoid setPath(Path path, boolean forceClosed)Associate a Pathboolean isClosed(a)Whether closedfloat getLength(a)Gets the length of Pathboolean nextContour(a)Jump to the next contourboolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)Interception of fragmentsboolean getPosTan(floatdistancefloat[] pos, float[] tan)Gets the position coordinates of the specified length and the value of the point tangentboolean getMatrix(float distance, Matrix matrix, int flags)Gets the position coordinates of the specified length and the point MatrixCopy the code

The source code

public class PathMeasure {
private Path mPath;

/** * Create an empty PathMeasure object. To uses this to measure the length * of a path, And /or to find the position and tangent along it, call * setPath. * Create an empty PathMeasure. * However, you need to call the setPath method to associate with Path before using it. * The Path to be associated must already be created. * If the Path content is changed after the association, the setPath method must be used to re-associate the Path. * Note that once a path is associated with the measure object, it is * undefined if the path is subsequently modified and the the measure object * is used. If the path is modified, you must call setPath with the path. */
public PathMeasure(a) {
    mPath = null;
    native_instance = native_create(0.false);
}

/** * Create a PathMeasure object associated with the specified path object * (already created and specified). The measure object can now return the * path's length, and the position and tangent of any position along the * path. * * Note that once a path is associated with the measure object, it is * undefined if the path is subsequently modified and the the measure object * is used. If the path is modified, You must call setPath with the path. * Create a PathMeasure and associate it with a specified path (the path must already be created). * Using this constructor to create a PathMeasure and associate it with a Path is the same as calling setPath after creating an empty PathMeasure. * Similarly, the Path to be associated must be already created. If the Path content changes after the association, * then the setPath method needs to be re-associated. * This method takes two arguments. The first argument is the associated Path, and the second argument is used to ensure that the Path is closed. If set to true, the Path will be closed (if it can be closed) regardless of whether it was previously closed. * There are two points to be made clear here: * 1. No matter what state forceClosed is set to (true or false), * will not affect the state of the original Path, that is, after the Path is associated with PathMeasure, the previous Path will not change. * 2. The setting state of forceClosed may affect the measurement result. * If the Path is not closed but forceClosed is set to true when associated with PathMeasure, * the measurement result may be slightly longer than the actual Path length. Gets the state of the Path when it is closed. *@paramPath The path that will be measured by The path associated with this object *@param forceClosed If true, then the path will be considered as "closed"
 *        even if its contour was not explicitly closed.
 */
public PathMeasure(Path path, boolean forceClosed) {
    // The native implementation does not copy the path, prevent it from being GC'dmPath = path; native_instance = native_create(path ! =null ? path.readOnlyNI() : 0,
                                    forceClosed);
}

/** * Assign a new path, or null to have none
public void setPath(Path path, boolean forceClosed) { mPath = path; native_setPath(native_instance, path ! =null ? path.readOnlyNI() : 0,
                   forceClosed);
}

/** * Return the total length of the current contour, Or 0 if no path is * associated with this measure object. * Returns the total length of the current contour, or 0 if there is no path. Associated with this measurement object. * /
public float getLength(a) {
    return native_getLength(native_instance);
}

/** * Pins distance to 0 <= distance <= getLength(), and then computes the * corresponding position and tangent. Returns false if there is no path, Or a zero-length path was specified, in which case position and tangent * are unchanged@paramDistance The distance along The current contour to sample location *@paramPos If not null, returns the sampled Position (x==[0], y==[1]@paramTangent (x==[0], y==[1]) tangent value *@return false if there was no path associated with this measure object
*/
public boolean getPosTan(floatdistancefloat pos[], float tan[]) {
    if(pos ! =null && pos.length < 2|| tan ! =null && tan.length < 2) {
        throw new ArrayIndexOutOfBoundsException();
    }
    return native_getPosTan(native_instance, distance, pos, tan);
}

public static final int POSITION_MATRIX_FLAG = 0x01;    // must match flags in SkPathMeasure.h
public static final int TANGENT_MATRIX_FLAG  = 0x02;    // must match flags in SkPathMeasure.h

/**
 * Pins distance to 0 <= distance <= getLength(), and then computes the
 * corresponding matrix. Returns false if there is no path, or a zero-length
 * path was specified, in which case matrix is unchanged.
 *
 * @param distance The distance along the associated path
 * @param matrix Allocated by the caller, this is set to the transformation
 *        associated with the position and tangent at the specified distance
 * @param flags Specified what aspects should be returned in the matrix.
 */
public boolean getMatrix(float distance, Matrix matrix, int flags) {
    return native_getMatrix(native_instance, distance, matrix.native_instance, flags);
}

/** * Given a start and stop distance, return in dst the intervening * segment(s). If the segment is zero-length, return false, else return * true. startD and stopD are pinned to legal values (0.. getLength()). * If startD >= stopD then return false (and leave dst untouched). * Begin the segment with a moveTo if startWithMoveTo is true. * * <p>On {@linkandroid.os.Build.VERSION_CODES#KITKAT} and earlier * releases, the resulting path may not display on a hardware-accelerated * Canvas. A simple workaround is to add a single operation To this path, * such as <code>dst.rLineTo(0, 0)</code>.</p> * Given start and stop distances, * returns middle segments in DST. * Returns false if the segment is zero-length, * true otherwise. * StestD and Stutd are fixed to legal values (0... GigLangTh ()). * startD> = stopD, returns false (and keeps DST untouched). * If one hypothesis is correct, start with a pattern. * * In earlier versions, the resulting path may not be displayed in hardware acceleration. * Canvas. * A simple workaround is to add an operation to the path, * such as sdst.rlin to (0, 0) */
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
    // Skia used to enforce this as part of it's API, but has since relaxed that restriction
    // so to maintain consistency in our API we enforce the preconditions here.
    float length = getLength();
    if (startD < 0) {
        startD = 0;
    }
    if (stopD > length) {
        stopD = length;
    }
    if (startD >= stopD) {
        return false;
    }

    return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo);
}

/** * Return true if the current contour is closed() *
public boolean isClosed(a) {
    return native_isClosed(native_instance);
}

/** * Move to the next contour in the path. Return true if one exists, or * false if we're done with the path. */
public boolean nextContour(a) {
    return native_nextContour(native_instance);
}

protected void finalize(a) throws Throwable {
    native_destroy(native_instance);
    native_instance = 0;  // Other finalizers can still call us.
}

private static native long native_create(long native_path, boolean forceClosed);
private static native void native_setPath(long native_instance, long native_path, boolean forceClosed);
private static native float native_getLength(long native_instance);
private static native boolean native_getPosTan(long native_instance, floatdistancefloat pos[], float tan[]);
private static native boolean native_getMatrix(long native_instance, floatdistancelong native_matrix, int flags);
private static native boolean native_getSegment(long native_instance, float startD, float stopD, long native_path, boolean startWithMoveTo);
private static native boolean native_isClosed(long native_instance);
private static native boolean native_nextContour(long native_instance);
private static native void native_destroy(long native_instance);

/* package */private long native_instance;
}
Copy the code

This class is designed to measure the current Path location. There are not many apis, so how to use it? First, let’s analyze the effect

Obviously we see that this is currently a circle, and we use a picture to move the picture around the current circle. So, this circle is what we drew with Path, so the current Path records all the points of the current circle, and we need to draw that arrow picture on top of our Path and manipulate it according to the Angle of the circle and the graph looks like this

So at this point we can reflect, we can get the current image and rotate, we can do that, but how do we determine the Angle of rotation? And the measurements provide

/** * Pins distance to 0 <= distance <= getLength(), and then computes the * corresponding position and tangent. Returns false if there is no path, Or a zero-length path was specified, in which case position and tangent * are unchanged@paramDistance The distance along The current contour to sample PATH Length of The starting point value range: 0 <= distance <= getLength *@paramPos If not null, returns the sampled Position (x==[0], y==[1]@paramTangent (x==[0], y==[1]) tangent value *@return false if there was no path associated with this measure object
*/
public boolean getPosTan(floatdistancefloat pos[], float tan[]) {
    if(pos ! =null && pos.length < 2|| tan ! =null && tan.length < 2) {
        throw new ArrayIndexOutOfBoundsException();
    }
    return native_getPosTan(native_instance, distance, pos, tan);
}
Copy the code

The getPosTan method is an obvious way to get XY and tangent to the specified length based on the length value of path, as shown in the figure below

So now you can see what’s called a tangent line, and I’m going to skip the height geometrically, a tangent line is a line that just touches a point on the curve. More precisely, when a tangent line passes through a point on the curve (the tangent point), the direction of the tangent line is the same as that of that point on the curve. In plane geometry, the line that has only one common intersection with the circle is called the tangent of the circle. The tangent function is the ratio of the opposite side to the adjacent side in a right triangle.

In rectangular coordinates, tangent theta is equal to y over x, and tangent is our tangent. As shown above, refer to the figure above. If an orange point is randomly selected (the exact position), the tangent line is the line that intersects the orange point, and the tangent Angle is vertical.

So in the picture below, if you really don’t understand the tangent, you understand that you have the coordinates of the center of the circle, because the tangent of the circle is the center of the circle

So at this point, the getPosTan method that we’ve got takes the tangent of this point and this point, and we can get the Angle by calculating the inverse tangent, so the Angle between the orange line and the X axis should actually be the Angle that we’re going to display in the past. So at this point, look at the figure below

The Angle that the red line plots is our current Angle, and the green line plots the Angle that we need to rotate, so the resource that we now have in our hands is the current tangent value, and by the tangent value we apply. The formula will calculate the current Angle

Math.tan2(tan[1], tan[0]) * 180 / PI

The inverse tangent Angle is math.atan2 (tan[1], tan[0]) * 180 / PI

This is the Angle we want to move, so we can do this case

  public class MyView1 extends View {
private float currentValue = 0;     // Record the current location. The value ranges from 0 to 1

private float[] pos;                // The actual position of the current point
private float[] tan;                // The tangent value of the current point is used to calculate the Angle of rotation required for the image
private Bitmap mBitmap;             // Arrow image
private Matrix mMatrix;             // matrix, used to perform some operations on the image
private Paint mDeafultPaint;
private int mViewWidth;
private int mViewHeight;
private Paint mPaint;

public MyView1(Context context) {
    super(context);
    init(context);
}

private void init(Context context) {
    pos = new float[2];
    tan = new float[2];
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 8;       // Zoom the image
    mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
    mMatrix = new Matrix();

    mDeafultPaint = new Paint();
    mDeafultPaint.setColor(Color.RED);
    mDeafultPaint.setStrokeWidth(5);
    mDeafultPaint.setStyle(Paint.Style.STROKE);

    mPaint = new Paint();
    mPaint.setColor(Color.DKGRAY);
    mPaint.setStrokeWidth(2);
    mPaint.setStyle(Paint.Style.STROKE);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mViewWidth = w;
    mViewHeight = h;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    // Shift coordinate system
    canvas.translate(mViewWidth/2,mViewHeight/2);
    // Draw the coordinate lines
    canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
    canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);

    Path path = new Path();                                 / / create the Path

    path.addCircle(0.0.200, Path.Direction.CW);           // Add a circle
    Log.i("barry"."----------------------pos[0] = " + pos[0] + "pos[1] = " +pos[1]);
    Log.i("barry"."----------------------tan[0] = " + tan[0] + "tan[1] = " +tan[1]);
    PathMeasure measure = new PathMeasure(path, false);     / / create PathMeasure

    currentValue += 0.005;                                  // calculate the ratio of the current position to the total length [0,1]
    if (currentValue >= 1) {
        currentValue = 0;
    }

    / / a
    // Get the current position and trend
    measure.getPosTan(measure.getLength() * currentValue, pos, tan);
    canvas.drawCircle(tan[0],tan[1].20,mDeafultPaint);

    / / reset Matrix
    mMatrix.reset();
    // Calculate the rotation Angle of the image
    float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
    // Rotate the image
    mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
    // Adjust the drawing center of the image to coincide with the current point
    mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);

    / / 2
    // Get the current position and trend matrix
    //measure.getMatrix(measure.getLength() * currentValue, mMatrix,
    //PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
    // Adjust the drawing center of the image to coincide with the current point (note: this is pre)
    //mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);canvas.drawPath(path, mDeafultPaint); canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); invalidate(); }}Copy the code