PathMeasure
大部分内容来自《Android自定义控件开发入门与实战》一书
PathMeasure
可以计算指定path的一些信息,比如路径总长、指定长度所对应的坐标点等
创建方式:
public PathMeasure()
public PathMeasure(Path path, boolean forceClosed)
forceClosed
-forceClosed
参数对绑定path不会产生任何影响,如果一个折线没有闭合,当forceClosed
为true
时,PathMeasure
计算的path是闭合的,但path本身绘制出来的是不会闭合的。forceClosed
参数只对PathMeasure
的测量结果有影响。如果一个折线,本身没有闭合,当forceClosed
为true
时,PathMeasure
的计算就会包含最后一段闭合的路径,与原来的path不同
一些方法
1.getLength()
Return the total length of the current contour, or 0 if no path is associated with this measure object.
获取计算的路径的长度
如下的例子,当设置forceClosed
为不同的值时,可以看出区别
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.translate(50, 50);
Path path = new Path();
path.moveTo(0,0);
path.lineTo(0, 100);
path.lineTo(100, 100);
path.lineTo(100, 0);
PathMeasure measure1 = new PathMeasure(path, false);
PathMeasure measure2 = new PathMeasure(path, true);
Log.e(TAG, "forceClosed = false--->" + measure1.getLength());
Log.e(TAG, "forceClosed = true--->" + measure2.getLength());
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
}
Log输出如下:
2021-07-14 14:15:05.530 23640-23640/com.example.pathmeasuredemo E/PaintView: forceClosed = false--->300.0
2021-07-14 14:15:05.530 23640-23640/com.example.pathmeasuredemo E/PaintView: forceClosed = true--->400.0
2.isClosed()
public boolean isClosed()
用于判断测量path时是否计算闭合。如果关联path的时候设置
forceClosed
为true
,则这个函数的返回值一定为true
3.nextContour()
public boolean nextContour()
path可以由多条曲线组成,但不论
getLength()
、getSegment()
还是其他函数,都只会针对其中第一条线段进行计算。
nextContour()
就是用于跳转到下一条曲线的函数。如果跳转成功,则返回true,否则返回false
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(8f);
paint.setStyle(Paint.Style.STROKE);
canvas.translate(150, 150);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
path.addRect(-120, -120, 120, 120, Path.Direction.CCW);
canvas.drawPath(path, paint);
PathMeasure measure = new PathMeasure(path, false);
do {
float len = measure.getLength();
Log.e(TAG, "len = " + len);
} while (measure.nextContour());
Log输出如下:
2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 400.0
2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 800.0
2021-07-14 16:54:59.315 28689-28689/com.example.pathmeasuredemo E/PaintView: len = 960.0
可见
getLength()
获取的是当前的曲线,而不是整个Path
4.getSegment()
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
startD
- 开始截取位置距离Path起始点的长度stopD
- 结束截取位置距离Path起始点的长度dst
- 截取的Path将会被添加到dst中startWithMoveTo
- 起始点是否使用moveTo
将路径的新起始点移到结果Path的起始点。通常设置为true
。
getSegment()
可以截取整个Path
中的某个片段
如下的例子,有个矩形
截取其中的0-150这段路径,截取的路径线段将添加到dst
路径中,并将dst
路径绘制出来
public class GetSegmentView extends View {
private Paint mPaint;
public GetSegmentView(Context context) {
super(context);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
public GetSegmentView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//禁用硬件加速功能
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.translate(100, 100);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, mPaint);
}
}
这里是顺时针绘制的,原因是原来路径是
Path.Direction.CW
顺时针方向
修改为Path.Direction.CCW
后的效果
如果dst路径不为空,即原来就有存在的路径,是什么效果呢?
如下dst原来就有一段从(0,0)
到(10,100)
的线段
canvas.translate(100, 100);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(10, 100);
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, mPaint);
可见,
getSegment
会将截取的Path片段添加到路径dst中,而不是替换dst中的内容
如果startWithMoveTo
为false
,会是什么样呢?
measure.getSegment(0, 150, dst, false);
如果
startWithMoveTo
为false
,则会将截取出来的Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性
例子,使用PathMeasure
实现路劲加载动画,如下的自定义view,GetSegmentView
public class GetSegmentView extends View {
private Paint mPaint;
private Path mDstPath;
private Path mCirclePath;
private PathMeasure mPathMeasure;
private Float mCurAnimValue = 0.0f;
public GetSegmentView(Context context) {
super(context);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
public GetSegmentView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//禁用硬件加速功能
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
mCirclePath = new Path();
mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
mPathMeasure = new PathMeasure(mCirclePath, true);
mDstPath = new Path();
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.WHITE);
float stop = mPathMeasure.getLength() * mCurAnimValue;
mDstPath.reset(); //清空之前生成的路径
mPathMeasure.getSegment(0, stop, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
}
}
修改下getSegment
方法中的startD
和stopD
,可以实现另一种效果
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.WHITE);
float length = mPathMeasure.getLength();
float stop = length * mCurAnimValue;
/**
* 在进度小于0.5时,start=0;而在进度大于0.5时,start=2*mCurAnimValue - 1;
*/
float start = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));
mDstPath.reset(); //清空之前生成的路径
mPathMeasure.getSegment(start, stop, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
}
5.getPosTan()
public boolean getPosTan(float distance, float pos[], float tan[])
用于得到路径上某一长度的位置以及该位置的正切值
- distance - 距离path起始点的长度,取值范围为
[0, getLength]
- pos[] - 该点的坐标值。pos[0]表示x坐标,pos[1]表示y坐标
- tan[] - 该点的正切值。代表单位圆的坐标点
(x,y)
,通过y/x
可以得到对应点的正切值
在上面圆圈加载路径动画的基础上,实现一个带箭头的加载动画,如下的GetPosTanView
public class GetPosTanView extends View {
private Paint mPaint;
private Path mDstPath;
private Path mCirclePath;
private PathMeasure mPathMeasure;
private Float mCurAnimValue = 0.0f;
private Bitmap mArrowBitmap;
private float[] pos = new float[2];
private float[] tan = new float[2];
public GetPosTanView(Context context) {
super(context);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
public GetPosTanView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//禁用硬件加速功能
setLayerType(LAYER_TYPE_SOFTWARE, null);
//箭头图片
mArrowBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.arraw);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
mCirclePath = new Path();
mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
mPathMeasure = new PathMeasure(mCirclePath, true);
mDstPath = new Path();
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(2000);
animator.start();
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.WHITE);
float length = mPathMeasure.getLength();
float stop = length * mCurAnimValue;
/**
* 在进度小于0.5时,start=0;而在进度大于0.5时,start=2*mCurAnimValue - 1;
*/
float start = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));
mDstPath.reset(); //清空之前生成的路径
mPathMeasure.getSegment(start, stop, mDstPath, true);
canvas.drawPath(mDstPath, mPaint);
//旋转箭头图片,并绘制
mPathMeasure.getPosTan(stop, pos, tan);
//取得弧度值
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);
Matrix matrix = new Matrix();
//围绕中心点旋转指定的角度
matrix.postRotate(degrees, mArrowBitmap.getWidth() / 2, mArrowBitmap.getHeight() / 2);
// 将图片从(0,0)移动到当前路径的最前端
matrix.postTranslate(pos[0],pos[1]);
//matrix.postTranslate(pos[0] - mArrowBitmap.getWidth() / 2, pos[1] - mArrowBitmap.getHeight() / 2);
canvas.drawBitmap(mArrowBitmap, matrix, mPaint);
}
}
可以看到箭头虽然随着路径移动,但是有点偏差。原因在于,移动箭头图片时,是以图片左上角为起始点开始移动的,修改为如下的形式:
// 将图片从(0,0)移动到当前路径的最前端
//matrix.postTranslate(pos[0],pos[1]);
matrix.postTranslate(pos[0] - mArrowBitmap.getWidth() / 2, pos[1] - mArrowBitmap.getHeight() / 2);
例子
1.支付成功的动画
public class AliPayView extends View {
private Path mCirclePath, mDstPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private Float mCurAnimValue;
private int mCentX = 100;
private int mCentY = 100;
private int mRadius = 50;
public AliPayView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.BLACK);
mDstPath = new Path();
mCirclePath = new Path();
//圆圈
mCirclePath.addCircle(mCentX, mCentY, mRadius, Path.Direction.CW);
//对勾
mCirclePath.moveTo(mCentX - mRadius / 2, mCentY);
mCirclePath.lineTo(mCentX, mCentY + mRadius / 2);
mCirclePath.lineTo(mCentX + mRadius / 2, mCentY - mRadius / 3);
mPathMeasure = new PathMeasure(mCirclePath, false);
/**
* 这里有2条路径,在0-1之间画第一条路径,在1-2之间画第二条路径
*/
ValueAnimator animator = ValueAnimator.ofFloat(0, 2);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
mCurAnimValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(4000);
animator.start();
}
boolean mNext = false;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
if (mCurAnimValue < 1) {
//画外圈的圆形
float stop = mPathMeasure.getLength() * mCurAnimValue;
mPathMeasure.getSegment(0, stop, mDstPath, true);
} else {
/**
* 画对勾
* 不要用mCurAnimValue == 1来判断圆是否已经画完,根据设置Interpolater的不同,中间值不一定输出1
*/
if (!mNext) {
mNext = true;
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
mPathMeasure.nextContour();
}
float stop = mPathMeasure.getLength() * (mCurAnimValue - 1);
mPathMeasure.getSegment(0, stop, mDstPath, true);
}
canvas.drawPath(mDstPath, mPaint);
}
}