我们平时自定义View的时候,经常会绘制path,想要求出path路径上的某个点的位置,很难通过一般的数学函数来计算,幸好系统给我们提供了一个PathMeasure可以用来测量一个path的路径,我们可以根据测量得到的值来绘制一些别的效果。
PathMeasure有两个构造方法
public PathMeasure() {
mPath = null;
native_instance = native_create(0, false);
}
public PathMeasure(Path path, boolean forceClosed) {
mPath = path;
native_instance = native_create(path != null ? path.readOnlyNI() : 0,forceClosed);
}
一个是有参数的,一个是没参数的
- 有参数的构造方法中第一个很好理解就是需要测量的path,第二个参数的意思是是否计算path路径闭合的路径。如果是true,那么不管我们path是不是闭合了,它都会去计算闭合的路径。如果是false就不会计算。
- 没有参数创建出来之后,可以通过
pathMeasure.setPath(mPath,false);
方法把path和forceClosed这两个参数传进去
下面来看PathMeasure中的几个比较常用的方法
getSegment
给定一个起始点,一个结束点和一个空path对象,把给定的起点到终点的路径赋值给这个空的path。public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
。最后一个参数startWithMoveTo表示起点是否使用moveTo,用来保证截取的path第一个点的位置不变。从而保证截取的片段不会变形
它返回的是一个boolean值,如果截取到的路径长度是0就返回false,不是0就返回true。
下面来看一下使用getSegment的简单案例,在onDraw方法中
mPath.reset();
mFloat += 0.01;
if (mFloat >= 1){
mFloat = 0;
}
mPath.addCircle(getWidth()/2,getHeight()/2,200,Path.Direction.CW);
mDst.reset();
pathMeasure.setPath(mPath,false);
float distance = pathMeasure.getLength() * mFloat;
pathMeasure.getSegment(0, distance , mDst, true);
canvas.drawPath(mDst, mPaint);
invalidate();
使用一个0-1的成员变量mFloat来控制当前截取比例,每次调用invalidate的时候mFloat的值都会改变,这样就可以弄个简单的动画效果 效果如下
上面一直是从0来时截取如果我们改一下截取的位置
pathMeasure.getSegment(2*distance/3, distance, mDst, true);
第一个参数改成了2*distance/3
,效果如下,类似一个加载动画的效果,这个参数可以随便改,更改的不同截取效果就不同
前面两个我们只绘制了我们截取的path,下面把原path也绘制上,截取的部分换一种颜色绘制,效果如下
getPosTan
getPosTan方法,可以获得path路径上某个点的位置和这个点的切线的值。public boolean getPosTan(float distance, float pos[], float tan[])
distance是当前需要截取的长度,pos是一个数组,如果不为空,可以给这个数组赋值,pos[0]是x坐标pos[1]是y坐标,tan跟也是个数组,tan[0],tan[1]代表切线的值,通过Math.atan2()方法传入tan[0],tan[1]可以获取到当前切线的角度。
下面看一个简单的例子
mPath.reset();
mFloat += 0.01;
if (mFloat >= 1){
mFloat = 0;
}
mPath.addCircle(getWidth()/2,getHeight()/2,200,Path.Direction.CW);
pathMeasure.setPath(mPath,false);
//用来记录位置
float []pos = new float[2];
//用来记录切点的位置
float []tan = new float[2];
float distance = pathMeasure.getLength() * mFloat;
pathMeasure.getPosTan(distance,pos,tan);
//计算出当前图片要旋转的角度
float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);
mMatrix.reset();
//设置旋转角度和旋转中心
mMatrix.postRotate(degree,mBitmap.getWidth() / 2,mBitmap.getHeight() / 2);
//设置绘制的中心点与当前图片中心点重合
mMatrix.postTranslate(pos[0]-mBitmap.getWidth() / 2,pos[1]-mBitmap.getHeight()/2);
canvas.drawPath(mPath, mPaint);
canvas.drawBitmap(mBitmap,mMatrix, mPaint);
效果如下:
通过getPosTan方法,获取到当前的角度,通过Matrix方法把一个图片旋转这个角度,然后绘制到圆圈上
除了使用getPosTan之外,还有一个稍微简单的方法可以获取到pos和tan的值,通过pathMeasure.getMatrix
方法
public boolean getMatrix(float distance, Matrix matrix, int flags)
前两个值好理解,最后一个值flag,用来指定矩阵中需要返回那些参数,有两个值
public static final int POSITION_MATRIX_FLAG = 0x01;
public static final int TANGENT_MATRIX_FLAG = 0x02;
这两个值分别代表前面的pos和tan,例如下面的代码
mPath.reset();
mFloat += 0.01;
if (mFloat >= 1){
mFloat = 0;
}
//路径
mPath.lineTo(0, 200);
mPath.lineTo(300, 200);
mPath.quadTo(450,100,600,200);
mPath.lineTo(900, 200);
pathMeasure.setPath(mPath,false);
//将pos信息和tan信息保存在mMatrix中
pathMeasure.getMatrix(pathMeasure.getLength() * mFloat, mMatrix,
PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
//将图片的旋转坐标调整到图片中心位置
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
canvas.drawPath(mPath, mPaint);
canvas.drawBitmap(mBitmap,mMatrix, mPaint);
效果:
下面使用pathmeasure实现一个支付宝的支付动画
效果如下:
主要用到getSegment这个方法
public class PathMeasurePay extends View {
private Paint mPaint = new Paint();
private Path mPath = new Path();
private Path mCircleDst = new Path();
private Path mLineDst1 = new Path();
private Path mLineDst2 = new Path();
private Path mLineDst = new Path();
private PathMeasure pathMeasure;
private int mCircleRadius;
/**
* 圆的中心点
*/
private float circleX,circleY;
/**
* 绘制类型 1是对号 2是叉号
*/
private int mType;
/**
* 动画控制的圆取值范围0-1
*/
private float mCircleValue;
/**
* 动画控制的直线取值范围0-1
* mType==1的时候代表对号的线
* mType==2的时候代表叉号的第一条线
*/
private float mLineValue;
/**
* 动画控制的叉号第二条线的取值范围0-1
*/
private float mLineValue2;
/**
* mType==1的时候代表是否绘制对号
* mType==2的时候代表是否绘制叉号的第一条线
*/
private boolean isDrawLine = false;
/**
* 是否绘制叉号的第二条线
*/
private boolean isDrawLine2 = false;
private ValueAnimator animatorCircle;
private ValueAnimator animatorLine;
private ValueAnimator animatorLine2;
private int strockWidth = 6;
public PathMeasurePay(Context context) {
this(context,null);
}
public PathMeasurePay(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public PathMeasurePay(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
circleX = w/2f;
circleY = h/2f;
mCircleRadius = Math.min(w/2-strockWidth,h/2-strockWidth);
}
private void init(Context context,AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PathMeasurePay);
int paintColor = a.getColor(R.styleable.PathMeasurePay_color, Color.BLACK);
mType = a.getInt(R.styleable.PathMeasurePay_type,1);
a.recycle();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(paintColor);
mPaint.setStrokeWidth(strockWidth);
pathMeasure = new PathMeasure();
animatorCircle = ValueAnimator.ofFloat(0,1);
animatorCircle.setDuration(1000);
animatorLine = ValueAnimator.ofFloat(0,1);
animatorLine.setDuration(mType==1?1000:500);
animatorLine2 = ValueAnimator.ofFloat(0,1);
animatorLine2.setDuration(500);
animatorCircle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCircleValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animatorCircle.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isDrawLine = true;
animatorLine.start();
}
});
animatorLine.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLineValue = (float) animation.getAnimatedValue();
invalidate();
}
});
animatorLine.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isDrawLine2 = true;
if(mType == 2){
animatorLine2.start();
}
}
});
animatorLine2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLineValue2 = (float) animation.getAnimatedValue();
invalidate();
}
});
animatorCircle.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.addCircle(getWidth()/2f,getHeight()/2f,mCircleRadius,Path.Direction.CW);
mCircleDst.reset();
pathMeasure.setPath(mPath,false);
float distance = pathMeasure.getLength() * mCircleValue;
pathMeasure.getSegment(0, distance , mCircleDst, true);
canvas.drawPath(mCircleDst, mPaint);
if(mType == 1){
if(isDrawLine){
mPath.reset();
mLineDst.reset();
mPath.moveTo(circleX-mCircleRadius/2f,circleY);
mPath.lineTo(circleX-mCircleRadius/10f,circleY+mCircleRadius/2f);
mPath.lineTo(circleX+mCircleRadius/2f,circleY-mCircleRadius/4f);
pathMeasure.setPath(mPath,false);
float dis = pathMeasure.getLength()*mLineValue;
pathMeasure.getSegment(0,dis,mLineDst,true);
canvas.drawPath(mLineDst,mPaint);
}
}else {
if(isDrawLine){
mPath.reset();
mLineDst1.reset();
mPath.moveTo(circleX - mCircleRadius/2f,circleY - mCircleRadius/2f);
mPath.lineTo(circleX+mCircleRadius/2f,circleY + mCircleRadius/2f);
pathMeasure.setPath(mPath,false);
float dis = pathMeasure.getLength()*mLineValue;
pathMeasure.getSegment(0,dis,mLineDst1,true);
canvas.drawPath(mLineDst1,mPaint);
}
if(isDrawLine2){
mPath.reset();
mLineDst2.reset();
mPath.moveTo(circleX+mCircleRadius/2f,circleY - mCircleRadius/2f);
mPath.lineTo(circleX - mCircleRadius/2f,circleY +mCircleRadius/2f);
pathMeasure.setPath(mPath,false);
float dis = pathMeasure.getLength()*mLineValue2;
pathMeasure.getSegment(0,dis,mLineDst2,true);
canvas.drawPath(mLineDst2,mPaint);
}
}
}
public void reset(){
isDrawLine = false;
isDrawLine2 = false;
animatorCircle.start();
}
}