美文网首页
Android动画学习(三):自定义属性动画

Android动画学习(三):自定义属性动画

作者: 静水红阳 | 来源:发表于2021-09-05 19:13 被阅读0次

    前言

    在前两篇文章中,我们介绍了Android中的帧动画,补间动画,并对基本属性动画进行了简单的说明,下面我们对属性动画做一个补充,对属性动画的估值器和插值器进行简单说明。

    前文回顾:
    Android动画学习(一):帧动画和补间动画
    Android动画学习(二):基本属性动画

    一、估值器(TypeEvaluator)

    在之前的补间动画和简易属性动画中,虽然我们能够实现View平移效果,但是其效果更多的是限于x轴或者Y轴的直线移动。虽然我们能够通过组合动画实现特殊方向移动效果,但是对于实现指定的自由函数曲线的移动是十分不方便的,利用属性动画中的估值器可以比较方便的实现这一动画。

    TypeEvaluator其具体的功能就是为动画设置其从开始到结束的值变化计算。

    1. 源码阅读

    我们首先对TypeEvalutor其接口源码进行查看。源代码很简单,接口内只有一个方法:

    public interface TypeEvaluator<T> {
        public T evaluate(float fraction, T startValue, T endValue);
    }
    

    查看代码注释,了解到此方法可以根据动画的起终值以及完成度来计算执行中动画值的变化,相当于将动画逐帧播放时每帧动画值计算。

    三个参数:

    1. fraction表示动画执行度
    2. startValue表示动画起始值
    3. endValue表示动画终值。

    2. demo实例

    由于TypeEvalutor是一个接口,所以如果我们需要自定义动画运动轨迹,则需要新建类继承TypeEvaluator接口,在evaluate方法中对动画值进行计算,达到动画轨迹运行的效果。

    下面我们举个例子进行说明:假设我们现在要使用自定义View实现一个小圆形View的正弦曲线动画效果。

    我们可以分为如下几步进行:

    首先我们定义一个View,用来完成动画的展示,在其中完成坐标和圆形区域的绘制。

    class PointAnimView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
        private val DEFAULT_RADIUS = 20.0
        var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
        var linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
        var color: Int = 0
        var radius: Double = 20.0
        var currentPoint: Point = Point(DEFAULT_RADIUS, height / 2f + DEFAULT_RADIUS)
        var pointStart: Point = Point(DEFAULT_RADIUS, DEFAULT_RADIUS)
        var pointEnd: Point = Point(DEFAULT_RADIUS, DEFAULT_RADIUS)
    
        init {
            mPaint.color = Color.BLACK
            linePaint.color = Color.BLACK
            linePaint.strokeWidth = 5f
        }
    
        override fun onDraw(canvas: Canvas?) {
            drawCircle(canvas)
            drawLine(canvas)
            super.onDraw(canvas)
        }
    
        private fun drawLine(canvas: Canvas?) {
            canvas?.drawLine(10f, height / 2f, width.toFloat(), height / 2f, linePaint)
            canvas?.drawLine(10f, height / 2f - 150, 10f, height / 2f + 150, linePaint)
            canvas?.drawPoint(currentPoint.x.toFloat(), currentPoint.y.toFloat(), linePaint)
        }
    
        private fun drawCircle(canvas: Canvas?) {
            var x = currentPoint.x
            canvas?.drawCircle(x.toFloat(), currentPoint.y.toFloat(), radius.toFloat(), mPaint)
        }
    }
    

    其中Point是一个用来标识坐标的类,示例如下:

    class Point(var x: Double, var y: Double) {
        override fun toString(): String {
            return "x:$x     y:$y"
        }
    }
    

    接下来我们写一个继承TypeEvaluator接口的类,用来计算圆形区域运动轨迹,代码如下:

    class PointSinEvaluator : TypeEvaluator<Any?> {
        override fun evaluate(fraction: Float, startValue: Any?, endValue: Any?): Any? {
            val startPoint = startValue as Point?
            val endPoint = endValue as Point?
            return if (startPoint != null && endPoint != null) {
                val x = (startPoint.x + fraction * (endPoint.x - startPoint.x))
                val y = ((sin(x * Math.PI / 180) * 100) + endPoint.y / 2.0)
                Point(x, y)
            } else {
                Point(0.0, 0.0)
            }
        }
    }
    

    利用动画完成度计算横轴x的值,利用正弦曲线公式计算y轴的值。

    然后,我们就可以在View中定义一个动画方法,如下:

        fun setAnimation() {
            pointEnd.x = width - DEFAULT_RADIUS
            pointEnd.y = height - DEFAULT_RADIUS
            //指定View的动画轨迹
            var valueAnimator: ValueAnimator =
                ValueAnimator.ofObject(PointSinEvaluator(), pointStart, pointEnd)
            valueAnimator.repeatCount = -1
            valueAnimator.duration = 5000
            valueAnimator.repeatMode = ValueAnimator.REVERSE
            valueAnimator.addUpdateListener {
                currentPoint = it.animatedValue as Point
                postInvalidate()
            }
            valueAnimator.start()
        }
    

    为动画设置时长为5s,并且设置循环效果,并在动画执行的时候不停的同步显示更新。

    我们在外部调用此View,得到的效果图如下:

    动画测试1.gif

    至此我们实现了一个简易的正弦曲线移动动画了。

    二、ObjectAnimator

    在上篇文章中,我们提到了补间动画只能够实现移动,缩放、旋转,透明度四种操作,没有扩展性,对于像动态改变View颜色这种功能不能够实现。

    但是对于ObjectAnimator来说却能够比较方便的实现这一动画效果。在上篇文章中,我们提到ObjectAnimator内部的工作机制是通过寻找指定属性的值进行不断更新达到动画效果的。因此,对于更新View颜色这样的问题,我们同样可以获取到颜色属性进行设置。

    对于上面的例子,如果我们在实现正弦曲线移动的同时还需要对圆形的颜色和大小进行变化,可以通过如下几步完成:

    1. 颜色设置

    首先完成对圆形的颜色的变化设定(随便设置了几个颜色):

            var animColor = ObjectAnimator.ofObject(
                mPaint,
                "color", ArgbEvaluator(),Color.BLACK, Color.YELLOW, Color.BLUE, Color.GRAY, Color.GREEN
            )
            animColor.repeatCount = -1
            animColor.repeatMode = ValueAnimator.REVERSE
    

    可以看到很方便的能获取到View的color属性,设置几个不同的颜色。

    2. 缩放设置

    接下来完成对圆形缩放的设定:

            var scaleAnim = ObjectAnimator.ofFloat(20f, 5f, 40f, 10f, 30f)
            scaleAnim.repeatCount = -1
            scaleAnim.repeatMode = ValueAnimator.REVERSE
            scaleAnim.duration = 5000
            scaleAnim.addUpdateListener {
                radius = (it.animatedValue as Float).toDouble()
            }
    

    3. 混合动画

    显然这已经是一个混合动画了,我们使用AnimatorSet完成属性动画的拼接:

            var animSet = AnimatorSet()
            animSet.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animation: Animator?) {
                    LogUtil.instance.d("动画开始")
                }
            })
            animSet.play(valueAnimator).with(scaleAnim).with(animColor)
            animSet.duration = 5000
            animSet.start()
    

    调用结果:

    测试动画2.gif

    三、动画插值器(TimeInterpolator)

    TimeInterpolator,动画插值器,通俗来说就行用来控制动画执行快慢的设置。

    在上篇文章中,我们提到一些常用的插值器,如LinearInterpolator是线性执行,动画变化率不变化。但是有时候我们需要完全自定义动画的执行速率,这就需要我们自己实现插值器接口TimeInterpolator了。

    1. 源码查看

    先看下此接口的源码,相对比较简答,只有一个方法:

    public interface TimeInterpolator {
        float getInterpolation(float input);
    }
    

    查看注释能大致理解此方法能够根据输入的动画时间比例输出当前动画应该执行的进度。

    接下来我们就可以继承此接口自定义插值器了。

    2. 插值器快慢估算

    写到这里问题又来了,我们怎么去区分当前执行的速率是快还是慢呢?究竟返回的值越大越快还是返回的值越小越快呢?

    其实区分方法还是比较简单的,我们从速度的定义来理解,动画执行速度就是一段固定时间内动画执行进度的多少,因此我们可以通过固定的时间节点做一个简单对比,我们以线性速率和指数速率做一个比较进行说明。

    首先我们先查看下线性插值器的代码,显然是直接输出了当前的时间节点:

    @HasNativeInterpolator
    public class LinearInterpolator extends BaseInterpolator implements NativeInterpolator {
    
        public LinearInterpolator() {
        }
    
        public LinearInterpolator(Context context, AttributeSet attrs) {
        }
    
        public float getInterpolation(float input) {
            return input;
        }
    
        /** @hide */
        @Override
        public long createNativeInterpolator() {
            return NativeInterpolatorFactory.createLinearInterpolator();
        }
    }
    

    我们自定义指数插值器f(x)= x^2,代码如下:

    class TestTimeInterpolator : TimeInterpolator {
        override fun getInterpolation(input: Float): Float {
            return input * input
        }
    }
    

    我们以20%的时间进度作为时间节点进行两个插值器的比较,如下表:

    时间 线性时间插值 当前公式时间插值
    0 0 0
    0.2 0.2 0.04
    0.4 0.4 0.16
    0.6 0.6 0.36
    0.8 0.8 0.64
    1 1 1

    从上面是时间对照表中可以看到:

    1. 常规的线性插值在0-0.2时执行0.2的内容,而平方插值在0-0.2内只执行了0.04的内容,所以开始可以看到平方插值执行缓慢,低于线性插值速度。
    2. 在0.8-1时线性插值执行了0.2的内容,而平法插值执行了0.36的内容,超过了平方插值;
    3. 平方插值在0-0.2时间内执行了0.04的内容,而在0.2-0.4之间执行了0.12的内容,大于0.04,而在0.4-0.6之间执行了0.2的内容,大于0.12,后面的时间节点同样如此,所以平方插值是执行速率是在逐渐变快

    从上面几个内容中,我们可以得到如下结论:

    平方插值在开始时执行速度低于线性插值,而后平方插值的速度越来越快,最后超过线性插值。

    我们对平方插值进行测试,还是继续采用上面正弦曲线的例子,我们设置平方插值:

            animSet.interpolator = TestTimeInterpolator()
    

    执行结果如下:

    动画练习.gif

    从动画效果我们可以看到起速率和我们预计的基本相当,其他的插值快慢也可以同样采用此种方法进行估算,无非是时间粒度的问题。

    总结

    本文是对属性动画做了一个简单的补充,通过一个实例对属性动画中常用的TypeEvaluatorObjectAnimator和插值器进行了简单说明。

    相关文章

      网友评论

          本文标题:Android动画学习(三):自定义属性动画

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