PathMeasure:路径动画飞机转圈的加载动画
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.02019-02-22 15:48:33.787 7889-7889/com.easy.customeasytablayout.customviews D/yanjin: len=800.02019-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的方式,刚刚好。
有时间再更新个支付宝支付成功的动画,嘻嘻。对了,不喜勿喷哦!,我的心脏很弱小的。