PathMeasure这个东西还是挺神奇的,我们看到的许多酷炫的动画大多要依靠他,他就像一个计算器,你给他一个path,他还你路径总长、指定长度的终点坐标,路径上某一点的tan、sin、cos值等等。这次我们来看看怎么用它做一个飞机转圈的加载动画,效果如下图:
先了解PathMeasure的一些方法:
一、初始化
他的初始化有两种,第一种直接new空的构造方法,得到实例后利用setPath传入路径,如:
PathMeasure p=new PathMeasure ();
p.setPath(path,true);
第二种,直接在构造时候传入path,
PathMeasure p=new PathMeasure (path,true);
我们看到true这个参数多次出现,他代表的是PathMeasure 是否闭合的参数,如果为true,那么不管path有没有闭合,PathMeasure 都会闭合,但是只会影响PathMeasure 对path的计算,而不会改变path本身。
二、getLength
顾名思义就是获取path在计算后的长度。下面我们利用getLength看看上面说的true是怎么影响计算的。我们先定义一个自定义view,如下:
public class MyView extends View {
private Paint paint;
public MyView(Context context) {
this(context,null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(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 pathMeasure1=new PathMeasure(path,false);
PathMeasure pathMeasure2=new PathMeasure(path,true);
Log.d("yanjin","pathMeasure1的length="+pathMeasure1.getLength()+"--pathMeasure2的length="+pathMeasure2.getLength());
canvas.drawPath(path,paint);
}
}
展示效果如上图,我们打印的getLength在设置为true和false的时候,会有不同的数值,一个为300,一个为400,多出来的100,大家应该也知道在哪来的吧,哈哈哈哈。
三、nextContour
我们都知道,一个path就相当于一个集合,他可以不断地add很多不连续的路径PathMeasure只对连续的路径有效果,那么假如path里面有A/B/C三个不连续的线段,怎么计算他们的值呢?这里就用到了nextContour函数,简单的说他就是跳到下一个线段的作用。比如如下代码:
public class MyView2 extends View {
private Paint paint;
public MyView2(Context context) {
this(context,null);
}
public MyView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
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.CW);
canvas.drawPath(path,paint);
PathMeasure pathMeasure=new PathMeasure(path,false);//已经闭合了,我们可以传false。
do {
float length = pathMeasure.getLength();
Log.d("yanjin","len="+length);
}while (pathMeasure.nextContour());
}
}
输出的值为:
2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=400.0
2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=800.0
2019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=960.0
我们可以得出以下结论:
1、nextContour函数得到的path循序与我们path.add时顺序一样。
2、getLength针对的是当前线段,不是整个path。
四、getSegment函数
他的定义如下:
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
getSegment是用来截取一段path的,通过startD与stopD设置起始点。然后将截取的path存到dst中,startWithMoveTo表示是否使用moveTo,将路径的新起点移动到结果path的起点,一般为true。
进过上面的介绍,我们可以先写一个常见的加载动画了,动画效果如下:
这个很常见吧,下面来讲讲他的代码。
先写自定义CirclePathAnimView代码
public class CirclePathAnimView extends View {
private float mAnimatorValue;
private PathMeasure mPathMeasure;
private Path mDevPath;
private Paint mPaint;
private ValueAnimator mValueAnimator;
public CirclePathAnimView(Context context) {
this(context, null);
}
public CirclePathAnimView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CirclePathAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayerType(LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
//初始化画笔
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(getResources().getDimension(R.dimen.dp_3));
mPaint.setColor(getResources().getColor(R.color.colorPrimary));
//画真正显示的path
mDevPath = new Path();
//开始动画,当然当前动画你可以单独写成一个方法
mValueAnimator = ValueAnimator.ofFloat(0, 1);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.setDuration(2000);
mValueAnimator.setRepeatCount(-1);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
mValueAnimator.start();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int radius = 0;
if (width >= height) {
radius = height / 2 - height / 8;
} else {
radius = width / 2 - width / 8;
}
//绘制path
//先画圆的path,但是这个圆只是用来计算
Path circlePath = new Path();
circlePath.addCircle(width / 2, height / 2, radius, Path.Direction.CW);
//计算圆的path的长度
mPathMeasure = new PathMeasure(circlePath, true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float length = mPathMeasure.getLength();
float stop = length * mAnimatorValue;
//在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢。
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * length));
mDevPath.reset();
mPathMeasure.getSegment(start, stop, mDevPath, true);
canvas.drawPath(mDevPath, mPaint);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mValueAnimator.cancel();
mValueAnimator = null;
}
}
我们可以先看构造方法,我们先设置mPaint ,然后开启动画,其实这个动画可以另写一个方法,手动掉一下,这里为了方便就写在这了,可以看到动画的更新监听里面我们获取动画值之后,调用invalidate刷新界面,这样会重走onDraw方法,这里讲onDraw之前,先看看onMeasure。
onMeasure里面我们主要拿到控件自己的宽高,设置了一个圆形Path--》circlePath ,但是这个circlePath 并没有被画出来,他只是用来被截取的,mPathMeasure 存入这个circlePath 。
然后动画中每调用invalidate进入onDraw的时候,拿动画值mAnimatorValue*path总长得到当前终点,起点的话,我们采取在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢的算法获得起点。这样我们就能调用截取方法了
mPathMeasure.getSegment(start, stop, mDevPath, true);
截取后原本为空的mDevPath就有数据了,我们就可以把它画下来了。为了让他看起来更有意思,我们在Activity中对他整个空间进行旋转,
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main29);
CirclePathAnimView circlePathAnimView = findViewById(R.id.view);
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(circlePathAnimView,"rotation",0,360);
objectAnimator.setRepeatCount(-1);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.setDuration(2500);
objectAnimator.start();
}
就能看到上面的效果了。
说了半天,答应的飞机呢?这样,我先上代码。还是那个自定义View,我只是改了一点点代码。
public class CirclePathAnimView extends View {
private float mAnimatorValue;
private PathMeasure mPathMeasure;
private Path mDevPath;
private Paint mPaint;
private ValueAnimator mValueAnimator;
private Bitmap airplayBitmap;
public CirclePathAnimView(Context context) {
this(context, null);
}
public CirclePathAnimView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CirclePathAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayerType(LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
//初始化画笔
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(getResources().getDimension(R.dimen.dp_2));
mPaint.setColor(getResources().getColor(R.color.colorPrimary));
//飞机图片
airplayBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.airplay);
//画真正显示的path
mDevPath = new Path();
//开始动画,当然当前动画你可以单独写成一个方法
mValueAnimator = ValueAnimator.ofFloat(0, 1);
mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mValueAnimator.setDuration(3000);
mValueAnimator.setRepeatCount(-1);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
mValueAnimator.start();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int radius = 0;
if (width >= height) {
radius = height / 2 - height / 8;
} else {
radius = width / 2 - width / 8;
}
//绘制path
//先画圆的path,但是这个圆只是用来计算
Path circlePath = new Path();
circlePath.addCircle(width / 2, height / 2, radius, Path.Direction.CW);
//计算圆的path的长度
mPathMeasure = new PathMeasure(circlePath, true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float length = mPathMeasure.getLength();
float stop = length * mAnimatorValue;
//在0到0.5以前,起点不变,0.5到1,起点开始向终点靠拢。
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * length));
mDevPath.reset();
mPathMeasure.getSegment(start, stop, mDevPath, true);
canvas.drawPath(mDevPath, mPaint);
Matrix matrix=new Matrix();
mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
matrix.preTranslate(-airplayBitmap.getWidth()/2,-airplayBitmap.getHeight()/2);
canvas.drawBitmap(airplayBitmap,matrix,mPaint);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mValueAnimator.cancel();
mValueAnimator = null;
}
}
在构造方法中,我们先获取图片
airplayBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.airplay);
在onDraw中,我们把飞机画上去。
Matrix matrix=new Matrix();
mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
matrix.preTranslate(-airplayBitmap.getWidth()/2,-airplayBitmap.getHeight()/2);
canvas.drawBitmap(airplayBitmap,matrix,mPaint);
就这么简单。
看是这么简单,但是里面有个getMatrix函数我们必须要讲一讲。
五、getMatrix函数
getMatrix函数可以获得某一长度终点的坐标以及该坐标的正切值的矩阵。
public boolean getMatrix(float distance, Matrix matrix, int flags)
distance指的是path长度,
matrix指的是容器,计算后会把结果存进来。
flags指的是要存入哪些内容,POSITION_MATRIX_FLAG是位置信息,TANGENT_MATRIX_FLAG是切边信息。
图片中箭头代表飞机,飞机没飞一点,就要调整角度,他的方向基本要与切线一样,那么根据图中所示,角a+角b=90度,角a=角c,所以飞机头要掉角c这么多度数,而getMatrix就是能获取这些正切值。结合画图也有传Matrix的方式,刚刚好。
有时间再更新个支付宝支付成功的动画,嘻嘻。
对了,不喜勿喷哦!,我的心脏很弱小的。