PathMeasure的API讲解与实战——Android高级UI

目录
1、前言
2、API讲解
3、实战
4、更多案例
5、写在最后
java

1、前言

2019年了,然而2017计划写的东西还没开始😂,此次的拖延症来的比日常早却去的比日常晚。今天进行分享的是UI中的PathMeasure,同时记录本身在使用过程当中的几个疑惑点。话很少说,开始进入正题。android

2、API讲解

这一小节主要是对PathMeasure的构造方法公有方法进行讲解git

一、构造方法

(1)PathMeasure()

public PathMeasure() 复制代码

方法描述: 建立一个空的PathMeasure,可是使用以前须要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经建立好的,若是关联以后 Path 内容进行了更改,则须要使用 setPath 方法从新关联。github

(2)PathMeasure(Path path, boolean forceClosed)

public PathMeasure(Path path, boolean forceClosed) 复制代码

方法描述: 建立 PathMeasure 并关联一个指定的Path,且Path须要已经建立完成。 这个构造方法其实 和 使用 PathMeasure() 后调用 setPath方法 进行关联一个Path的效果是同样的;固然,被关联的 Path 也必须是已经建立好的,若是关联以后 Path 内容进行了更改,则须要使用 setPath 方法从新关联。canvas

参数解析: 第一个参数 path: 被关联的 Path,也就是须要测量的Path; 第二个参数 forceClosed: 是否要闭合Path。 设置为true:则不论Path是否闭合,都会自动闭合该 Path(若是Path能够闭合的话),而后进行测量; 设置为false:则Path保持原来的样子,进行测量;数组

值得注意的两个小点:(敲黑板了!!!)
一、不论 forceClosed 设置为什么种状态(true 或者 false),都不会影响原有Path的状态,即 Path 与 PathMeasure 关联以后,以前的Path 不会有任何改变
二、forceClosed 的设置状态可能会影响测量结果,若是 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,具体请看下面的例子。微信

举个栗子🌰ide

完整代码请看这里,传送门函数

代码主要画了以下图的路径,而后对使用PathMeasure与该path进行关联,一个对forceClosed设置为true,一个为false,而后进行日志打印。 post

能够清楚的设置为true的路径长度为800(五段折线加起来是600,再加上头尾相连的长度200,正好是800),而为false的长度为600(正好是五段折线加起来是600) 若是你的Path已是闭合的(即头尾相连的),则此时forceClosed设置为true或false,其长度结果是同样的。

Path mPath;
Paint mPaint;
int width;
int height;
boolean isInit = false;
PathMeasure closePathMeasure;
PathMeasure noClosePathMeasure;

@Override
protected void init(Context context) {
    mPath = new Path();
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(ContextCompat.getColor(context, R.color.color_blue));
    mPaint.setStyle(Paint.Style.STROKE);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (!isInit) {
        isInit = true;
        width = getMeasuredWidth() / 2;
        height = getMeasuredHeight() / 2;
        
        mPath.lineTo(0, 100);
        mPath.lineTo(100, 100);
        mPath.lineTo(100, -100);
        mPath.lineTo(200, -100);
        mPath.lineTo(200, 0);

        closePathMeasure = new PathMeasure(mPath, true);
        float closeLength = closePathMeasure.getLength();

        noClosePathMeasure = new PathMeasure(mPath, false);
        float noCloseLength = noClosePathMeasure.getLength();
        
        Log.i(TAG, "[closeLength:" + closeLength +
                "; noCloseLength:" + noCloseLength + "]");
    }
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(width, height);
    canvas.drawPath(mPath, mPaint);
}
复制代码

日志输出:

二、共有方法

(1)setPath

public void setPath(Path path, boolean forceClosed) 复制代码

方法描述: 关联一个Path,该方法的做用是:当路径Path变更后,PathMeasure须要从新关联,不然从PathMeasure获得的数据仍是以前关联的Path数据,而并不是新的Path数据。

参数解析: 第一个参数 path: 被关联的 Path,也就是须要测量的Path; 第二个参数 forceClosed: 是否要闭合Path。 设置为true:则不论Path是否闭合,都会自动闭合该 Path(若是Path能够闭合的话),而后进行测量; 设置为false:则Path保持原来的样子,进行测量;

值得注意的两个小点:(此处和构造方法PathMeasure(Path path, boolean forceClosed)的描述是同样)
一、不论 forceClosed 设置为什么种状态(true 或者 false),都不会影响原有Path的状态,即 Path 与 PathMeasure 关联以后,以前的Path 不会有任何改变
二、forceClosed 的设置状态可能会影响测量结果,若是 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,具体可看PathMeasure(Path path, boolean forceClosed)方法讲解中的例子。

(2)getLength

public float getLength() 复制代码

方法描述: 返回当前关联路径轮廓的总长度,或者若是没有路径,则返回0。

(3)isClosed

public boolean isClosed() 复制代码

方法描述: 测量的路径是否闭合。ture为闭合,false为不闭合。

值得注意 这里的闭合取决于两点: 一、Path 原本就是闭合的,则isClosed返回的就是true。 二、若是 Path 不是闭合的,但在与PathMeasure关联时(经过构造方法关联或是经过setPath关联),将forceClosed设置为true。此时,isClosed返回true。

(4)nextContour

public boolean nextContour() 复制代码

方法描述: 获取在路径中下一个轮廓,若是有下一个轮廓,则返回true,且PathMeasure切至下一个轮廓的数据;若是没有下一个轮廓则返回false。至于怎么才算一个轮廓,且看下面例子:

举个栗子🌰 这段代码主要是画了三次,即moveTo了三次,因此即便在图中看起来是两个正方形,但在PathMeasure中能够得出三段轮廓。每次调用nextContour,都按咱们画的顺序给咱们切换,直至最后一个轮廓在调用nextContour时返回false,则中断循环。

Path mNextContourPath;
PathMeasure nextContourPathMeasure;
int width;
int height;
boolean isInit = false;
Paint mPaint;

@Override
protected void init(Context context) {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(ContextCompat.getColor(context, R.color.color_purple));
    mPaint.setStyle(Paint.Style.STROKE);

    mNextContourPath = new Path();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (!isInit) {
        isInit = true;

        width = getMeasuredWidth() / 2;
        height = getMeasuredHeight() / 2;
    
    	 // 第一个轮廓
        mNextContourPath.moveTo(-100, -100);
        mNextContourPath.lineTo(-100, 100);
        mNextContourPath.lineTo(100, 100);
        mNextContourPath.lineTo(100, -100);
        mNextContourPath.lineTo(-100, -100);

		 // 第二个轮廓
        mNextContourPath.moveTo(-50, -50);
        mNextContourPath.lineTo(-50, 50);
        mNextContourPath.lineTo(50, 50);
        mNextContourPath.lineTo(50, -50);

		 // 第三个轮廓
        mNextContourPath.moveTo(50, -50);
        mNextContourPath.lineTo(-50, -50);

        nextContourPathMeasure = new PathMeasure(mNextContourPath, false);

        int i = 0;
        while (nextContourPathMeasure.nextContour()) {
            ++i;
            Log.i(TAG, "第" + i + "个轮廓的 Length:" + nextContourPathMeasure.getLength());
        }
    }
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(width, height);
    canvas.drawPath(mNextContourPath, mPaint);
}
复制代码

效果图:

日志输出:

(5)getMatrix

public boolean getMatrix(float distance, Matrix matrix, int flags) 复制代码

方法描述: 用于获取关联的Path上距离起始点长度( 即传入的distance,范围0<=distance<=getLength() )的点的坐标和正切值(二者可选,由flags决定)。

返回值: 一、为true时,说明获取成功,数据存进matrix; 二、为false时,说明获取失败,matrix不变更;

参数解析: 第一个参数 distance: 即须要的测量点与当前path起始位置的距离,取值范围:0<=distance<=getLength() ; 第二个参数 matrix: 测量点的矩阵,能够选择包含点的坐标和正切值,所包含的数据由flags决定; 第三个参数 flags: 决定matrix中包含的数据,能够选择的值有:POSITION_MATRIX_FLAG(位置)ANGENT_MATRIX_FLAG(正切) 若是须要两个值时,能够用或“|”将其拼凑后传入,例如:

pathMeasure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
复制代码

知识点拓展:若是对 POSITION_MATRIX_FLAG|ANGENT_MATRIX_FLAG 这种传值不太理解的童鞋能够查看我写的另一篇文章《android位运算简单讲解》

(6)getSegment

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 复制代码

方法描述: 获取关联的path的片断路径,添加至dst路径中(并不是替换,是增长)

返回值: 一、为true时,说明截取成功,添加至dst路径中; 二、为false时,说明截取失败,dst路径不变更;

参数解析: 第一个参数 startD: 截取的路径的起始点距离path起始点的长度,取值范围:0<=startD<stopD<=Path.getLength(); 第二个参数 stopD: 截取的路径的终止点距离path起始点的长度,取值范围:0<=startD<stopD<=Path.getLength(); 第三个参数 dst: 截取的路径保存的地方,此处特别注意截取的路径是添加到dst中,而非替换第四个参数 startWithMoveTo: 截取的片断的第一个点是否保持不变; 设置为true:保持截取的片断不变,添加至dst路径中; 设置为false:会将截取的片断的起始点移至dst路径中的最后一个点,让dst路径保持连续

值得一提 若是你在4.4或更早的版本使用在使用这个函数时,须要先调用一下 mDst.lineTo(0, 0); 这句代码,这是由于硬件加速致使的问题;如不调用,会致使没有任何效果。

举个例子🌰 咱们以屏幕中心为原点,先画一条从 (0,0) 到 (200,200) 的直线,而后从一个顺时针画的圆中截取 0.25 到 0.5 距离的圆弧放置dst中,先将startWithMoveTo设置为true,具体代码以下:

mGetSegmentPathMeasure = new PathMeasure();
// 顺时针画 半径为400px的圆
mPath.addCircle(0, 0, 400, Path.Direction.CW);
mGetSegmentPathMeasure.setPath(mPath, false);

// 画直线
mDst.moveTo(0, 0);
mDst.lineTo(200, 200);

// 截取 0.25 到 0.5 距离的圆弧放置dst中
mGetSegmentPathMeasure.getSegment(mGetSegmentPathMeasure.getLength() * 0.25f,
         mGetSegmentPathMeasure.getLength() * 0.5f,
         mDst,
         true);

canvas.drawPath(mDst, mPaint);
复制代码

代码只是截取主要部门,须要查看完整代码的童鞋,请入传送门

效果图

若是将 startWithMoveTo 参数值改成 false,则效果不一样,代码以下:

mGetSegmentPathMeasure = new PathMeasure();
// 顺时针画 半径为400px的圆
mPath.addCircle(0, 0, 400, Path.Direction.CW);
mGetSegmentPathMeasure.setPath(mPath, false);

// 画直线
mDst.moveTo(0, 0);
mDst.lineTo(200, 200);

// 截取 0.25 到 0.5 距离的圆弧放置dst中
mGetSegmentPathMeasure.getSegment(mGetSegmentPathMeasure.getLength() * 0.25f,
         mGetSegmentPathMeasure.getLength() * 0.5f,
         mDst,
         false);

canvas.drawPath(mDst, mPaint);
复制代码

效果图

从两个效果图,可看出startWithMoveTo参数设置为true和false,会致使dst路径的不一样。为true时,保持 截取的片断路径 的原样将其添加至 dst路径 中;为false时,会将截取的片断的起始点移至dst路径中的最后一个点,让dst路径保持连续。

值得注意 在写这篇博客时,将startWithMoveTo参数设置为false,在两台测试机(Mate10 Android 8.1.0和oppo A57 Android 6.0.1)上运行,效果有些许不一样。 Demo使用的是px做为单位,两台手机的分辨率不一样,因此在 A57 机型上按比例缩小了一倍进行绘制 (即圆半径从400px变为200px,斜线从(0,0)->(200,200)变为(0,0)->(100,100) ),从下面👇的OPPO A57的效果图能够很明显的看出,圆弧的路径已经受到dst中最后一个点的影响,改变了形状。(Mate10的效果图请翻阅上面👆)

OPPO A57的效果图

(6)getPosTan

public boolean getPosTan(float distance, float pos[], float tan[]) 复制代码

方法描述: 获取关联的Path距离起始点长度(distance)的点坐标(pos)余弦(tan[0],即cos)正弦(tan[1],即sin)

返回值 一、为true时,说明获取成功,该点的 坐标 以及 正余弦 将各自存进pos和tan参数 二、为false时,说明获取失败,pos与tan没有变更

参数解析: 第一个参数 distance: 即须要的测量点与当前path起始位置的距离,取值范围:0<=distance<=getLength() ; 第二个参数 pos: 测量点的坐标,pos[0]为x坐标,pos[1]为y坐标; 第三个参数 tan: 测量点的正余弦值,tan[0]为cos,即余弦值或称为单位圆的x坐标;tan[1]为sin,即正弦值或称为单位圆的y坐标

数学小课堂: 单位圆指的是平面直角坐标系上,圆心为原点,半径为1的圆。 cos = 邻边/斜边 = OB/OA = OB(由于OA长度为1)= x sin = 对边/斜边 = AB/OA = AB (由于OA长度为1) = y

在这里插入图片描述

3、实战

转圈的箭头

按照国际惯例,先上效果图

动画解析 让箭头绕着红色圆转圈,同时须要改变箭头的方向,使其朝向当前位置的切线方向

实现思路与代码解析 先进行初始化对象,主要是初始化画笔、图片、路径、PathMeasure、装载变量、估值器,具体为每一个对象设置的属性请看下面代码,此处比较简单,就再也不赘述

// 初始化 画笔 [抗锯齿、不填充、红色、线条2px]
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setColor(Color.RED);
mCirclePaint.setStrokeWidth(2);

// 获取图片
mArrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, null);

// 初始化 圆路径 [圆心(0,0)、半径200px、顺时针画]
mCirclePath = new Path();
mCirclePath.addCircle(0, 0, 200, Path.Direction.CW);

// 初始化 装载 坐标 和 正余弦 的数组
mPos = new float[2];
mTan = new float[2];

// 初始化 PathMeasure 而且关联 圆路径
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mCirclePath, false);

// 初始化矩阵
mMatrix = new Matrix();

// 初始化 估值器 [区间0-一、时长5秒、线性增加、无限次循环]
valueAnimator = ValueAnimator.ofFloat(0, 1f);
valueAnimator.setDuration(5000);
// 匀速增加
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // 第一种作法:经过本身控制,是箭头在原来的位置继续运行
        mCurrentValue += DELAY;
        if (mCurrentValue >= 1) {
            mCurrentValue -= 1;
        }

        // 第二种作法:直接获取能够经过估值器,改变其变更规律
		//mCurrentValue = (float) animation.getAnimatedValue();

        invalidate();
    }
});
复制代码

初始化工做完成后,接下来就是进行绘制工做,咱们按照步骤来说解: 第一步,将屏幕的中心点做为原点,方便操做和绘制

// 移至canvas中间
canvas.translate(mWidth / 2, mHeight / 2);
复制代码

第二步,绘制圆,即箭头走的轨迹,PathMeasure所关联的Path就是此处的mCirclePath,在上面的初始化代码能够清晰的看到

// 画圆路径
canvas.drawPath(mCirclePath, mCirclePaint);
复制代码

第三步,获取当前点的坐标以及正余弦的值,存放至mPos和mTan变量中

// 测量 pos(坐标) 和 tan(正切)
mPathMeasure.getPosTan(mPathMeasure.getLength() * mCurrentValue, mPos, mTan);
复制代码

第四步,经过反正弦atan2计算出角度(单位为弧度),因此须要进行将单位在转为度。

// 计算角度
float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
复制代码

数学小课堂 咱们来拆分下这个公式,先看Math.atan2(mTan[1], mTan[0]) 这段,这里关系到的是直角坐标系与极坐标系的转换,因此咱们先来重拾下忘记的第一个知识点 (1)直角坐标系与极坐标系的转换(图片是本身手写的,字迹粗糙勿喷😄)

从图中能够知道 θ 的计算是经过该点的x和y坐标得出,而且还要根据 y/x 计算结果的符号和该点存在的象限来共同决定。而经过getPosTan方法得到的 tan[]中的值即可以看做该点的x、y坐标值(具体缘由能够查看前面getPosTan方法中的数学小课堂)。

因此只需对(y/x)进行求反正切即可,但这里有存在一个问题,也就是咱们刚刚提到的 θ 由 y/x 计算结果的符号和该点存在的象限决定,若是使用 Math.atan(double a) 方法进行求反正切,其结果范围为开区间的 (-pi/2,pi/2),然而一个圆的的范围是(-pi,pi),这显然直接使用是不能知足的。幸亏Math类提供了一个让咱们省事的API atan2(double y, double x),其返回值的范围正是 (-pi,pi)

到这里已经能经过atan2函数获得该点的角度,可是其单位是弧度,并不能在直角坐标系中直接拿来使用,须要进行转换。因此咱们须要引出第二个被遗忘的知识点

(2)弧度制 弧度制是什么这里就不作过多解释。这里涉及到一个公式就是 1° = π/180 rad ,看到这里你们应该就明白为何要 乘以 180 / Math.PI,由于求出的反正切的值单位为弧度,须要转为咱们一般使用的角度制中的度。

第五步,重置矩阵,避免矩阵内有以前遗留的操做。

// 重置矩
mMatrix.reset();
复制代码

第六步,根据第四步计算得出的角度而且以图片的中心点进行旋转

// 设置旋转角度
mMatrix.postRotate(degree, mArrowBitmap.getWidth() / 2, mArrowBitmap.getHeight() / 2);
复制代码

第七部,进行偏移,由于直接绘制的话,箭头会在轨道以外,须要挪动箭头的宽和高各一半

// 设置偏移量
mMatrix.postTranslate(mPos[0] - mArrowBitmap.getWidth() / 2,
        mPos[1] - mArrowBitmap.getHeight() / 2);
复制代码

第八步,使用矩阵将箭头绘制至画布中

// 画箭头,使用矩阵旋转
canvas.drawBitmap(mArrowBitmap, mMatrix, mCirclePaint);
复制代码

至此,效果已完成。

须要查看完整代码的童鞋,请进传送门

4、更多案例

一、拖动的loading线条

效果图

代码传送门 完整代码请进

二、乘风破浪的小船

效果图

代码传送门 完整代码请进

5、写在最后

PathMeasure能够说是自定义UI的利器之一,熟练的掌握能让咱们斩获更多的产品😈。若是各位童鞋在阅读中发现有错误或是晦涩难懂的地方请与我联系,我会及时修改,让咱们共同进步。一样若是你喜欢的话,请给个赞并关注我吧😄。

高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄

若是须要更多的交流与探讨,能够经过如下微信二维码加小盆友好友。

相关文章
相关标签/搜索