学习资料:
- 徐医生的PathMeasure之迷径追踪
- GcsSloop同学的安卓自定义View进阶-PathMeasure
徐医生,《Android群英传》的作者,不用多说
GcsSloop同学,今年大四,一个超级厉害的同学,个人博客超级棒
1. PathMeasure
在Android 自定义View学习(九)——Bezier贝塞尔曲线学习中学习到了使用De Casteljau 德卡斯特里奥算法
利用贝塞尔曲线的起始点,控制点,终点
来帮助计算曲线上任意点的坐标。在其他的Path
路径中,系统提供了一个封装好的PathMeasure
来帮助辅助测量
顾名思义,可以理解为用来辅助计算Path
的计算器,PathMeasure
的public
方法不多,一共也就7个方法
1.1 初始化,构造方法
PathMeasure
构造方法有两个,一个无参,一个有参
1. public PathMeasure(){}
2. public PathMeasure(Path path, boolean forceClosed){}
使用构造方法1
得到一个mPathMeasure
对象后,mPathMeasure.setPath(Path path, boolean forceClosed)
与Path
关联,setPath()
方法中,也需要一个boolean forceClosed
值
- boolean forceClosed
代表测量计算时是否闭合,不关乎Path
绘制,ture
闭合,false
不闭合。forceCloseed
不会对Path
有任何影响,只是对PathMeasure
测量时候有影响。
private void init() {
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setStrokeWidth(10f);
mPaint.setStyle(Paint.Style.STROKE);
//Path
mPath = new Path();
mPath.moveTo(100f,0f);
mPath.lineTo(100f,100f);
mPath.lineTo(200f,100f);
mPath.lineTo(200f,0f);
//PathMeasure
mPathMeasure = new PathMeasure(mPath,true);
Log.e("length","&&&&"+mPathMeasure.getLength());
}
- ture , 400
- false ,300
getLength()
,就是获得测量计算的长度
但无论true
还是false
,绘制都一样
无论是通过setPath()
方法还是通过构造方法2
与mPath
关联,mPath
都必须是之前创建好的。关联之后的mPath
发生变化时,需要再次调用setPath()
对改变后的mPath
再次进行关联
PathMeasure
是否闭合可以用isClosed()
方法的返回值进行判断
1.2 getSegment()截取片段
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
可以用来截取整个Path
的某一个片段
图截取自GcsSloop同学的安卓自定义View进阶-PathMeasure
boolean startWithMoveTo
通常设置为true
;
设置为false
时,一般是和dst
一起使用。由于截取出来的片段是添加到dst
中并不是代替,所以设置为false
时是将截取出来的Path
的起点,移动到dst
的终点,保证dst
中的片段的连续性
感觉文字比较难理解,看代码比较明显
这个方法有个bug
,需要考虑硬件加速问题,上面的图片最后给出了解决方案
测试使用:
public class PathLoadingView extends View {
private Path mPath;
private Paint mPaint, defaultPaint;
private PathMeasure mPathMeasure;
private Path dst;
public PathLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
//默认画笔 绘制辅助圆用
defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
defaultPaint.setColor(Color.CYAN);
defaultPaint.setStrokeWidth(10f);
defaultPaint.setStyle(Paint.Style.STROKE);
//截取画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setStrokeWidth(10f);
mPaint.setStyle(Paint.Style.STROKE);
//Path
mPath = new Path();
mPath.addCircle(300f, 300f, 100f, Path.Direction.CW);//加入一个半径为100圆
//PathMeasure
mPathMeasure = new PathMeasure(mPath, false);
// Path dst 用来存储截取的Path片段
dst = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
dst.reset();
//避免硬件加速的Bug
dst.lineTo(0, 0);
//截取圆的1/4
final float stopP = (float) (Math.PI * 2 * 100 / 4);
mPathMeasure.getSegment(0, stopP, dst, true);
canvas.drawPath(mPath,defaultPaint);//绘制mPath辅助圆
canvas.drawPath(dst, mPaint);//绘制截取的片段
}
}
布局文件
红色就是截取的片段
上面的dst
一开始是没有值的,下面给dst
加入值
修改代码:
//避免硬件加速的Bug
dst.lineTo(0, 0);
dst.lineTo(300,300);
//截取圆的1/4
也就加入dst.lineTo(300,300)
,就是从控件的起点和圆心连接起来
此时mPathMeasure.getSegment(0, stopP, dst, true)
,startWithMoveTo
值为true
,截取的片段的起点并没有改变,将startWithMoveTo
设为false
此时,截取的片段就和
dst
连接了起来,并且截取的片段形态也发生了改变
利用这个方法可以做出一个类似Material Design
风格的圆形进度条
public class PathLoadingView extends View {
private Path mPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private Path dst;
private float mLength;
private float mAnimatorValue;
public PathLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setStrokeWidth(10f);
mPaint.setStyle(Paint.Style.STROKE);
//Path
mPath = new Path();
mPath.addCircle(300f, 300f, 100f, Path.Direction.CW);//加入一个半径为100圆
//PathMeasure
mPathMeasure = new PathMeasure(mPath, false);
mLength = mPathMeasure.getLength();//此时为圆的周长
// Path dst 用来存储截取的Path片段
dst = new Path();
//属性动画
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
//设置动画过程的监听
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimatorValue = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
dst.reset();
//避免硬件加速的Bug
dst.lineTo(0, 0);
//截取片段
float stop = mLength * mAnimatorValue;
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, dst, true);
canvas.drawPath(dst, mPaint);//绘制截取的片段
}
}
代码最关键的地方就是利用属性动画得到的mAnimatorValue
值计算开始和结束截取点
1.3 getPosTan() 获取一点坐标及点的正切值
-
boolean getPosTan(float distance, float pos[], float tan[])
可以获取路径上一个点的坐标以及该点的正切值
代码:
public class PathLoadingView extends View {
private Path mPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private float mAnimatorValue;
private float[] pos;
private float[] tan;
private float mLength;
public PathLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
pos = new float[2];//点的坐标
tan = new float[2];//直角三角形两个的直角边
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setStrokeWidth(10f);
mPaint.setStyle(Paint.Style.STROKE);
//Path
mPath = new Path();
mPath.addCircle(0f, 0f, 200f, Path.Direction.CW);//加入一个半径为100圆
//PathMeasure
mPathMeasure = new PathMeasure(mPath, false);
mLength = mPathMeasure.getLength();
//属性动画
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
//设置动画过程的监听
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimatorValue = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取在动画某一个时刻点的坐标及正切值
mPathMeasure.getPosTan(mLength * mAnimatorValue,pos,tan);
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
Log.e("degrees","&&&"+degrees+"--->"+Math.atan2(tan[1], tan[0])+"--->tan[1]= "+tan[1]+"---tan[0]= "+tan[0]+"---pos[0] ="+pos[0]+"---pos[1] ="+pos[1]);
canvas.save();
canvas.translate(getWidth()/2, getHeight()/2);//将坐标系移动到控件的中心位置
canvas.drawPath(mPath, mPaint);
canvas.drawCircle(pos[0], pos[1], 10, mPaint);//在路径的点上绘制一个小圆
canvas.rotate(degrees);//将画布旋转 此时坐标系也跟着旋转
canvas.drawLine(0, -200, 100, -200, mPaint);//绘制一段长度为100的正切线 200是圆的半径
canvas.restore();
}
}
运行后效果
这段代码的效果看起来是切线在圆上滑动,实际是画布旋转的效果,切线是同一条,根据动画的时间,计算出对应旋转的角度,将画布进行旋转
getPosTan(mLength * mAnimatorValue,pos,tan)
会将拿到的坐标及正切值存入pos,tan
两个数组中
-
pos[0]
,就是点x
轴坐标 -
pos[1]
,就是点y
轴坐标
tan
值不好理解,值是取自半径为1
的单位圆上的坐标
-
tan[0]
,单位圆上点x
轴坐标,其实就是角对边的边长 -
tan[1]
,单位圆上点y
轴坐标,邻边的边长
图从GcsSloop同学博客盗来的,源自维基百科
double radian = Math.atan2(double y ,double x);
-
y
,y
轴值 -
x
,x
轴值
注意X,Y值顺序
得到的结果radian
并不是角度,而是是弧度,取值范围(-π,π)
,弧度转角度公式:
角度 = 弧度 * 180 / π
得到角度后,就可以根据需要进行操作
1.4 getMatrix() 得到点位置及正切值矩阵
getMatrix(float distance, Matrix matrix, int flags)
- distance,距离起点的距离
- matrix,用来位置或者正切值的矩阵
- flags,矩阵的类型,有两种,PathMeasure.TANGENT_MATRIX_FLAG正切,PathMeasure.TANGENT_MATRIX_FLAG位置
代码:
public class PathLoadingView extends View {
private Path mPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private float mAnimatorValue;
private float mLength;
private Matrix mMatrix;
private Bitmap mBitmap;
public PathLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fly);
//矩阵
mMatrix = new Matrix();
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setStrokeWidth(10f);
mPaint.setStyle(Paint.Style.STROKE);
//Path
mPath = new Path();
mPath.addCircle(0f, 0f, 200f, Path.Direction.CW);//加入一个半径为100圆
//PathMeasure
mPathMeasure = new PathMeasure(mPath, false);
mLength = mPathMeasure.getLength();
//属性动画
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
//设置动画过程的监听
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimatorValue = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//得到矩阵
mPathMeasure.getMatrix(mLength * mAnimatorValue, mMatrix, PathMeasure.POSITION_MATRIX_FLAG);
canvas.translate(getWidth() / 2, getHeight() / 2);//将坐标系移动到控件的中心位置
canvas.drawPath(mPath, mPaint);
//绘制小三角形
canvas.drawBitmap(mBitmap, mMatrix, null);
}
}
运行后效果
因为使用canvas.translate()
将坐标系进行了调整,圆心处其实就是坐标系原点(0,0)
,此时小飞机有两个问题
- 朝向并不是正切线方向
- 小飞机自身中心不在圆上
此时代码并没有使用正切矩阵
修改代码,加入正切矩阵:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//得到矩阵正切和位置矩阵
mPathMeasure.getMatrix(mLength * mAnimatorValue, mMatrix, PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG );
canvas.translate(getWidth() / 2, getHeight() / 2);//将坐标系移动到控件的中心位置
canvas.drawPath(mPath, mPaint);
//绘制小三角形
mMatrix.preRotate(270);//调整朝向,朝着正切线的方向
canvas.drawBitmap(mBitmap, mMatrix, null);
}
**注意:对Matrix的操作应该放在getMatrix()之后,getMatrix()会将之前的操作重置掉 **
- 先加上正切矩阵
PathMeasure.TANGENT_MATRIX_FLAG
,但由于正切矩阵的影响,小飞机的角度需要调整 - 然后,再
mMatrix.preRotate(270)
,这里旋转的角度需要根据自己的图片来修改
第一个问题解决后,第二个问题也就好解决了,只需要利用前乘平移,将小飞机的中心朝左上方移动,移动到圆上就好了
//绘制小三角形
mMatrix.preRotate(270);//调整朝向,朝着正切线的方向
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);//将小飞机移动到圆上
canvas.drawBitmap(mBitmap, mMatrix, null);
最终效果
PathMeasure
的方法差不多学习完了
2. 最后
PathMeasure
和Path
在自定义View
使用的比较多,需要再多学习。
本篇的学习主要就是抄袭徐医生和GcsSloop同学的博客 :)
共勉 :)