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

前言

在前两篇文章中,我们介绍了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和插值器进行了简单说明。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容