自定义控件神器--PathMeasure

自定义控件神器–PathMeasure

我们平时自定义View的时候,经常会绘制path,想要求出path路径上的某个点的位置,很难通过一般的数学函数来计算,幸好系统给我们提供了一个PathMeasure可以用来测量一个path的路径,我们可以根据测量得到的值来绘制一些别的效果。

PathMeasure有两个构造方法

1
2
3
4
5
6
7
8
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方法中

1
2
3
4
5
6
7
8
9
10
11
12
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来时截取如果我们改一下截取的位置

1
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]可以获取到当前切线的角度。

下面看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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();
}
}

源码地址

参考:https://www.jianshu.com/p/3efa5341abcc

# UI

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×