动画的进阶

2020-12-13  本文已影响0人  code希必地

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

从输出结果可知:

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片段的连续性。
参数:

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)

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

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)
    }
}

需要注意的是:

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)

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

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()
}
上一篇下一篇

猜你喜欢

热点阅读