属性动画

546 阅读11分钟

上一篇说了View动画和帧动画,google为了解决它们带来的问题,在3.0之后又添加了属性动画,目的是为了解决 :

1.不再局限于视图View对象

2.不再局限于4种基本变换:平移、旋转、缩放 & 透明度

属性动画继承关系图:

属性动画的工作原理:在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,不断的调用invalidate方法,不断触发onDrow重新绘制视图,从而实现该对象在该属性上的动画效果。

ValueAnimator类

ValueAnimator本质只是一种值的操作机制,至于实现动画,是需要开发者手动将这些 值 赋给 对象的属性值。

原理图:

从上面原理可以看出:ValueAnimator类中有3个重要的静态变换逻辑方法:

  1. ValueAnimator.ofInt(int values)
  2. ValueAnimator.ofFloat(float values)
  3. ValueAnimator.ofObject(int values)

ValueAnimator.ofInt(int values)

将初始值 以整型数值的形式 过渡到结束值。对应的估值器是IntEvaluator

动画的实现方式同View动画一样有两种写法,xml(不说明)和代码,实际开发中,一般都使用Java代码实现属性动画:因为很多时候属性的起始值是无法提前确定的(无法使用XML设置),这就需要在Java代码里动态获取。

事例代码写法:

tvjump.setOnClickListener {
    // 步骤1:设置动画属性的初始值 & 结束值,内部创建一个ValueAnimator对象,并默认设置了正数估值器
    val valueAnimator = ValueAnimator.ofInt(0, 30)
    // ofInt()作用:1.创建动画实例 ;2.将传入的多个Int参数进行平滑过渡:此处传入0和4,表示将值从0平滑过渡到4
    // 如果传入了3个Int参数 a,b,c ,则是先从a平滑过渡到b,再从b平滑过渡到C,以此类推
    // ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置,即默认设置了如何从初始值过渡到结束值的逻辑

    // 步骤2:设置动画的播放各种属性

    // 设置动画运行的时长
    valueAnimator.setDuration(500)
    // 设置动画延迟播放时间
    valueAnimator.setStartDelay(500)
    // 设置动画重复播放次数 = 重放次数+1,动画播放次数 = infinite时,动画无限重复
    valueAnimator.setRepeatCount(0)
    // 设置重复播放动画模式 ValueAnimator.RESTART(默认):正序重放 ;ValueAnimator.REVERSE:倒序回放
    valueAnimator.setRepeatMode(ValueAnimator.RESTART);

    //步骤3:通过监听回调获取值的每个变化节点
    valueAnimator.addUpdateListener {
        //获取每个变化的值
       var newValue =  it.getAnimatedValue() as? Int
        Log.e("rrrrrrr",newValue.toString())
        //步骤4:手动赋值
        // 获取的值可以做任何操作,这里是设置给一个控件的属性,内部自动去调用重绘方法
        tvjump.translationY = newValue?.toFloat()?:0F
    }
    // 启动值的改变
    valueAnimator.start()
}

注意:上边改变translationY的值,对应的控件的坐标参数:左上角xy(3.0之后增加的坐标)的坐标和translationY会改变,而对应的left,top,right,bottom的值并不会改变,(这些位置都是相对于父容器坐标而言)它们之间的对应公式:

x=left+translationX

y=top+translationY

ValueAnimator.oFloat(float values)

ValueAnimator.ofInt()ValueAnimator.oFloat()仅仅只是在估值器上的区别:(即如何从初始值 过渡 到结束值)

  • ValueAnimator.oFloat()采用默认的浮点型估值器 (FloatEvaluator)
  • ValueAnimator.ofInt()采用默认的整型估值器(IntEvaluator

在使用上完全没有区别,此处对ValueAnimator.oFloat()的使用就不作过多描述。

 ValueAnimator.ofObject()

将初始值以对象的形式过渡到结束值。

讲解之前先先说一下什么是插值器,什么是估值器。

插值器:决定值的变化模式(匀速、加速blabla)

估值器:决定值的具体变化数值

ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具备系统内置的估值器,即FloatEvaluator & IntEvaluator。

对于ValueAnimator.ofObject(),对对象的动画操作,系统无法知道如何从初始对象过度到结束对象,我们需自定义估值器(TypeEvaluator)来告知系统如何进行从初始对象过渡到结束对象的逻辑:

自定义估值器的步骤:

1.创建对象类型

2.实现TypeEvaluator和传递类型

3.复写evaluate方法,里边写变化逻辑

4.返回计算后的值

5.在需要获取值的地方手动获取并使用

类似这样的代码

// 实现TypeEvaluator接口,写出对象泛型
class ObjectEvaluator : TypeEvaluator<Bean> {

    override//复写evaluate()在evaluate()里写入对象动画过渡的逻辑
    fun evaluate(fraction: Float, startValue: Bean, endValue: Bean): Bean {
        // 参数说明
        // fraction:表示动画完成度(根据它来计算当前动画的值)
        // startValue、endValue:动画的初始值和结束值

        //......

        // 返回对象动画过渡的逻辑计算后的值
        return Bean()
    }
}

事例演示1:

步骤1:创建对象类型:

data class Bean(val x: Float, val y: Float) 

步骤2,3,4

class ObjectEvaluator : TypeEvaluator<Bean> {
    override//复写evaluate()在evaluate()里写入对象动画过渡的逻辑
    fun evaluate(fraction: Float, startValue: Bean, endValue: Bean): Bean {
        //计算出来变化的值,这个方法会自动循环被系统调用,直到达到设置的结束值为止
        val newX = startValue.x + fraction * (endValue.x - startValue.x)
        val newY = startValue.y + fraction * (endValue.y - startValue.y)
        // 已经变化的值 = 变化率*总需要变化的区间
        //新值 = 开始值+已经变化的值
        return Bean(newX, newY)
    }
}

步骤5:

//开始对象
val startBean = Bean(0F, 0F)
//结束对象
val endBean = Bean(100F, 300F)
//传递自定义的估值器和开始对象和结束对象
val objectAnimator = ValueAnimator.ofObject(ObjectEvaluator(), startBean, endBean)
//设置动画时间
objectAnimator.setDuration(1000)
//通过回调获取值的变化,设置给控件的属性,来进行动画
objectAnimator.addUpdateListener {
    val bean = it.getAnimatedValue() as Bean
    Log.e("rrrrrrr", bean.toString())
    tvjump.translationX = bean.x
    tvjump.translationY = bean.y
}
//最后别忘了开启动画
objectAnimator.start()

从上面可以看出,其实**ValueAnimator.ofObject()**的本质还是操作 ** 值 **,只是是采用将 多个值 封装到一个对象里的方式 同时对多个值一起操作而已

事例演示2:自定义控件中动画获取估值器返回的值

class MyView : View {
    val paint: Paint//画笔
    var bean: Bean? = null//数据变化对象
    val RADIUS:Float = 50F//圆形的半径
    //构造方法
    constructor(context: Context?) : this(context, null)
    //构造方法
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        //抗锯齿
        paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //设置画笔颜色
        paint.color = Color.BLACK
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (bean == null) {
            bean = Bean(300F, 0F)
            //开启画开始的圆形
            canvas.drawCircle(bean!!.x , bean!!.y, RADIUS, paint)
            //传递自定义的估值器和开始对象和结束对象
            val objectAnimator = ValueAnimator.ofObject(ObjectEvaluator(), bean, Bean(500F, 300F))
            //设置动画时间
            objectAnimator.setDuration(1000)
            //获取数据变化回调
            objectAnimator.addUpdateListener {
                //从新赋值
                bean = it.getAnimatedValue() as Bean
                //刷新绘制
                invalidate()
            }
            objectAnimator.start()

        }else{
            canvas.drawCircle(bean!!.x , bean!!.y, RADIUS, paint)
        }

    }

}

ObjectAnimator类

继承自ValueAnimator类,即底层的动画实现机制是基于ValueAnimator类对值的操作,通过不断控制值的变化,再不断自动赋给对象的属性,从而实现动画效果。既然是继承关系,那么子类也有用父类手动去设置动画的方法,并且丰富了自动设置属性的方法

从上面的工作原理可以看出:ObjectAnimatorValueAnimator类的区别:

  • ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;
  • ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;

类似类似设置一个控件让他进行平移一段距离如果用ValueAnimtor去设置,需要代码如下:

ValueAnimtor写法:

val valueAnimator = ValueAnimator.ofFloat(100F)
valueAnimator.setDuration(500)
valueAnimator.addUpdateListener {
    var newValue = it.getAnimatedValue() as? Float
    tvjump.translationY = newValue!!
}
valueAnimator.start()

ObjectAnimator写法

// 创建动画实例并参数设置,系统内置的浮点型估值器FloatEvaluator
val anim = ObjectAnimator.ofFloat(tvjump, "translationX", 100F)
//参数1:需要动画的对象(属性属于的哪个对象)
//参数2:需要改变的属性key
//参数3:float ....values:动画初始值 & 结束值(不固定长度)
// 开启动画
anim.start()

很明显objectAnimator少了监听,封装隐藏了变换的细节,和赋值的细节。

同View动画一样系统还给我们提供了旋转,平移,缩放,透明度的属性值供我们使用:

1.透明度

val anim = ObjectAnimator.ofFloat(tvjump, "alpha", 100F)
anim.setDuration(1000)
anim.start()

2.旋转

val anim = ObjectAnimator.ofFloat(tvjump, "rotation", 100F)
anim.setDuration(1000)
anim.start()

3.平移

val anim = ObjectAnimator.ofFloat(tvjump, "translationX", 100F)
anim.setDuration(1000)
anim.start()

4.缩放

val anim = ObjectAnimator.ofFloat(tvjump, "scaleX", 100F)
anim.setDuration(1000)
anim.start()

对于透明度,平移,缩放,旋转还可以设置一下属性: Alpha ,TranslationX ,TranslationY , ScaleX , ScaleY , Rotation ,  RotationX , RotationY 。

除了系统给我们的一些常用的动画属性之外,我们还可以自己去自定义属性动画,这是由自定属性动画的原理决定。

需要注意:采用**ObjectAnimator** 类实现动画效果,那么需要操作的对象就必须有该属性的**set() & get()方法 。**

回顾一下之前的知识,并利用之前的知识去让一个控件去同时去做两个动画:

如果有一个需求是让一个控件同时去做平移个旋转组合动画我们可以这样写:

使用ValueAnimator:

val tran = ValueAnimator.ofFloat(300F)
tran.duration = 1000
val roat = ValueAnimator.ofFloat(180F)
roat.duration = 1000
tran.addUpdateListener { tvjump.translationY= it.getAnimatedValue() as Float }
roat.addUpdateListener { tvjump.rotation = it.getAnimatedValue() as Float }
tran.start()
roat.start()

或者用ofObject把多个数据当成对象去处理,然后自定义估值器

使用ObjectAnimator:

val tranY = ObjectAnimator.ofFloat(tvjump, "translationY", 300F)
val roatall = ObjectAnimator.ofFloat(tvjump, "rotation", 180F)
tranY.start()
roatall.start()

我们很明显就会发现有很多重复的代码,当然google工程师同样能发现这个问题,类似View的组合动画有AnimationSet类,同样的属性动画也有对应的AnimatorSet类。

AnimatorSet

专门用在组合属性动画的类

**我们知道之前View动画要设置动画的先后顺序,我们一般都是通过延迟时间,系统并没有给我们提供其他的控制方法。幸运的是,**AnimatorSet给我们提供了动画执行先后顺序的方法:

  • AnimatorSet.play(Animator anim) :要播放的动画

  • AnimatorSet.with(Animator anim) :将现有动画和传入的动画同时执行 

  • AnimatorSet.after(Animator anim) :将现有动画插入到传入的动画之后执行 

  • AnimatorSet.before(Animator anim) : 将现有动画插入到传入的动画之前执行

  • AnimatorSet.after(long delay) :将现有动画延迟x毫秒后执行

play(a1).with(a2) a1和a2同时开始

play(a1).before(a2) 先a1后a2

play(a1).after(a2) 先a2后a1

顺序连接

上边的代码我们可以通过AnimatorSet组合到一起去执行:

val tran = ValueAnimator.ofFloat(0F, 300F)
val roat = ValueAnimator.ofFloat(0F, 180F)
tran.addUpdateListener { tvjump.translationY = it.getAnimatedValue() as Float }
roat.addUpdateListener { tvjump.rotation = it.getAnimatedValue() as Float }
val animatorSet = AnimatorSet()
animatorSet.play(tran).with(roat)
animatorSet.duration=1000
animatorSet.start()

或者这样写:

val tranY = ObjectAnimator.ofFloat(tvjump, "translationY", 300F)
val roatall = ObjectAnimator.ofFloat(tvjump, "rotation", 180F)
val animatorSet = AnimatorSet()
animatorSet.play(tranY).with(roatall)
animatorSet.duration = 1000
animatorSet.start()

动画监听

所有属性动画都可以设置关键帧的监听,因为他在基类中:

animatorSet.addListener(object:Animator.AnimatorListener{
    override fun onAnimationRepeat(animation: Animator?) {
    
    }
    override fun onAnimationEnd(animation: Animator?) {

    }
    override fun onAnimationCancel(animation: Animator?) {

    }
    override fun onAnimationStart(animation: Animator?) {

    }
})

有些时候我们并不需要监听动画的所有时刻,这个时候我们可以使AnimatorListenerAdapter来解决不必要的重写问题:

animatorSet.addListener(object : AnimatorListenerAdapter() {

    override fun onAnimationStart(animation: Animator?) {

    }
})

小结:

  • 属性动画的本质是对值的变换的操作,然后赋值给属性。

  • ValueAnimator手动赋值,可以自定义估值器对自定义的对象操作值的变化

  • ObjectAnimator自动赋值,隐藏实现细节

  • AnimatorSet组合动画,提供同时,延迟,之前,之后执行的方法

  • 听动画执行的特殊帧状态addListener,所有属性动画都有这个监听

  • 听动画所有帧的状态,只有ValueAnimator或者继承ValueAnimator的类才有这个监听

  • 属性动画在页面关闭的时候应该停止动画,防止内存泄漏

常见问题:

OOM问题:发生在帧动画的使用中,尽量少用帧动画

内存泄漏问题:无限循环属性动画有可能出现内存泄漏,需要在页面关闭的时候取消动画

setVisibility(View.GONE)失效:出现在View动画中,这个时候需要view.clearAnimation()清除View动画

拓展思考

既然属性动画是对值的操作,那么我们能不能用循环去代替值的变换呢?

不能!!!

一定不能!!!

原因:

for循环是没有时间概念的,而属性动画是有时间概念!

比如我们要画一条动态的直线动画,如果创建一个属性,并且写了set方法如下,

private var progress = 0
    set(value) {
        field = value
        invalidate()
    }

这个时候循环去改变这个值,是没有动画的,(除非我们每隔一段时间改变一个值,比如写个定时器去改变)

而我们用属性动画去改变,它是有动画的!

所以,属性动画准确的说是:具有时间概念的,对值改变的操作!