学习资料:
- 徐医生的PathMeasure之迷径追踪
- GcsSloop同学的安卓自定义View进阶-PathMeasure
徐医生,《Android群英传》的作者,不用多说
GcsSloop同学,今年大四,一个超级厉害的同学,个人博客超级棒
1. PathMeasure <p>
在Android 自定义View学习(九)——Bezier贝塞尔曲线学习中学习到了使用De Casteljau 德卡斯特里奥算法
利用贝塞尔曲线的起始点,控制点,终点
来帮助计算曲线上任意点的坐标。在其他的Path
路径中,系统提供了一个封装好的PathMeasure
来帮助辅助测量
顾名思义,可以理解为用来辅助计算Path
的计算器,PathMeasure
的public
方法不多,一共也就7个方法
1.1 初始化,构造方法 <p>
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()截取片段 <p>
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);//绘制截取的片段
}
}
布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_path_measure"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.szlk.customview.custom.PathLoadingView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true" />
</RelativeLayout>
截取四分之一圆
红色就是截取的片段
上面的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);//绘制截取的片段
}
}
PathLoadingView
代码最关键的地方就是利用属性动画得到的mAnimatorValue
值计算开始和结束截取点
1.3 getPosTan() 获取一点坐标及点的正切值 <p>
-
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() 得到点位置及正切值矩阵 <p>
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. 最后 <p>
PathMeasure
和Path
在自定义View
使用的比较多,需要再多学习。
本篇的学习主要就是抄袭徐医生和GcsSloop同学的博客 :)
共勉 :)
网友评论