Android自定义控件开发入门与实战(6)路径动画

第五章、动画进阶

前面几章所讲的内容其实都只是比较普通、简单的动画,这章开始学习较难、较为有深度、也比较可以实现更加炫酷效果的动画,通过PathMeasure和SVG动画来实现。

PathMeasure实现路径动画

PathMeasure是Android官方API,是之前第一张关于路径只是的一个扩展,十分的有用。
PathMeasure可以计算出路径的总长、指定长度所对应的坐标等等路径信息。

有两种初始化方式:
第一种:

PathMeasure pathMeasure = new PathMeasure();

直接new一个,然后接下来通过PathMeasre的setPath()绑定PathMeasure和Path

setPath(Path path,boolean forceClosed)

这样就完成初始化,接着就可以用pathMeasure来调用关于路径的信息辽。

第二种方法:
直接通过PathMeasure构造方法初始化。

PathMeasure(Path path,boolean forceClosed);

这两种方法都会涉及到的forceClosed是计算path是否闭合,但是path的闭合并不是由forceClosed控制,绘制出来时,path该闭合还是闭合,不闭合就是不闭合,但是如果forceClosed设置为true时,会当做path闭合,把闭合的路径算进去。

一些简单的函数使用
1、getLength()

public float getLength()

该函数的作用为计算路径长度,使用非常广泛。
我们用路径来画一个没有闭合的正方形:

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.e(TAG, "forceClose = false : " + pathMeasure1.getLength());
        Log.e(TAG, "forceClose = true : " + pathMeasure2.getLength());
        canvas.drawPath(path,paint);

在这里插入图片描述
打出的第一个Log长度为300,而第二个则为400.因为第二个已经考虑到闭合了。

2、isClose()
判断测量的path是否闭合。
如果PathMeasurei的forceClosed设置为true时,则isClosed()一定为true

3、nextContour()
Path可以有很多曲线、线段构成,但是getLength()只会取第一条线进行计算。
而nextContour()是跳转到下一条曲线的函数。如果跳转成功则返回true,否则返回false。
注:pathMeasure.getLength()只针对第一条曲线

getSegment
用法

boolean getSegment(float startD,float stopD,Path dst,boolean startWithMoveTo);

顾名思义,这个函数用截取一个Path中的某一个片段。
通过参数startD和stopD来控制截取的长度。并将截取后的Path保存到参数dst中。
最后一个参数startWithMoveTo表示起始点是否使用moveTo将路径的新起始点移到结果Path的起始点,通常设置为true。用来保证每次截取segment都是连续的、完整的。

  • 其中startD为开始截取位置距离Path起点的长度,stopD为结束时截取位置距离Path起点的长度。如果startD和stopD的范围不再Path的长度范围内或者 startD==stopD该函数返回false
  • 如果在开启硬件加速并使用该方法,绘图会出现问题,所以在使用getSegment时要禁用硬件加速。

这里来截取一个path,代码如下:

        canvas.translate(100, 100);
        Path path = new Path();
        path.addRect(-50,-50,50,50, Path.Direction.CW);
        Path dst = new Path();
        PathMeasure pathMeasure = new PathMeasure(path, false);
        pathMeasure.getSegment(0,150,dst,true);
        canvas.drawPath(dst, paint);

截取的path如下
在这里插入图片描述
这说明截取是左上角开始截取,并且方向是根据Path的绘制方向截取,上面path绘制是CW(顺时针),所以截取了上半部分。

如果dst本来就已经是一个路径,这个时候再去取别的path的路径,会怎么样呢?
答案是 原来的路径不会被覆盖,反而和新的截取到的路径一起绘制出来。

如下图所示:
在这里插入图片描述
如果这个时候我们把 PathMeasure的startWithMoveTo改为false会怎么样呢?下过如下所示:
在这里插入图片描述
这里咋一看不是很好理解,其实画个图就ok,因为startWithMoveTo设置为false就是将新的Path的起始点拉到自己原本dst的结束点(因为dst自己画的是不能变的) ,然后目标path其他位置的点不变
就像是使用processon、viso软件画图的时候,用一条线的起点去连另一条线的终点这样。

示例
路径绘制是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()函数。

思路是:

  1. 先用path预先按顺序画好外面的圆和里面的勾
  2. 设置动画,大体上分成两个部分,第一个部分画圆,第二个画勾,中间使用nextContour过渡
  3. 通过动画的进度,使用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);
            mPathMeasure.nextContour();
            flag = 0;
        } else {
            float stop = mPathMeasure.getLength() * (mProgress - 1f);
            mPathMeasure.getSegment(0, stop, mDst, true);
        }
        canvas.drawPath(mDst, mPaint);
    }

实现起来其实挺简单,但是有一点,在onDraw时,书上时当mProgress为1时做过渡,但我测的时候实际情况mProgress很少能够得到1,一般时1.xxxxx或者0.9xxxx,当完全为1.0时很少存在,所以我加了一个范围,并且nextContour只能走一次。

效果如下:
在这里插入图片描述
PathMeasure的学习就到这里了。
但是以后还是会经常用到PathMeasure动画的(毕竟getSegment这个方法太精髓,太经典)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值