前言

在Path在UI系统傍边不管是在自定义View还是动画,都占有无足轻重的地位。制作Path,能够经过Android供给的API,或者是贝塞尔曲线、数学函数、图形组合等等方式,而要获取Path上每一个构成点的坐标,一般需求知道Path的函数办法,例如求解贝塞尔曲线上的点的De Casteljau算法,但对于一般的Path来说,是很难经过简略的函数办法来进行核算的,那么,今日需求了解的便是PathMeasure,关于Path丈量的运用

PathMeasure

今日需求了解的API十分简略,关于Path的丈量,咱们首要来看一些作用

这种load作用咱们经常在项目傍边遇见,那么其中有一部分作用是经过丈量Path来进行实现的

那么首要咱们来看到PathMeasure这个类,那么具体API具体介绍我就列入到下面,今日最主要的中心是,掌握这个类的运用技巧,而不是死板的API,那么咱们来首要先看下这个类傍边的API

公共办法

    回来值                       办法名
    void setPath(Path path, boolean forceClosed) 相关一个Path
    boolean isClosed()       是否闭合
    float getLength()   获取Path的长度
    boolean nextContour()   跳转到下一个轮廓
    boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)    截取片段
    boolean getPosTan(float distance, float[] pos, float[] tan) 获取指定长度的方位坐标及该点切线值
    boolean getMatrix(float distance, Matrix matrix, int flags) 获取指定长度的方位坐标        及该点Matrix

源码

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.
 *  创立一个空的PathMeasure
 *用这个结构函数可创立一个空的 PathMeasure,
 * 可是运用之前需求先调用 setPath 办法来与 Path 进行相关。
 * 被相关的 Path 必须是现已创立好的,
 * 假如相关之后 Path 内容进行了更改,
 * 则需求运用 setPath 办法重新相关。
 * 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() {
    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.
 * 创立 PathMeasure 并相关一个指定的Path(Path需求现已创立完结)。
 * 用这个结构函数是创立一个 PathMeasure 并相关一个 Path,
 * 其实和创立一个空的 PathMeasure 后调用 setPath 进行相关作用是一样的,
 * 同样,被相关的 Path 也必须是现已创立好的,假如相关之后 Path 内容进行了更改,
 * 则需求运用 setPath 办法重新相关。
 *该办法有两个参数,第一个参数自然便是被相关的 Path 了,
 * 第二个参数是用来确保 Path 闭合,假如设置为 true,
 * 则不管之前Path是否闭合,都会自动闭合该 Path(假如Path能够闭合的话)。
 * 在这里有两点需求明确:
 * 1.不管 forceClosed 设置为何种状况(true 或者 false),
 * 都不会影响原有Path的状况,即 Path 与 PathMeasure 相关之后,之前的的 Path 不会有任何改变。
 * 2.forceClosed 的设置状况或许会影响丈量成果,
 * 假如 Path 未闭合但在与 PathMeasure 相关的时分设置 forceClosed 为 true 时,
 * 丈量成果或许会比 Path 实践长度稍长一点,获取到到是该 Path 闭合时的状况。
 * @param path The path that will be measured by this object 被相关的Path
 * @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'd
    mPath = path;
    native_instance = native_create(path != null ? path.readOnlyNI() : 0,
                                    forceClosed);
}
/**
 * Assign a new path, or null to have none.
 *  相关一个Path
 */
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.
 * 回来当时轮廓的总长度,或者假如没有途径,则回来0。与此衡量目标相相关。
 */
public float getLength() {
    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.
 *  获取指定长度的方位坐标及该点切线值
 * @param distance The distance along the current contour to sample 方位
 * @param pos If not null, returns the sampled position (x==[0], y==[1]) 坐标值
 * @param tan If not null, returns the sampled tangent (x==[0], y==[1])  切线值
 * @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float 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 {@link android.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>
 * 给定发动和中止间隔,
 * 在DST中回来中心段。
 * 假如该段为零长度,则回来false,
 * 不然回来true。
 * StestD和Stutd被固定到合法值(0…GigLangTh())。
 * startD>=stopD,则回来false(并保持DST未被触碰)。
 * 假如有一个假设是正确的,就开端以一个模式开端。
 *
 * 早期版别,成果途径或许不会在硬件加速中显现。
 * Canvas。
 * 一个简略的解决办法是在这个途径中增加一个操作,
 * 这样的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() {
    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() {
    return native_nextContour(native_instance);
}
protected void finalize() 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, float distance, float pos[], float tan[]);
private static native boolean native_getMatrix(long native_instance, float distance, long 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;
}

从源码上剖析咱们能够看得到其实这个类便是为了让咱们丈量到当时Path所在的方位 API不多,那么究竟怎样运用呢?

首要咱们来剖析这个作用

很明显咱们看到当时这里是一个圆,运用了一张图片,让这张图能够沿着当时的这个圆进行移动

那么,这个圆形是咱们用Path所制作的,那么当时Path会记录下当时圆的所有点,而咱们需求将那个箭头图片制作到咱们path的点上面,并且按照圆形视点来进行操控而图形是这样的

那么这个时分咱们能够反映过来,去得到当时图片进行旋转,能够做到这一点, 可是咱们如何判断这旋转的视点?

而丈量傍边供给了

/**
 * 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.
 *  获取指定长度的方位坐标及该点切线值
 * @param distance The distance along the current contour to sample 
                PATH起点的长度取值规模: 0 <= distance <= getLength
 * @param pos If not null, returns the sampled position (x==[0], y==[1]) 坐标值
 * @param tan If not null, returns the sampled tangent (x==[0], y==[1])  切线值
 * @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float 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);
}

那么此刻看到这个getPosTan办法其实咱们就能够很明显了解到,经过这个办法咱们能够根据path的长度值,去获得指定长度所在的XY和切线XY,见下图


那么此刻能够看到所谓的切线,下面扫盲,段位高跳过 几何 上,切线指的是一条刚好触碰到 曲线 上某一点的直线。更准确地说,当切线经过曲线上的某点(即 切点 )时,切线的方向与曲线上该点的方向是相同的。平面几何 中,将和圆只有一个公共交点的直线叫做圆的切线
正切函数 是 直角三角形 中,对边与邻边的比值叫做正切。放在 直角坐标系 中(如图)即 tanθ=y/x
而tan便是咱们的 正切值 如上图,参阅上图
随机选取了一个橙点(具体方位),那么切线是和橙点相交的这条线,切线视点为笔直关系,所以如下图 真实不理解TAN的话,你们就理解为当时得到了圆心坐标,因为圆的切线是圆心《主张去温习下初中数学》

那么此刻,咱们拿到的getPosTan办法,能够把当时这个点,和这个点的正切值拿到,咱们能够经过反正切核算获得视点,那么橙线和X轴的夹角其实实践上应该是咱们到时分显现曩昔的视点,那么此刻,看下图

红线所制作的视点是咱们当时视点,绿线制作的是需求旋转的视点, 那么咱们现在手里拥有的资源是,当时正切值,经过正切值咱们运用
公式能够核算得到当时视点

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

而反切视点的话是
Math.atan2(tan[1], tan[0]) * 180 / PI
这个便是咱们的要移动的视点

那么咱们当时上面这个案例就能完结

  public class MyView1 extends View {
private float currentValue = 0;     // 用于纪录当时的方位,取值规模[0,1]映射Path的整个长度
private float[] pos;                // 当时点的实践方位
private float[] tan;                // 当时点的tangent值,用于核算图片所需旋转的视点
private Bitmap mBitmap;             // 箭头图片
private Matrix mMatrix;             // 矩阵,用于对图片进行一些操作
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;       // 缩放图片
    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);
    // 平移坐标系
    canvas.translate(mViewWidth/2,mViewHeight/2);
    // 画坐标线
    canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
    canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
    Path path = new Path();                                 // 创立 Path
    path.addCircle(0, 0, 200, Path.Direction.CW);           // 增加一个圆形
    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);     // 创立 PathMeasure
    currentValue += 0.005;                                  // 核算当时的方位在总长度上的比例[0,1]
    if (currentValue >= 1) {
        currentValue = 0;
    }
    // 方案一
    // 获取当时方位的坐标以及趋势
    measure.getPosTan(measure.getLength() * currentValue, pos, tan);
    canvas.drawCircle(tan[0],tan[1],20,mDeafultPaint);
    // 重置Matrix
    mMatrix.reset();
    // 核算图片旋转视点
    float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
    // 旋转图片
    mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
    // 将图片制作中心调整到与当时点重合
    mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);
    // 方案二
    // 获取当时方位的坐标以及趋势的矩阵
    //measure.getMatrix(measure.getLength() * currentValue, mMatrix,
    //PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
    // 将图片制作中心调整到与当时点重合(留意:此处是前乘pre)
    //mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
    canvas.drawPath(path, mDeafultPaint);
    canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);
    invalidate();
}
}

更多Android 常识点可参阅

Android 功能调优系列https://0a.fit/dNHYY

Android 车载学习指南https://0a.fit/jdVoy

Android Framework中心常识点笔记https://0a.fit/acnLL

Android 八大常识系统https://0a.fit/mieWJ

Android 中高级面试题锦https://0a.fit/YXwVq