美文网首页
动画的进阶

动画的进阶

作者: code希必地 | 来源:发表于2020-12-13 23:03 被阅读0次

    1、使用PathMeasure实现路径动画

    1.1、PathMeasure的初始化

    方法一:
    创建PathMeasure()对象

     public PathMeasure()
    

    调用setPath()方法绑定path

    public void setPath(Path path, boolean forceClosed)
    

    方法二:
    利用其带参构造函数

    public PathMeasure(Path path, boolean forceClosed)
    

    无论是哪种方法,都会接收一个参数forceClosed,它表示的是无论与之绑定的Path是否闭合,在使用PathMeasure计算的时候都会按照Path闭合的状态下进行计算,但是这并不会影响Path。

    1.2、简单函数的使用

    1.2.1、getLength()

    public float getLength() //用于计算路径的长度
    

    下面举例看下用法,分别设置forceClosed为true和false

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        paint.color = Color.RED
        paint.style=Paint.Style.STROKE
        paint.strokeWidth=5f
        path.moveTo(0f,0f)
        path.lineTo(0f,100f)
        path.lineTo(100f,100f)
        path.lineTo(100f,0f)
    
        val measure1=PathMeasure(path,false)
        val measure2=PathMeasure(path,true)
        Log.e(javaClass.simpleName,"measure1 ${measure1.length} , measure2 ${measure2.length}}")
        canvas?.drawPath(path,paint)
    }
    

    上面代码绘制出的路径图如下


    image.png

    从图中可以看出我们绘制的只是边长为100的正方形的三条边,而日志打印如下:

    E/PathView: measure1 300.0 , measure2 400.0}
    

    很明显,当forceClosed为false时,测出的是当前Path的长度,而当forceClosed为true时, 则不论path是否闭合,测量的都是Path闭合状态的长度。

    1.2.2、isClosed()

    函数的声明如下

    public boolean isClosed()
    

    该函数用于判断在测量Path时是否计算闭合。所以在关联Path时设置forceClosed为true则这个函数的返回值一定为true。如果这个Path是闭合的,即使forceClosed设置为false,这个函数也一定返回true。

    1.2.3、nextContour()

    函数的声明如下:

    public boolean nextContour()
    

    我们知道Path可能有多条曲线构成,但是不论getLength()、getSegment()还是其他函数,都是针对第一条曲线进行计算的,而nextContour()就是用于跳转到下一条曲线的函数。如果跳转成功了,则返回true,如果跳转失败了,则返回false。
    我们创建了一个Path并使其包含三条闭合的曲线,如下图所示,下面我们就用PathMeasure测量这三条曲线的长度。

    canvas?.translate(150f,150f)
    paint.color = Color.RED
    paint.style=Paint.Style.STROKE
    paint.strokeWidth=5f
    
    path.addRect(-50f,-50f,50f,50f,Path.Direction.CCW)
    path.addRect(-100f,-100f,100f,100f,Path.Direction.CCW)
    path.addRect(-120f,-120f,120f,120f,Path.Direction.CCW)
    
    val measure=PathMeasure(path,false)
    canvas?.drawPath(path,paint)
    
    do{
        val len=measure.length
        Log.e(javaClass.simpleName,"len=$len")
    }while (measure.nextContour())
    

    输出log如下:

    E/PathView: len=400.0
    E/PathView: len=800.0
    E/PathView: len=960.0
    

    从输出结果可知:

    • 通过PathMeasure.nextContour()得到的曲线顺序和添加的顺序一致。
    • getLength()获取的当前曲线的长度,不是整条曲线的长度。

    1.2.4、getSegment()

    函数的声明如下

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

    这个函数是用于截取整个Path中的某个片段,通过startD和stopD控制截取的长度,并将截取后的结果保存在dst中。最后一个参数startWithMoveTo表示起始点是否使用moveTo将路径的新起始点移到结果Path的起始点,通常设置为true。以保证每次截取的Path都是正常的、完整的,通常和dst一起使用,因为dst中保存的Path是被不断添加的,而不是每次被覆盖的,如果设置为false,则新增的Path会从上一次Path的终点开始计算,这样可以保证Path片段的连续性。
    参数:

    • float startD:开始截取位置距Path起点的长度
    • float stopD:结束截取位置距Path起点的长度
    • Path dst:截取的Path将会被添加到dst中。注意是添加,而不是替换
    • boolean startWithMoveTo:起始点是否使用moveTo
      **注意:
    • 1、如果startD、stopD的数值不在取值范围[0,getLength]内或者startD==stopD,则返回false,而且不会改变dst中的内容。
    • 2、开启硬件加速后,绘图会出现问题。使用getSegment()需要禁用硬件加速功能。**
      示例一
    canvas?.translate(150f,150f)
    paint.color = Color.RED
    paint.style=Paint.Style.STROKE
    paint.strokeWidth=5f
    canvas?.drawPoint(0f,0f,paint)
    path.addRect(-50f,-50f,50f,50f,Path.Direction.CCW)
    canvas?.drawPath(path,paint)
    val measure=PathMeasure(path,true)
    val dstPath=Path()
    measure.getSegment(50f,150f,dstPath,true)
    measure.getSegment(200f,350f,dstPath,true)
    paint.color=Color.BLACK
    canvas?.drawPath(dstPath,paint)
    

    绘制完成后的如下图


    image.png

    红色矩形为原Path的路径,黑色为截取后的片段
    当startWithMoveTo设置为false后,并不会调用moveTo将路径起点移动到裁剪路径的起点,效果如下图


    image.png
    示例二:路径加载动画
    路径绘制是PathMeasure的常用的功能,下面看下如何实现一条圆形路径如何从0增加到这个圆,代码如下
    lass PathView(context: Context, sttrs: AttributeSet) : View(context, sttrs) {
        private val path = Path()
        private val paint = Paint()
        private var stopD: Float = 0f
        private lateinit var measure: PathMeasure
        private val dstPath = Path()
    
        init {
            setLayerType(LAYER_TYPE_SOFTWARE,null)
            paint.color = Color.RED
            paint.style = Paint.Style.STROKE
            paint.strokeWidth = 5f
            path.addCircle(0f, 0f, 100f, Path.Direction.CCW)
            measure = PathMeasure(path, true)
    
            val animtor = ValueAnimator.ofFloat(0f, measure.length)
            animtor.addUpdateListener {
                Log.e(javaClass.simpleName,"$stopD")
                dstPath.rewind()
                stopD = it.animatedValue as Float
                measure.getSegment(0f, stopD, dstPath, true)
                invalidate()
            }
            animtor.duration = 1000
            animtor.repeatMode=REVERSE
            animtor.repeatCount= INFINITE
            animtor.start()
        }
    
    
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            canvas?.translate(150f, 150f)
            canvas?.drawPath(dstPath, paint)
        }
    }
    

    1.2.5、getPosTan()

    函数的声明如下

     public boolean getPosTan(float distance, float[] pos, float[] tan)
    

    该函数用于获取路径上某一段距离的位置和正切值。
    参数:

    • float distance:距Path起点的一段距离
    • float[] pos:该点的坐标值,该点在画布上的位置有两个点,pos[0]表示点的x坐标,pos[1]表示点的y坐标。
    • float[] tan:表示该点的正切值
      下图展示了坐标系中某点的正切值的计算方法


      image.png

      比如上图中我们要求A点的正切值,就是将A点与坐标原点链接起来,所形成的夹角a的正切值就是A点的正切值。
      而getPosTan()函数中获取的正切值也是一个二维数组,它代表了一个坐标(x,y),而通过y/x得到的就是对应点的正切值。而这个二维数组所代表的坐标对应的就是半径为1的圆的对应点。
      getPosTan()函数返回的是半径为1的圆中对应点的x,y坐标,那怎么求得夹角的值呢?
      在Math类中,有两个求反正切值的函数

    double atan(double d)
    double atan2(double y,double x)
    

    这两个函数都可以根据一个正切值求得对应的夹角数。函数atan(double d)的参数是一个弧度值,即正切的结果值。而函数atan2(double y,double x)的参数x,y就是正切的点的坐标。
    很显然我们通过atan2()函数就能得到夹角度数。
    而这个夹角的用处非常大,比如下图中有一个沿着圆形旋转的箭头,而当箭头围绕圆形旋转时, 应该实时的旋转箭头的方向,以使它的头与圆形边线吻合,比如从X轴开始移动了,移动了a角度后的情形如下图所示:

    image.png
    在移动a角度后,三角形应该旋转多少度才能跟圆形边线吻合呢?只有箭头一直沿着切线的方向 ,才能与圆形边线吻合,所以∠C就是我们要旋转的角度,由于∠a+∠b=90°,∠b+∠c=90°,所以∠a=∠c,正切夹角是多少度就旋转多少度。
    所以,如果想让移动点旋转至与切线重合,则旋转角度要与正切角度相同。
    示例:箭头加载动画
    这里将利用getPosTan()实现下面箭头加载动画
    image.png
    具体代码如下:
    class PathView(context: Context, sttrs: AttributeSet) : View(context, sttrs) {
        private val path = Path()
        private val paint = Paint()
        private var stopD: Float = 0f
        private lateinit var measure: PathMeasure
        private val dstPath = Path()
        private val posArray = FloatArray(2)
        private val tanArray = FloatArray(2)
        private val arrow = BitmapFactory.decodeResource(context.resources, R.mipmap.arrow)
        private val mMatrix=Matrix()
    
        init {
            setLayerType(LAYER_TYPE_SOFTWARE, null)
            paint.color = Color.RED
            paint.style = Paint.Style.STROKE
            paint.strokeWidth = 5f
            path.addCircle(0f, 0f, 100f, Path.Direction.CCW)
            measure = PathMeasure(path, true)
    
            val animtor = ValueAnimator.ofFloat(0f, measure.length)
            animtor.addUpdateListener {
                dstPath.rewind()
                stopD = it.animatedValue as Float
                measure.getSegment(0f, stopD, dstPath, true)
    
                mMatrix.reset()
                measure.getPosTan(stopD, posArray, tanArray)
                val degree=Math.toDegrees(Math.atan2(tanArray[1].toDouble(),tanArray[0].toDouble()))
                mMatrix.postRotate(degree.toFloat(),arrow.width/2.toFloat(),arrow.height/2.toFloat())
                mMatrix.postTranslate(posArray[0],posArray[1])
                invalidate()
            }
            animtor.duration = 3000
            animtor.repeatMode = REVERSE
            animtor.repeatCount = INFINITE
            animtor.start()
        }
    
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            canvas?.translate(150f, 150f)
    
            canvas?.drawBitmap(arrow,mMatrix,paint)
            canvas?.drawPath(dstPath, paint)
        }
    }
    

    需要注意的是:

    • 通过Math.atan2(tanArray[1].toDouble(),tanArray[0].toDouble())得到的是弧度值,而不是角度值,所以这里使用Math.toDegrees()将弧度值转换成了角度值。
    • 先利用matrix.postRotate()将图片围绕图片的中心点旋转指定角度,以便和切线重合。然后利用matrix.postTranslate()将图片从默认的(0,0)移动到当前路径的最前端。最后将图片绘制到画布上。
      但效果图却如下图
      image.png
      从效果图中可以看出箭头虽然沿着路径,但是有点偏差,图片移动的情况如下图
      image.png
      在移动图片时,以图片的左上角为起始点开始移动,所以原来的(0,0)点移动(pos[0],pos[1])距离后,图片的左上角在(pos[0],pos[1])位置上。这说明我们移动过头了,少移动半个图片就够了。
      将移动的半个图片加以改造,少移动半个图片即可
    mMatrix.postTranslate(posArray[0]-arrow.width/2,posArray[1]-arrow.height/2)
    

    1.2.6、getMatrix()

    函数声明如下

    public boolean getMatrix(float distance, Matrix matrix, int flags)
    

    这个函数用于得到路径上某一长度的位置以及该位置的正切值的矩阵。

    • float distance:距离Path起点的长度。
    • Matrix matrix:根据flags封装好的matrix会根据flags的设置而存入不同的内容
    • int flags:用于指定哪些内容存入matrix中。flags的值有两个:PathMeasure.POSITION_MATRIX_FLAG表示位置信息,PathMeasure.TANGENT_MATRIX_FLAG表示当前位置点的切边信息,使得图片按照Path旋转。可以指定一个,也可以使用 ‘|’位运算符同时指定。
      很明显:getMatrix()是getPosTan()的另一种实现而已,只不过getPosTan()是将位置信息和切边信息分别存在了pos和tan的数组中。而getMatrix()直接将信息存入matrix数组中。
      下面尝试使用getMatrix()替换getPosTan()实现箭头动画
    init {
        //.....代码相同 省略
        animtor.addUpdateListener {
            dstPath.rewind()
            stopD = it.animatedValue as Float
            measure.getSegment(0f, stopD, dstPath, true)
    
            mMatrix.reset()
    //            measure.getPosTan(stopD, posArray, tanArray)
    //            val degree=Math.toDegrees(Math.atan2(tanArray[1].toDouble(),tanArray[0].toDouble()))
    //            mMatrix.postRotate(degree.toFloat(),arrow.width/2.toFloat(),arrow.height/2.toFloat())
    //            mMatrix.postTranslate(posArray[0]-arrow.width/2,posArray[1]-arrow.height/2)
    
    //上面注释代码是使用getPosTan()实现的,下面两句是通过getMatrix()方法实现的       measure.getMatrix(stopD,mMatrix,PathMeasure.POSITION_MATRIX_FLAG or PathMeasure.TANGENT_MATRIX_FLAG)
            mMatrix.preTranslate(-arrow.width/2.toFloat(),-arrow.height/2.toFloat())
    
            invalidate()
        }
        animtor.duration = 3000
        animtor.repeatMode = REVERSE
        animtor.repeatCount = INFINITE
        animtor.start()
    }
    

    相关文章

      网友评论

          本文标题:动画的进阶

          本文链接:https://www.haomeiwen.com/subject/ltqygktx.html