Android属性动画学习

1,988 阅读33分钟

本篇笔记主要是学习Android属性动画的一些基本使用,根据官方文档属性动画概览部分进行学习,基本就是照搬这一块的文档,想要看原文档的可以直接点击连接进入。

概述

属性动画是一个很强劲的框架,可以为几乎任何内容添加动画。开发者可以定义一个随时间更改任何对象属性的动画,无论其是否绘制到屏幕上。属性动画会在指定时长内更改属性的值。借助属性动画,可以定义动画的以下特性:

  • 时长: 可以指定动画的时长,默认为300毫秒
  • 时间插值: 可以指定如何根据动画的当前已播放时长来计算属性的值
  • 重复计数和行为: 可以指定是否在某个时长结束后重复播放动画以及重复播放动画多少次。还可以指定是否要反向播放动画。如果将其设置为反向播放,则会先播放动画,然后反向播放动画,直到达到重复次数
  • Animator集: 可以将动画分成多个逻辑集,它们可以一起播放,按顺序播放或者在指定的延迟时间后播放
  • 帧刷新延迟: 可以指定动画帧的刷新频率。默认设置为每10毫秒刷新一次,但应用刷新帧的速度最终取决于整个系统的繁忙程度以及系统为底层计时器提供服务的速度。

API概览

属性动画的大多数API都i可以在android.animation包中找到,其中用到的一些插值器的API则可以在android.view.animation包中找到。

Animator及其子类

Animator类提供了创建动画的基本结构,但是通常不会直接使用此类,因为它只提供了很少的功能,这些功能必须经过扩展才能全面支持为值添加动画效果,下表描述了扩展了Animator类的子类:

说明
ValueAnimator继承自Animator,属性动画的主计时引擎,它也可计算要添加动画效果的属性的值,具有计算动画值所需的所有核心功能,同时包含每个动画的计时详情,有关动画是否重复播放的信息,用于接收更新事件的监听器以及设置待评估自定义类型的功能。为属性添加动画效果分为两个步骤:计算添加动画效果之后的值,以及对要添加动画效果的对象和属性设置这些值。ValueAnimator不会执行第二个步骤,因此,我们必须监听ValueAnimator计算的值的更新情况,并使用自己的逻辑修改要添加动画效果的对象
ObjectAnimator继承自ValueAnimator,用于设置目标对象和对象属性以添加动画效果。此类会在计算出动画的新值之后相应地更新属性。在大多数情况下都可以使用此类,因为它可以极大地简化对目标对象的值添加动画效果这一过程。不过,有时候我们仍然可能需要使用ValueAnimator,因为ObjectAnimator存在其它一些限制,例如要求目标对象具有特定的访问器方法
AnimatorSet继承自Animator,此类提供一种将动画分组在一起的机制,以使它们彼此相对运行。我们可以将动画设置为一起播放,按顺序播放或者在指定的延迟时间后播放。

评估程序TypeEvaluator

评估程序负责告知属性动画如何计算指定属性的值,它们使用由Animator类提供的计时数据(既动画的起始值和结束值),并根据这些数据计算属性添加动画效果之后的值,属性动画系统可以提供以下评估程序:

类/接口说明
IntEvaluator用于计算int属性的值的默认评估程序,实现了TypeEvaluator
FloatEvaluator用于计算float属性的值的默认评估程序,实现了TypeEvaluator
ArgbEvaluator用于计算颜色属性的值的默认评估程序,实现了TypeEvaluator
TypeEvaluator这个接口可以创建我们自己的评估程序,如果要添加动画效果的对象属性不是int,float或颜色,那么我们就必须实现TypeEvaluator接口,才能指定如何计算对象属性添加动画效果之后的值。如果我们也想以不同于欧仁行为的方式处理int,float或颜色,也可以为这些类型的值指定自定义的TypeEvaluator

时间插值器Interpolator

以下类是系统提供的一些插值器:

类/接口说明
AccelerateDecelerateInterpolator该插值器的变化率在开始和结束时缓慢但在中间会加快
AccelerateInterpolator该插值器的变化率在开始时较为缓慢,然后加快
AnticipateInterpolator该插值器先反向变化然后再急速正向变化
AnticipateOvershootInterpolator该插值器先反向变化,再急速正向变化,然后超过定位值,最后返回到最终值
BounceInterpolator该插值器的变化会跳过结尾处
CycleInterpolator该插值器的动画会在指定数量的周期内重复
DecelerateInterpolator该插值器的变化率开始很快,然后减速
LinearInterpolator该插值器的变化率恒定不变
OvershootInterpolator该插值器会急速正向变化,再超出最终值,然后返回
TimeInterpolator通过该接口来实现自定义的插值器

使用ValueAnimator添加动画效果

通过上面的概览其实我们也可以了解到,属性动画其实就是针对某一个对象的属性的值的改变,注意这里是值的改变,并不一定会反映到View或者屏幕上,也就是说我们可以设置一个属性动画,让它执行,但是用户可能什么都看不到。所以这里对于属性动画,首先要做到的就是将动画与View分离开来,动画仅仅是一系列值的变化,当我们把这一系列值设置到某个属性上的时候,这个值就会发生改变,如果恰巧这个属性是某一个View的属性,那么此时才会观察到View的变化。

下面是使用ValueAnimator创建的一个动画:

    //使用ValueAnimator添加动画效果
    private fun createValueAnimator() {
        ValueAnimator.ofInt(0, 100).apply {
            duration = 10 * 1000
            this.addUpdateListener {
                Logs.e("动画执行中:${it.animatedValue}")
            }
            start()
        }
    }

可以看到,我们使用ValueAnimator.ofInt创建了一个动画,动画的数值在10秒的时间从0改变到100,然后调用start()方法开始执行动画,同时我们添加了一个动画数值改变的监听事件,打印出当前变化的动画数值,最终会出现如下的打印信息:

    ……
 动画执行中:98
 动画执行中:98
 动画执行中:99
 动画执行中:99
 动画执行中:100

ValueAnimator除了提供上面的ofInt(int... values)外,同时还提供了ofFloat(float... values),ofArgb(int... values),ofPropertyValuesHolder(PropertyValuesHolder... values)以及ofObject(TypeEvaluator evaluator,Object... values)这些方法来针对不同的数据类型使用不同的方法。

比如针对上面的ofInt(0,100)来说,我们使用ofObject也能达到相应的效果:

    private fun createValueAnimatorOfObject() {
        ValueAnimator.ofObject(object : TypeEvaluator<Int> {
            override fun evaluate(fraction: Float, startValue: Int?, endValue: Int?): Int {
                Logs.e("startValue: $startValue, endValue: $endValue")
                if (startValue == null)
                    return 0
                if (endValue == null)
                    return 0
                return (fraction * (endValue - startValue)).toInt()
            }

        }, 0, 100).run {
            duration = 3 * 1000
            start()
        }
    }

通过上面的代码我们就可以创建和之前使用ValueAnimator.ofInt创建出来的动画效果一样的动画,只是在使用ValueAnimator.ofObject的时候需要传入一个评估程序,而这里的评估程序就是把IntEvaluator里面的代码直接取过来的。当然,直接使用IntEvaluator也是没有任何问题的:

        ValueAnimator.ofObject(IntEvaluator(), 0, 100).run {
            duration = 3 * 1000
            addUpdateListener {
                Logs.e("动画执行中:${it.animatedValue}")
            }
            start()
        }

在之前关于ValueAnimator的简介中就提到过,ValueAnimator只能提供动画数值的改变,至于动画数值改变之后的操作则需要我们自己去处理,上面的代码中我们都是直接将数据打印出来,下面我们可以通过将数据变化之后的结果和View的某一个属性关联就可以看到明显的动画效果了,比如下面的程序演示了当动画数值变换之后我们将一个Button的左右Margin进行改变:

    //将ValueAnimator关联到View的Margin属性
    private fun createValueAnimatorToMargin() {
        val params =
            mBinding.btnUseValueAnimatorToMargin.layoutParams as ViewGroup.MarginLayoutParams
        ValueAnimator.ofInt(params.leftMargin, params.leftMargin + 100).run {
            duration = 3 * 1000
            addUpdateListener {
                params.leftMargin = (it.animatedValue as Int)
                params.rightMargin = (it.animatedValue as Int)
                mBinding.btnUseValueAnimatorToMargin.layoutParams = params
            }
            start()
        }
    }

在上面的代码中,我们首先获取到ViewmarginLeft属性的值作为开始值,然后使用marginLeft + 100作为结束值,启动动画后将动画执行过程中新的值设置给View,然后就可以看到View的左右Margin不断变大的动画效果。

使用ObjectAnimator添加动画效果

ObjectAnimator是上面提到的ValueAnimator的子类,相比于ValueAnimatorObjectAnimator帮我们自动添加了对动画执行过程中给属性赋值的这一过程,这样我们就不需要通过监听动画更新事件ValueAnimator.AnimatorUpdateListener来手动给属性赋值了。

下面的代码演示了将一个Button在水平位置上移动100个像素的动画效果:

    //使用ObjectAnimator移动View
    private fun createObjectAnimatorToMove() {
        ObjectAnimator.ofFloat(
            mBinding.btnUseObjectAnimatorToMove,
            "translationX",
            0f, 100f
        ).run {
            duration = 5 * 1000
            start()
        }
    }

在上面的代码中,我们通过ObjectAnimator.ofFloat来设置了一个动画,其中第一个参数表示我们要对哪个对象设置动画,第二个参数表示我们要对这个对象的哪个属性设置动画,也就是动画执行过程中要改变值的属性,最后设置动画开始和结束的值分别是0100,运行上面的程序,就可以看到设置的那个View向右移动了100个像素。

ValueAnimator一样,ObjectAnimator也提供了ofIntofArgb,ofObject系列方法来针对不同的应用场景使用不同的方法。

需要注意的是,为了能够使ObjectAnimator正确更新属性,我们需要执行以下操作:

  • 要添加动画效果的对象必须具有set<PropertyName>()形式的setter函数。由于ObjectAnimator会在动画过程中自动更新属性,它必须能够使用此setter方法访问该属性。例如,如果属性名称为foo,那么则需要使用setFoo()方法。如果此setter方法不存在,那么有三种选择解决:

    • 如果我们有这个类的权限,那么就在这个类中将这个setter方法补充上去即可
    • 如果没有权限,则可以针对这个类做一个封装类,在封装类中使用有效的setter方法接收值并将其转发给原始对象
    • 改用ValueAnimator
  • 在上面的代码中,我们为ofFloat()参数指定了开始值和结束值,不过这里也可以接收一个值,如果只指定一个值,则系统会假定这是动画的结束值。这样,系统就会自动通过getter方法获取这个属性的原始值来作为开始值。同样的,getter函数也必须使用get<PropertyName>()形式,如属性名为foo,则需要使用getFoo()方法。

  • 要添加动画效果的属性的gettersetter方法的操作对象必须与我们为ObjectAnimator指定的起始值和结束值的类型相同,例如构建下面的ObjectAnimator,则必须具有targetObject.setPropName(float)targetObject.getPropName() -> float

    ObjectAnimator.ofFloat(targetObject,"propName",1f)
  • 根据要添加动画效果的属性或对象,我们可能需要对视图调用invalidate()方法,以强制屏幕使用添加动画效果之后的值重新绘制自身。我们可以在onAnimationUpdate回调中执行此操作。例如:如果为可绘制对象的颜色属性添加动画效果,则仅当该对象重新绘制自身时,屏幕才会刷新。视图的所有属性setter(例如setAlpha()setTranslationX())都会使视图失效,因此,在使用新值调用这些方法时,我们无需使试图失效。

使用AnimatorSet编排多个动画

在许多情况下,我们对某一个对象设置的动画都不是单一的,某一个对象在一段时间内的变化可能是多个维度上面的,比如在一段时间内,一个View可能会在margin变化的同时alpha也在变化。在这种情况下,我们就可以使用AnimationSet来编排多个动画,从而实现我们需要的动画效果。

需要注意的是:AnimatorSet直接继承自Animator,这也就意味着ValueAnimator类中的AnimatorUpdateListener不能用在这里。

下面的代码演示了将两个动画编排在一起的效果:

首先创建两个动画

        //首先创建第一个动画,View的alpha将会从1变为0
        val targetView = mBinding.btnUseAnimatorSet
        val alphaAnimator = ObjectAnimator.ofFloat(targetView,"alpha",1f,0f)
        //第二个动画,将View向右移出窗口
        val params = targetView.layoutParams as ViewGroup.MarginLayoutParams
        val moveX = targetView.measuredWidth + params.rightMargin
        val translationAnimator = ObjectAnimator.ofFloat(targetView,"translationX",0f,moveX.toFloat())

创建AnimatorSet并设置两个动画同时执行

        //创建AnimationSet并设置两个动画同时执行
        AnimatorSet().apply {
            //设置每个动画执行时间为10秒
            duration = 10 * 1000
            //两个动画同时执行
            this.play(alphaAnimator).with(translationAnimator)
            start()
        }

这里需要注意的是在AnimatorSet中设置的duration,动画的持续时间是对AnimatorSet中所包含的每一个动画设置的时间,如上面设置的duration时间为10秒

先播放透明度的动画,再播放移动的动画:

 this.play(translationAnimator).after(alphaAnimator)

上面的代码可以理解为:播放translationAnimator在播放alphaAnimator之后,那么也就是先播放alphaAnimator,后播放tranlationAnimator

上面是使用after首先动画的先后顺序,使用before也可以做到上面的效果:

this.play(alphaAnimator).before(translationAnimator)

上面的这段代码可以理解为:播放alphaAnimator在播放translationAnimator之前,说成人话也就是:先播放alphaAnimator,再播放translationAnimator

这里需要注意的是:对于AnimatorSet中的duration的设置,分为以下情况:

  • AnimatorSet中设置了duration:

    此时不管AnimationSet中所包含的Animator是否设置单独的持续时间,都是使用AnimationSet中设置的这个duration。比如上面的两个动画,即使我们在alphaAnimatortranslationAnimator中都设置了它们单独的duration,最后执行的时候也是使用AnimatorSet中设置的duration

    在这种情况下,仍然分为两种情况:

    • 如果多个个动画同时执行,也就是调用的是play().with(),那么动画总的持续时间就是设置的duration的时间
    • 如果多个动画分开执行,也就是调用的是play().before()或者play().after(),那么这里设置的duration就是每一个动画持续时间。

    比如上面的duration为10秒钟,在调用play().with()时就是这两个动画共用了10秒钟,在调用play().after()或者play().before()时,整个动画的持续时间就变为了20秒

  • Animator中没有设置duration

    此时调用的就是每一个Animator中单独设置的duration:

    • 对于同时执行的动画,也就是通过play().with()执行的动画,总的持续时间是所有动画中持续时间最长的那一个
    • 对于有先后顺序的动画,则总的持续时间应该是所有动画持续时间的总和。

所以这里我个人感觉应该在每一个单独执行的Animator中设置不同的duration,这样可以精确地控制每一个动画的执行时间,不在AnimatorSet中设置duration

动画监听器

可以使用下述的动画监听器来监听动画播放过程中的重要事件:

  • Animator.AnimatorListener

    • onAnimationStart:在动画开始播放时调用
    • onAnimationEnd: 在动画结束播放时调用
    • onAnimationRepeat: 在动画重复播放时调用
    • onAnimationCancel: 在动画取消播放时调用。取消的动画也会调用onAnimationEnd,无论动画以何种方式结束。
  • ValueAnimator.AnimatorUpdateListener

    onAnimatorUpdate: 对动画的每一帧调用。监听这个事件就可以使用ValueAnimator在动画播放期间生成的计算值。这个事件中会传递进来ValueAnimator对象,我们可以在这个对象中直接获取到这个值。

有时候我们并不需要实现Animator.AnimatorListener所有方法,那么则可以扩展AnimatorListenerAdapter类,而非实现接口,AnimatorListenerAdapter类提供了方法的空实现,我们只需要实现自己关注的方法的即可,如下所示:

    addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationStart(animation: Animator?) {
            super.onAnimationStart(animation)
            Logs.e("开始播放动画")
        }

        override fun onAnimationEnd(animation: Animator?) {
            super.onAnimationEnd(animation)
            Logs.e("动画结束")
        }
                
    })

ViewGroup对象的布局更改添加动画效果

属性动画系统提供对ViewGroup对象的更改添加动画效果的功能,还可轻松为视图对象本身添加动画效果。

我们可以使用LayoutTransition类为ViewGroup内的布局更改添加动画效果。当我们向ViewGroup添加视图或删除其中的视图时,或者我们使用VISIBLE,INVISIBLE,GONE来调整视图的visibility属性时,这些视图可能会经历出现和消失的动画。向ViewGroup添加视图或者删除其中的视图时,其中剩余的视图还可能以动画形式移动到新位置。我们可以调用setAnimator()并使用以下任一LayoutTransition常量传入Animator对象,从而在LayoutTransition对象中定义动画:

  • APPEARING:该标记表示动画在容器中出现的项上执行
  • CHANGE_APPEARING:该标记表示动画在因某个新项目在容器中出现而变化的项上运行
  • DISAPPEARING:该标记表示动画在从容器中消失的项上运行
  • CHANGE_DISAPPEARING:该标记表示动画在因某个项从容器中消失而变化的项上运行。

我们可以为这四类事件定义自己的自定义动画,从而自定义布局转换的外观,或者告诉动画系统使用默认动画。

    <LinearLayout
            android:id="@+id/test_view_grouo_animator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:animateLayoutChanges="true"
            />

在上面的代码中,我们在LinearLayout标签的属性中指定animatrLayoutChanges="true",此时当我们向这个Layout中添加或者删除View的时候本身就已经能够使用动画效果了。如下所示:

"系统本身的动画效果"

现在我们可以设置自定义的动画效果,我们设置添加View的时候从屏幕的右边滑动到左边,删除View的时候从屏幕的左边滑动到右边,代码如下:

    private val mViewGroupTransition by lazy {
        LayoutTransition()
    }

    //添加View或者View由不显示到显示的时候View进入的动画
    private val mViewInAnimator by lazy {
        val width = mBinding.testViewGrouoAnimator.measuredWidth
        ObjectAnimator.ofFloat(null, "translationX", width.toFloat(), 0f)
    }

    //删除View或者View由显示到不显示的时候执行的动画
    private val mViewOutAnimator by lazy {
        val width = mBinding.testViewGrouoAnimator.measuredWidth
        ObjectAnimator.ofFloat(null, "translationX", 0f, width.toFloat())
    }

    //为ViewGroup的布局更改添加动画
    private fun addAnimatorToViewGroup() {
        mViewGroupTransition.setAnimator(LayoutTransition.APPEARING, mViewInAnimator)
        mViewGroupTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, mViewInAnimator)
        mViewGroupTransition.setAnimator(LayoutTransition.DISAPPEARING, mViewOutAnimator)
        mViewGroupTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, mViewOutAnimator)
        mBinding.testViewGrouoAnimator.layoutTransition = mViewGroupTransition
        //待添加的View
        val button = Button(this)
        button.layoutParams = LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT
        )
        button.text = "待添加的View"
        button.setOnClickListener {
            mViewOutAnimator.target = it
            mBinding.testViewGrouoAnimator.removeView(it)
//            mViewOutAnimator.target = it
//            it.visibility = View.GONE
        }
        mViewInAnimator.target = button
        mBinding.testViewGrouoAnimator.addView(button)
    }

通过上面的代码我们就自定义了ViewGroup添加或者删除View的时候执行的动画,分为以下几步:

  • 首先创建LayoutTransition对象,用于当ViewGroup添加或者删除View时执行动画的载体
  • 分别创建子View进入和退出的动画
  • 将动画根据类型设置到LayoutTransition
  • LayoutTransition设置到ViewGroup
  • 由于在创建动画的时候我们并不知道要执行动画的View,所以每当要添加或者删除View的时候都需要设置target属性来指定要执行动画的对象

自定义动画后的效果如下:

"自定义ViewGroup添加和删除View的动画"

为视图状态更改添加动画效果

通过StateListAnimator类,可以定义在视图状态更改时运行的动画效果。此对象充当Animator对象的封装容器,只要指定的视图状态发生更改(例如按下或者聚焦),就会调用该动画。

可以使用根<selector>元素和子<item>元素在XML资源中定义StateListAnimator,每个元素都指定一个由StateListAnimator类定义的不同状态视图,每个item都包含一个AnimatorSet的定义。

动画资源

由于在上面为视图状态更改添加动画效果的时候我们需要从XML资源文件中添加动画,所以这里先查看如何在XML文件中定义动画资源。

在XML文件中定义属性动画资源

定义的动画资源的XML文件的位置:

res/animator/filename.xml

同时使用这个文件的文件名作为资源ID。编译后的资源数据类型为ValueAnimator,ObjectAnimator或者AnimatorSet

定义完动画资源之后,通过如下方式引用:

  • 在Java或者Kotlin代码中:R.animator.filename
  • 在XML文件中:@[package:]animator/filename

定义动画资源的语法如下:

    <set
        android:ordering=["together" | "sequentially"]
    >

        <objectAnimator
            android:propertyName="string"
            android:duration="int"
            android:valueFrom="float | int | color"
            android:valueTo="float | int | color"
            android:startOffset="int"
            android:repeatCount="int"
            android:repeatMode=["repeat" | "reverse"]
            android:valueType=["intType" | "floatType"]
        />
        <animator
            android:duration="int"
            android:valueFrom="float | int | color"
            android:valueTo="float | int | color"
            android:startOffset="int"
            android:repeatCount="int"
            android:repeatMode=["repeat" | "reverse"]
            android:valueType=["intType" | "floatType"]
        />
    </set>

创建的这个动画资源文件必须有一个根元素,可以是<set>,<objectAnimator>或者animator。也可以将动画元素(包括其它<set>元素)组合到<set>元素中,也就是说<set> 内部可以包含上面三种动画类型。

上面动画资源语法的说明如下:

元素名称必需说明可选字段可选字段说明
<set>-容纳其它动画元素(<objectAnimator>,<animator><set>)的容器,代表的是AnimatorSet--
android:ordering指定此集合中动画的播放顺序together(默认) sequentiallysequentially(依序播放此集合中的动画) together(同时播放此集合中的动画)
<objectAnimator>-在特定的一段时间内为对象的特定属性创建动画,代表的是ObjectAnimator,由于在<objectAnimator>中不能设置target属性,所以需要首先通过loadAnimator()来加载此动画XML资源,然后再调用setTarget()来设置目标对象--
<android:propertyName>值为字符串,表示要添加动画的属性对象,通过名称引用,例如要为View指定alpha或者backgroundColor,则可以在此处填入这两个数据string需要设置的target目标对象包含此string设置的那个属性,并提供相应的setter,如果没有设置初始值则还需要提供相应的getter
android:valueTo动画属性的结束值,如果是颜色值则以六位十六进制数字表示(例如#333333)int或者float或者color颜色值以六位十六进制数表示
valueFrom动画属性的开始值,如果没有指定,则动画将从属性的getter方法获取值int,float或者color颜色以六位十六进制数表示
android:duration动画的持续时间,单位为毫秒,默认300毫秒int-
android:startOffset调用start()后动画延迟的毫秒数int整数
android:repeatCount动画重复次数,如果设置为-1,表示无限次重复,也可以设置为正整数,如果设置为1表示动画在初次播放之后再重复播放一次,因此动画总共播放两次,默认值为0,表示不重复int-
android:repeatMode动画播放到结尾处的行为。android:repeatCount的值必须设置为正整数或者-1此属性才有效,此属性设置为reverse可以让动画在每次迭代时反向播放,设置为repeat则可以让动画每次从头开始循环播放reverserepeat-
valueType设置动画值的类型,如果值为颜色,则不需要指定此属性intType floatType(默认)-
<animator>-在指定时间内执行的动画,代表ValueAnimator,此标签下可用的属性和<objectAnimator>下可用的属性几乎一样,只是没有<android:propertyName>属性--

下面是一个例子,一个View先移动,然后背景颜色会变化:

<?xml version="1.0" encoding="utf-8"?>
<!--顺序执行-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
        android:ordering="sequentially"
        >
    <!--位置移动的动画-->
    <!--同时执行-->
    <set
            android:ordering="together"
            >

        <objectAnimator
                android:propertyName="translationX"
                android:valueTo="200"
                android:duration="500"
                android:valueType="floatType"
                />
        <objectAnimator
                android:propertyName="translationY"
                android:valueTo="200"
                android:duration="500"
                android:valueType="floatType"
                />

    </set>
    <!--颜色变化的动画-->
    <objectAnimator
            android:propertyName="backgroundColor"
            android:valueFrom="#ff0000"
            android:valueTo="#00ff00"
            android:duration="1000"
            />
</set>

在上面的动画资源文件中,定义了3个属性动画,分别是x轴移动,y轴移动和背景颜色由红色变为绿色,其中x轴移动和y轴移动同时执行,移动动画执行完成后背景颜色再开始变化。

定义了上面的动画资源之后,我们就可以使用上面的动画了,下面是在Activity中加载这个动画资源并设置给一个Button,如下所示:

    //从XML文件中添加一个动画资源并设置到View上
    private fun loadAnimatorWithXML() {
        AnimatorInflater.loadAnimator(this, R.animator.animator_view_translation_alpha).apply {
            setTarget(mBinding.btnLoadAnimatorWithXml)
            start()
        }
    }

动画执行效果如下:

从XML文件中加载动画资源并设置到View上

在XML中定义视图动画

上面介绍了如何在资源文件中定义属性动画资源,我们还可以定义视图动画的资源文件,视图动画框架可以支持补间动画和逐帧动画,两者都可以在XML中声明。

补间动画

补间动画用于对图形执行旋转,淡出,转移和拉伸等转换。在XML中定义的补间动画的资源的文件存放在res/anim/filename.xml,同时使用该文件名作为资源ID。在XML中定义的视图动画编译后的数据类型为指向Animation的资源指针。

在Java或者Kotlin中引用视图动画资源时使用R.anim.filename,在XML文件中引用的时候使用@[package:]anim/filename

定义视图动画的语法如下:

 <set
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"]
    >

    <alpha
        android:fromAlpha="float"
        android:toAlpha="float"
    />
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float"
    />
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float"
    />
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float"
    />

    <set>

        ......

    </set>

 </set>

我们定义的这个视图资源文件必须具有一个根元素,可以是<alpha>,<scale>,<translate>,<rotate>或者包含一组(或多组)其它动画元素(甚至是嵌套<set>元素)的<set>元素。

以上元素以及其可以使用的属性说明如下:

  1. <set> -- 容纳其它动画元素的容器,代表AnimationSet
属性说明
android:interpolator插值器资源,指要应用于动画的Interpolator。该值必须是对指定插值器的资源的引用(而不是插值器类名称)。这里可以使用平台提供的默认插值器资源,也可以创建自定义的插值器资源
android:shareInterpolator布尔值,如果要在所有元素中使用同一插值器,则为true
  1. alpha -- 淡入或者淡出动画,代表AlphaAnimation
属性说明
android:fromAlpha浮点数,起始不透明度,0.0表示透明,1.0表示不透明
android:toAlpha浮点数,结束不透明度,0.0表示透明,1.0表示不透明
  1. scale -- 大小调整动画,代表ScaleAnimation

这个动画可以通过指定pivotXpivotY,来指定图片向外(或向内)扩展的中心点,例如,如果这两个值为0,0(左上角),则所有扩展均向右下方进行。

属性说明
android:fromXScale浮点数,起始X尺寸偏移,其中1.0表示不变
android:toXScale浮点数,结束X尺寸偏移,1.0表示不变
android:fromYScale浮点数,起始Y尺寸偏移,1.0表示不变
android:toYScale浮点数,结束Y尺寸偏移,1.0表示不变
android:pivotX浮点数,在对象缩放时要保持不变的X坐标
android:pivotY浮点数,在对象缩放时要保持不变的Y坐标
  1. translate -- 垂直或水平移动,代表TranslateAnimation

其属性的值可以采用以下三种格式之一:

  • 从-100到100以"%"结尾的值,表示相对于自身的百分比
  • 从-100到100的以"%p"结尾的值,表示相对于其父项的百分比
  • 不带后缀的浮点数,表示绝对值
属性说明
android:fromXDelta浮点数或百分比。起始X偏移,表示方式:相对于正常位置的像素数(例如"5"),相对于自身宽度的百分比(例如"5%"),相对于父项宽度的百分比(例如"5%p")
android:toXDelta浮点数或百分比。结束X偏移,表达方式与上一项相同
android:fromYDelta浮点数或百分比。起始Y偏移,表达方式和上一项相同
android:toYDelta浮点数或百分比。结束Y偏移,表达方式和上一项相同
  1. rotate -- 旋转动画,代表RotateAnimation
属性说明
android:fronDegrees浮点数,起始角度位置,以度为单位
android:toDegrees浮点数,结束角度位置,以度为单位
android:pivotX浮点数或百分比。旋转中心的X坐标。表达方式:相对于对象左边缘的像素数(例如"5"),相对于对象左边缘的百分比(例如"5%"),相对于父级容器左边缘的百分比(例如"5%p")
android:pivotY浮点数或百分比。旋转中心Y的坐标,表达方式和上一项相同
  1. Animation类中提供的其它可公共使用的标签属性
属性说明
android:duration动画运行的时间,以毫秒为单位
android:fillAfter设置为true时,动画结束后将应用动画变换。默认值为false,如果fillEnabled未设置为trueView上未设置动画,则将fillAfter假定为true
android:fillBefore当设置为true或者未将fillEnabled设置为true时,将在开始动画之前应用动画转换。默认值是true
android:fillEnabled设置为true时,将会考虑fillBefore的值
android:repeatCount定义动画应重复多少次,默认值为0
android:repeatMode定义当动画到达终点且重复计数大于0或无限时的动画行为,默认值为重新启动
android:startOffset定义动画运行之前延迟的毫秒数
android:zAdjustment允许在动画期间调整要动画的内容的Z顺序,默认值是正常

下面的代码在资源文件中定义了一个动画资源:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="false"
        android:shareInterpolator="false">


    <translate
            android:duration="500"
            android:fromYDelta="100%p"
            android:toYDelta="0%" />
    
    <set
            android:fillAfter="false"
            android:interpolator="@android:anim/accelerate_decelerate_interpolator"
            android:shareInterpolator="true"
            android:startOffset="500">
        <!--变大的动画-->
        <scale
                android:duration="500"
                android:fromXScale="0.5"
                android:fromYScale="0.5"
                android:pivotX="0.2"
                android:pivotY="0.8"
                android:toXScale="1.5"
                android:toYScale="1.5" />

    </set>
    

</set>

在上面的动画资源文件中,首先定义了一个Y轴方向位移的动画,从父项的最底部移动到自身的最顶部,然后定义了一个缩放的动画,从1.5倍缩放到1.0倍。

定义完上面的动画资源之后,我们就可以在代码中使用上面的动画了:

    //从XML文件中添加一个视图动画资源并设置到View上
    private fun loadAnimationWithXML() {
        val animAll = AnimationUtils.loadAnimation(this, R.anim.anim_all)
        mBinding.btnLoadAnimationWithXml.startAnimation(animAll)
    }

动画执行的效果如下:

"加载资源文件中定义的视图动画"

插值器

关于插值器的内容之前已经了解过了,下面主要是总结插值器在资源文件中的引用路径:

插值器类资源ID描述
AccelerateDecelerateInterpolator@android:anim/accelerate_decelerate_interpolator开始和结尾慢,中间快
AccelerateInterpolator@android:anim/accelerate_interpolator先满后快
AnticipateInterpolator@android:anim/anticipate_interpolator方向先反后正
AnticipateOvershootInterpolator@android:anim/anticipate_overshoot_interpolator先反后正,超过定位值再返回
BounceInterpolator@android:anim/bounce_interpolator跳过结尾处
CycleInterpolator@android:anim/cycle_interpolator重复
DecelerateInterpolator@android:anim/decelerate_interpolator先快后慢
LinearInterpolator@android:anim/linear_interpolator线性
OvershootInterpolator@android:anim/overshoot_interpolator急速正向变化,再超出限定值,再返回

上面所写的这些插值器都可以在资源文件中通过android:interpolator来进行索引。

自定义插值器

如果对于上面列表中提到的插值器的效果不满意,则可以使用修改过的属性创建自定义插值器资源,为了使用自定义的插值器资源,则需要在XML文件中创建自己的插值器资源。文件位置为res/anim/filename.xml,在XML中定义自定义插值器的语法如下:

<interceptorName xmls:android="http://schemas.android.com/apk/res/android"
    android:attribute_name="value"
>

如果不应用任何属性,则表现方式和默认的表现方式相同.

上面的插值器类以及其所支持自定义的属性如下:

插值器资源支持修改的属性属性值介绍
accelerateDecelerateInterpolator-
accelerateInterpolatorfactor浮点数,加速率(默认为1)
anticipateInterpolatetension浮点数,要应用的张力(默认为2)
anticipateOvershootInterpolatortension extraTensiontension 浮点数,要应用的张力(默认为2) extraTension浮点数,张力乘以的倍数(默认1.5)
bounceInterpolator-
cycleInterpolatorcycles整数,循环次数,默认为1
decelerateInterpolatefacor浮点数,减速率,默认为1
linearInterpolator-
overshootInterpolatortension浮点数,要应用的张力,默认为2

下面使用一个自定义的插值器:

<overshootInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
        android:tension="10"
        >

</overshootInterpolator>

定义一个动画资源,使用这个插值器:

<scale xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="5000"
        android:fillAfter="true"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:interpolator="@anim/my_over_shoot_interpolator"
        android:toXScale="2.0"
        android:toYScale="2.0">

</scale>

加载这个动画资源并应用到View上:

    //使用自定义的插值器
    private fun useMyInterpolator() {
        val anim = AnimationUtils.loadAnimation(this, R.anim.view_scale)
        mBinding.btnLoadMyInterpolator.startAnimation(anim)
    }

最后执行效果如下:

使用自定义的插值器资源

逐帧动画

逐帧动画就是在XML文件中按顺序显示一系列图片的动画,文件定义在res/drawable/filename.xml,编译之后是指向AnimationDrawable的资源指针,语法如下:

    <animation-list 
        android:ontshot=["true" | "false"]
    >
        <item
            android:drawable="@[package:]drawable/drawable_resource_name"
            android:duration="integer"
        >
    </animation-list>

元素说明如下:

元素/属性名称说明
<animation-list>此元素必须是根元素,包含一个或多个<item>元素
oneshot布尔值,如果想执行一次动画,则为true,如果要循环播放动画,则为false
<item>单帧动画,必须是<animation-list>元素的子元素
android:drawable可绘制资源,要用于此帧的可绘制对象
android:duration整数,显示此帧的持续时间,以毫秒为单位

下面是定义的一个逐帧动画:

首先定义三个drawable资源

<shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle"
        >

    <solid
        android:color="#ff0000"
            />

</shape>

除此之外在定义两个资源,和上面的一样,只有颜色不一样,然后定义animation-list资源文件:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">

    <item
            android:drawable="@drawable/rect_solid_red"
            android:duration="1000" />
    <item
            android:drawable="@drawable/rect_solid_blue"
            android:duration="1000" />
    <item
            android:drawable="@drawable/rect_solid_green"
            android:duration="1000" />

</animation-list>

将上面的逐帧动画资源文件设置为Button的背景:

    <Button
            android:id="@+id/btn_load_animation_list_with_xml"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            style="@style/ButtonStyle"
            android:text="@string/load_animation_list_with_XML"
            android:layout_gravity="center"
            android:layout_marginBottom="20dp"
            android:onClick="doClick"
            android:background="@drawable/animation_list_view_bakground"
            />

点击按钮的时候启动这个动画:

    //加载逐帧动画并启动
    private fun loadAnimationListWithXML() {
        val backResource = mBinding.btnLoadAnimationListWithXml.background
        if (backResource is Animatable) {
            backResource.start()
        }
    }

最后的结果如下:

逐帧动画

为视图状态更改添加动画

现在我们已经了解了如何在XML文件中定义动画资源了,我们就可以在资源文件中为视图的状态更改添加一些动画效果。

可以使用根<selector>元素和子<item>元素在XML资源中定义StateListAnimator,每个元素都指定一个由StateListAnimator类定义的不同视图状态。每个<item>都包含一个属性动画集的定义。

下面的文件定义了一个状态列表Animator,可以在按下之后更改视图的x和y的比例,文件定义在res/drawable/animate_scale

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <set>
            <objectAnimator
                    android:propertyName="scaleX"
                    android:duration="200"
                    android:valueTo="1.5"
                    android:valueType="floatType"
                    />
            <objectAnimator
                    android:propertyName="scaleY"
                    android:duration="200"
                    android:valueTo="1.5"
                    android:valueType="floatType"
                    />
        </set>
    </item>

    <item android:state_pressed="false">
        <set>
            <objectAnimator
                    android:propertyName="scaleX"
                    android:duration="200"
                    android:valueTo="1"
                    android:valueType="floatType"
                    />
            <objectAnimator
                    android:propertyName="scaleY"
                    android:duration="200"
                    android:valueTo="1.0"
                    android:valueType="floatType"
                    />
        </set>
    </item>
</selector>

上面资源文件中定义的动画,就是当手指按下的时候View变大,手指抬起的时候变小的动画,定义完上面的动画资源之后,设置到ViewstateListAnimator属性中:

    <Button
            android:id="@+id/btn_use_state_list_animator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="@style/ButtonStyle"
            android:text="@string/use_state_list_animator"
            android:onClick="doClick"
            android:textAllCaps="false"
            android:stateListAnimator="@drawable/animator_scale"
            />

最终执行的效果如下:

stateListAnimator设置视图状态变更时的动画

另外,如果我们希望在代码中将动画资源设置给View,那么也可以使用AnimatorInflater。loadStateListAnimator()方法,然后使用View.setStateListAnimator()方法将Animator分配给相应的视图,如下所示:

    val stateListAnimator =
            AnimatorInflater.loadStateListAnimator(this, R.drawable.animator_scale)
    mBinding.btnUseStateListAnimator.stateListAnimator = stateListAnimator

注意:这里官方文档还提出了可以使用AnimatedStateListDrawable在状态更改间播放动画,而不是为视图的属性添加动画效果。但是个人感觉用处不大,暂时没有学习。

使用TypeEvaluator

TypeEvaluator是一个接口,接口中只有一个需要实现的方法public T evaluate(float fraction, T startValue, T endValue);这个方法的作用就是告诉你现在动画进行到了哪个程度,也就是第一个参数表示的值,一般情况下这个值处于0~1之间,但是由于收到插值器的影响,可能大于1。但是总的来说,这个方法告诉了你现在动画进行到了哪个程度,动画的开始值和结束值,拿到这三个值,需要我们返回当前这个阶段的实际值。我们之前使用ObjectAnimatorofIntofFloat等都是使用了相应的TypeEvaluator

下面是IntEvaluator中的源码:

public class IntEvaluator implements TypeEvaluator<Integer> {

    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

可以看到,这里的源码相对简单,就是根据动画已经进行的时间比例计算出当前的值。

系统已经为我们提供了IntEvaluatorFloatEvaluator,ArgbEvaluator等评估程序,但是有时候不能满足我们的需求,我们可以通过实现TypeEvaluator接口来实现自己的评估程序。

下面是我们自己实现的一个TypeEvaluator,这个TypeEvaluator可以对一个LinearLayout.LayoutParams进行处理:

    private fun useMyTypeEvaluator() {
        val marginParams =
            mBinding.btnUseMyTypeEvaluator.layoutParams as LinearLayout.LayoutParams

        val endParams = LinearLayout.LayoutParams(marginParams)
        endParams.leftMargin += 200
        endParams.rightMargin += 200

        val animator = ValueAnimator.ofObject(
            TypeEvaluator<LinearLayout.LayoutParams> { fraction, startValue, endValue ->
                val params = LinearLayout.LayoutParams(startValue)
                val left =
                    startValue.leftMargin + (endValue.leftMargin - startValue.leftMargin) * fraction
                val right =
                    startValue.rightMargin + (endValue.rightMargin - startValue.rightMargin) * fraction
                val top =
                    startValue.topMargin + (endValue.topMargin - startValue.topMargin) * fraction
                val bottom =
                    startValue.bottomMargin + (endValue.bottomMargin - startValue.bottomMargin) * fraction
                params.leftMargin = left.toInt()
                params.topMargin = top.toInt()
                params.rightMargin = right.toInt()
                params.bottomMargin = bottom.toInt()
                params
            },
            marginParams, endParams
        )
        animator.addUpdateListener {
            val params = it.animatedValue as LinearLayout.LayoutParams
            mBinding.btnUseMyTypeEvaluator.layoutParams = params
        }

        animator.interpolator = OvershootInterpolator()
        animator.duration = 1000
        animator.start()
    }

上面的代码中我们自定义了一个TypeEvaluator,通过泛型参数我们可以发现,这个评估程序主要是对LinearLayout.LayoutParams进行处理。之前我们通过ofInt方法仍然对一个ViewMargin属性进行过处理,但是那时候我们只能指定一个参数,如果我们希望左边距从0到100,右边距从0到200,那么用ofInt则会相对麻烦一些,而使用上面我们定义的TypeEvaluator则会相对容易一些。

使用插值器

插值器指定了如何根据时间计算动画中的特定值。例如:可以指定整个动画以线性方式播放,即动画在整个播放期间匀速运动,也可以指定动画使用非线性时间,例如动画在开始加速并在结束前减速。

动画系统中的插值器会接收来自Animator的分数,该分数表示动画的已播放时间。插值器会修改此分数,使其与要提供的动画类型保持一致。系统在android.view.animation包中提供了一组常用的插值器,如果已有的这些插值器仍然不能满足需求,则可以实现TimeInterpolator接收来提供自定义的插值器。

下面的代码是AccelerateDecelerateInterpolatorLinearInterpolator计算插值分数的方式。其中LinearInterpolator对已完成动画分数没有任何影响。AccelerateDecelerateInterpolator会在动画开始后加速,并在动画结束前减速,下面是两个插值器的源码:

AccelerateDecelerateInterpolator中的源码

    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

LinearInterpolator中的源码:

public float getInterpolation(float input) {
        return input;
    }

指定关键帧

KeyFrame对象由时间值对组成,用于在动画的特定时间定义特定的状态。每个关键帧还可以用自己的插值器控制动画在上一关键帧时间和此关键帧之间的时间间隔内的行为。其实就是我们可以指定动画在某一个时间执行到了什么程度,之前都是根据插值器来自动计算的。

要实例化KeyFrame对象,则必须使用它的任一工厂方法(ofIntofFloatofObject)来获取合适的类型。然后,通过调用ofKeyFrame()工厂方法来获取PropertyValuesHolder对象。获取对象后,可以通过传入PropertyValuesHolder对象以及要添加动画效果的对象来获取Animator,如下代码所示:

    //使用关键帧来构建动画
    private fun useKeyFrame() {
        //定义关键帧
        val keyFrame1 = Keyframe.ofFloat(0f, 0f)
        val keyFrame2 = Keyframe.ofFloat(0.5f, 30f)
        val keyFrame3 = Keyframe.ofFloat(1.0f, 360f)
        //获取PropertyValuesHolder对象
        val holder = PropertyValuesHolder.ofKeyframe("rotation", keyFrame1, keyFrame2, keyFrame3)
        //构建动画并开始执行
        ObjectAnimator.ofPropertyValuesHolder(mBinding.btnUseKeyFrame, holder).apply {
            interpolator = AccelerateDecelerateInterpolator()
            duration = 5 * 1000
            start()
        }
    }

在上面我们定义关键帧的时候,我们定义了在动画的前50%的时间旋转了30度,在后50%的时间旋转了剩下的330度,一共旋转了360度,通过这种方式,我们就可以实现类似于自定义插值器的效果。

使用ViewPropertyAnimator添加动画效果

ViewPropertyAnimator有助于使用单个底层Animator对象轻松为View的多个属性并行添加动画,它的行为方式与ObjectAnimator非常相似,因为它同样会修改视图属性的实际值,但在同时为多个属性添加动画效果时,它更加高效。

下面使用三种方式演示了让一个Button向右移动一半宽度并变为2倍大小。

  1. 首先使用组合动画AnimatorSet
    //使用AnimatorSet实现组合动画
    private fun useAnimatorSetMoreAnimator() {
        val target = mBinding.btnUseAnimatorSetMoreAnimator
        val translationAnimator =
            ObjectAnimator.ofFloat(target, "translationX", target.measuredWidth.toFloat() / 2)
        val scaleXAnimator = ObjectAnimator.ofFloat(target, "scaleX", 2f)
        val scaleYAnimator = ObjectAnimator.ofFloat(target, "scaleY", 2f)
        val set = AnimatorSet()
        set.play(translationAnimator).with(scaleXAnimator).with(scaleYAnimator)
        set.interpolator = OvershootInterpolator()
        set.duration = 5000
        set.start()
    }
  1. 使用PropertyValuesAniamtor
    //使用PropertyValuesHolder实现组合动画
    private fun usePropertyValuesHolderMoreAnimator(){
        val target = mBinding.btnUsePropertyValuesHolderMoreAnimator
        val translationHolder = PropertyValuesHolder.ofFloat("translationX",target.measuredWidth.toFloat() / 2)
        val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX",2.0f)
        val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY",2.0f)
        ObjectAnimator.ofPropertyValuesHolder(target,translationHolder,scaleXHolder,scaleYHolder).apply {
            interpolator = OvershootInterpolator()
            duration = 5 * 1000
            start()
        }
    }
  1. 使用ViewPropertyAnimator
    //使用ViewPropertyAnimator实现组合动画
    private fun useViewPropertyAnimatorMoreAnimator() {
        val target = mBinding.btnUseViewPropertyAnimatorMoreAnimator
        target.animate()
            .translationX(target.measuredWidth.toFloat() / 2)
            .scaleX(2f)
            .scaleY(2f)
            .start()
    }

以上三种方式都能实现同一种效果,但是最后一种方式更简洁而且可读性更好。