示例
路径绘制是PathMeasure最常用的功能,下面实现一个转圈圈的加载效果图。
思路是通过ValueAnimator动画算出当前的动画的进度,通过进度获取转圈圆的周长,拿到周长后通过PathMeasure的getLength和getSegment去画圆。
我们再构造函数中做new的操作:
public PathMeasureView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
setLayerType(LAYER_TYPE_SOFTWARE, null);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
dst = new Path();
circlePath = new Path();
circlePath.addCircle(100, 100, 50, Path.Direction.CW);
pathMeasure = new PathMeasure(circlePath, true);
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
drawProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.setInterpolator(new AccelerateInterpolator());
animator.setDuration(2000);
animator.start();
}
之后再draw函数中做下面的操作:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
float stop = pathMeasure.getLength() * drawProgress;
dst.reset();
pathMeasure.getSegment(0, stop, dst, true);
canvas.drawPath(dst, paint);
}
最后效果如下所示:
但是这个加载圈的起点一直在画圆的起点,和我们平时看到的加载圆有点不一样,所以我们可以去改变它的起始点,来让圆更加生动:
当动画开始到一半的时候,起点都是最开始的画圆的起点,到后半段,dst圆的起始点开始逐渐向结束点靠拢,最后到达开始位置的时候,两个端点重合
可以得出当
进度drawProgress< 0.5时 startD=0
进度drawProgress>0.5时 startD=(2*drawProgress-1)*length
通过合并公式可以得出 startD = stopD - (0.5 - |drawProgress - 0.5| )*length
float start = (float) (stop - (0.5 - Math.abs(drawProgress - 0.5)) * pathMeasure.getLength());
pathMeasure.getSegment(start, stop, dst, true);
canvas.drawPath(dst, paint);
这就很顶啦。
getPosTan()
getPosTan()函数用于得到路径上某一长度的位置以及该位置的正切值。
boolean getPosTan(float distance ,float[]pos,float[]tan);
float distance: 距离Path起始点的长度,取值范围为0≤distance≤getLength
float[]pos:该点的坐标值 pos[0]表示x坐标 pos[1]表示y坐标
float[]tan:该点的tan值。
所谓的求tan,就是将该点与坐标轴原点连接在一起,与x轴的夹角为α,而tanα就是该点的正切值。
我们通过坐标(x,y)用y/x来获取正切值。
又通过正切值我们就可以通过 Math atan2(double y,double x) 来获取一个α
这个夹角其实用处很大,比如我们在上面加载圈圈的例子中,添加一个箭头,但是如果箭头没有任何知识,它是不会跟着圆圈转的,所以就有必要知道它的夹角,根据夹角来让这个箭头转。
如果想让箭头的旋转角度和切向方向相同,则该点旋转角度要和该点正切角度相同。
下面来实现一下
private Bitmap mArrawBmp;
private float[] pos = new float[2];
private float[] tan = new float[2];
public PathMeasureView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载图片
mArrawBmp = BitmapFactory.decodeResource(getResources(), R.drawable.arrow);
…
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
…
//旋转箭头图片,并绘制
pathMeasure.getPosTan(stop, pos, tan);
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180f / Math.PI);
Matrix matrix = new Matrix();
matrix.postRotate(degrees, mArrawBmp.getWidth() / 2, mArrawBmp.getHeight() / 2);
matrix.postTranslate(pos[0] - mArrawBmp.getWidth() / 2, pos[1] - mArrawBmp.getHeight() / 2);
canvas.drawBitmap(mArrawBmp, matrix
, paint);
}
就可以实现下面的效果:
getMatrix()
这个函数用来得到路径上某一长度的位置以及该位置的正切值的矩阵。
boolean getMatrix(float distance,Martix matrix,int flags)
distance:距离Path起点的长度
matrix:根据Matrix封装好的matrix会根据flags的设置而存入不同的内容
flags:用于指定哪些内容会存入matrix中,flags的值有两个:PathMeasure.POSITION_MATRIX_FLAG:获取位置信息;pathMeasure.TANGENT_MATRIX_FLAG:获取切边信息,使得图片按Path旋转。
可以指定一个,也可以使用“|”同时指定。
可以看的出来,getMatrix是PathMeasure.getPosTan()的另一个实现而已,getPosTan将获取到的位置信息和切边信息保存在pos和tan数组中,而getMartix则直接把这些信息保存到matrix中。
示例:
这里做一个支付宝支付成功的动画。 就是外面先画一个圆,再在圆圈内打一个勾。
因为圆圈和勾是分开的,所以会用到nextContour()函数。
思路是:
先用path预先按顺序画好外面的圆和里面的勾
设置动画,大体上分成两个部分,第一个部分画圆,第二个画勾,中间使用nextContour过渡
通过动画的进度,使用PathMeasure的getSegment来绘制
勾的坐标要确定三个,其中mCenterX,mCenterY为圆心坐标,mRadius为半径
第一个是勾的左边顶点,坐标为(mCenterX-mRadius/2,mCenterY)
第二个为勾的下顶点,坐标为(mCenterX,mCenterY+mRadius/2)
第三个为勾的右顶点,坐标为(mCenterX+mRadius/2,mCenterY-mRadius/3)
代码如下:
public AliPayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(3);
mPaint.setColor(0xff4b66ed);
mDst = new Path();
mCircle = new Path();
mCenterX = 500;
mCenterY = 1100;
mRadius = 300;
mCircle.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);
mCircle.moveTo(mCenterX - mRadius / 2, mCenterY);
mCircle.lineTo(mCenterX, mCenterY + mRadius / 2);
mCircle.lineTo(mCenterX + mRadius / 2, mCenterY - mRadius / 3);
mPathMeasure = new PathMeasure(mCircle, false);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 2);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mProgress = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(4000);
valueAnimator.setInterpolator(new AccelerateInterpolator());
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
if (mProgress < 1f) {
float stop = mPathMeasure.getLength() * mProgress;
mPathMeasure.getSegment(0, stop, mDst, true);
} else if (mProgress >= 1f && mProgress <= 1.1f && flag == 1) {
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDst, true);
imator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
if (mProgress < 1f) {
float stop = mPathMeasure.getLength() * mProgress;
mPathMeasure.getSegment(0, stop, mDst, true);
} else if (mProgress >= 1f && mProgress <= 1.1f && flag == 1) {
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDst, true);