目录
1、前言
2、API讲解
3、实战
4、更多案例
5、写在最后
java
2019年了,然而2017计划写的东西还没开始😂,此次的拖延症来的比日常早却去的比日常晚。今天进行分享的是UI中的PathMeasure,同时记录本身在使用过程当中的几个疑惑点。话很少说,开始进入正题。android
这一小节主要是对PathMeasure的构造方法和公有方法进行讲解git
public PathMeasure() 复制代码
方法描述: 建立一个空的PathMeasure,可是使用以前须要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经建立好的,若是关联以后 Path 内容进行了更改,则须要使用 setPath 方法从新关联。github
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
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);
}
复制代码
日志输出:
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)方法讲解中的例子。
public float getLength() 复制代码
方法描述: 返回当前关联路径轮廓的总长度,或者若是没有路径,则返回0。
public boolean isClosed() 复制代码
方法描述: 测量的路径是否闭合。ture为闭合,false为不闭合。
值得注意 这里的闭合取决于两点: 一、Path 原本就是闭合的,则isClosed返回的就是true。 二、若是 Path 不是闭合的,但在与PathMeasure关联时(经过构造方法关联或是经过setPath关联),将forceClosed设置为true。此时,isClosed返回true。
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);
}
复制代码
效果图:
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位运算简单讲解》
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的效果图
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
按照国际惯例,先上效果图
动画解析 让箭头绕着红色圆转圈,同时须要改变箭头的方向,使其朝向当前位置的切线方向
实现思路与代码解析 先进行初始化对象,主要是初始化画笔、图片、路径、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)直角坐标系与极坐标系的转换(图片是本身手写的,字迹粗糙勿喷😄)
因此只需对(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);
复制代码
至此,效果已完成。
须要查看完整代码的童鞋,请进传送门
效果图
代码传送门 完整代码请进
效果图
代码传送门 完整代码请进
PathMeasure能够说是自定义UI的利器之一,熟练的掌握能让咱们斩获更多的产品😈。若是各位童鞋在阅读中发现有错误或是晦涩难懂的地方请与我联系,我会及时修改,让咱们共同进步。一样若是你喜欢的话,请给个赞并关注我吧😄。
高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄
若是须要更多的交流与探讨,能够经过如下微信二维码加小盆友好友。