Android动画之闲聊片

2,240 阅读17分钟

在Android开发过程中,或多或少肯定会与动画打交道,今天我就来聊聊Android动画

视图动画

视图动画是仅针对view对象添加动画效果, 仅支持平移(Translate)缩放(Scale)旋转(Rotate)透明度(Alpha), 所以你无法对背景颜色等做动画效果

动画常用属性介绍:

- android:duration   动画时长(毫秒)
- android:startOffset   动画开始时delay时长(毫秒)
- android:fillAfter   动画结束时,是否保持在最后的位置(默认false,即会恢复初始状态)
- android:fillBefore   动画开始时,在startOffset阶段时是否应用动画属性的初始值,否则应用view的初始值(默认true),需要配合fillEnabled使用,如果fillEnabled为false,则应用动画属性的初始值; 即如果startOffset为0,则fillBefore设不设制 效果都一样
- android:fillEnabled    设置fillBefore属性是否有效

注意:fillBeforefillEnabledAnimationSet对象设置不起作用

平移(Translate)

利用xml定义动画,则动画文件需要定义在res/anim文件夹下

res/anim/view_anim_ translate.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0" // 定义动画开始时,x轴的位置
    android:toXDelta="300" // 定义动画结束时,x轴的位置
    android:fromYDelta="0" // 定义动画开始时,y轴的位置
    android:toYDelta="0" // 定义动画结束时,y轴的位置
    android:duration="2000"/>

fromXDeltatoXDeltafromYDeltatoYDelta属性可以设置三种类型的值:数值、百分数、百分数p

  • 数值: 以view左上角为原点,x轴或y轴偏移多少px
  • 百分数: 以view左上角为原点,x轴或y轴偏移View宽度或高度的百分比
  • 百分数p: 以view左上角为原点,x轴或y轴偏移父控件宽度或高度的百分比

缩放(Scale)

res/anim/view_anim_scale.xml

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="0.5" // 定义动画开始时,水平缩放因子
    android:toXScale="2.0" // 定义动画结束时,水平缩放因子
    android:fromYScale="1.0" // 定义动画开始时,垂直缩放因子
    android:toYScale="1.0" // 定义动画结束时,垂直缩放因子
    android:pivotX="0" // 定义原点x轴的位置
    android:pivotY="0" // 定义原点y轴的位置
    android:duration="2000"/>

pivotXpivotY属性也可以设置三种类型的值:数值、百分数、百分数p

  • 数值: 以view左上角为原点,x轴或y轴偏移多少px后 为缩放动画的原点
  • 百分数: 以view左上角为原点,x轴或y轴偏移View宽度或高度的百分比后 为缩放动画的原点
  • 百分数p: 以view左上角为原点,x轴或y轴偏移父控件宽度或高度的百分比后 为缩放动画的原点

旋转(Rotate)

res/anim/view_anim_rotate.xml

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="10"
    android:toDegrees="350"
    android:pivotY="50%"
    android:pivotX="50%"
    android:duration="2000"/>

pivotXpivotY属性也可以设置三种类型的值:数值、百分数、百分数p,参考上面的说明

透明度(Alpha)

res/anim/view_anim_alpha.xml

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.5" // 动画开始时的透明度
    android:toAlpha="1"  // 动画结束时的透明度
    android:duration="2000"/>

上面介绍了 怎么在xml中定义动画文件,然后就需要在代码中使用AnimationUtils加载动画文件

val translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_anim_alpha)
view.startAnimation(translateAnimation)

上面的四种动画对应的java动画类是TranslateAnimationScaleAnimationRotateAnimationAlphaAnimation;所以也可以直接使用java代码来实现上面的四种动画

// 以AlphaAnimation为例
val alphaAnimation = AlphaAnimation(1f, 0.2f)
alphaAnimation.duration = 2000
view.startAnimation(alphaAnimation)

AnimationSet

AnimationSet是实现将一组动画一起播放的效果

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:fromXDelta="0"
        android:toXDelta="100%"
        android:fromYDelta="0"
        android:toYDelta="0"
        android:duration="2000"/>

    <scale
        android:fromXScale="1.0"
        android:toXScale="2.0"
        android:fromYScale="1.0"
        android:toYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="2000"/>
</set>

fillAfter、fillBefore 和 fillEnabled

这里单独在介绍一下fillAfterfillBeforefillEnabled

先看看fillAfter的效果

val translate1Animation = TranslateAnimation(0f, 300f, 0f, 0f)
translate1Animation.fillAfter = false
translate1Animation.duration = 1000
tv_view1.startAnimation(translate1Animation)

val translate2Animation = TranslateAnimation(0f, 300f, 0f, 0f)
translate2Animation.fillAfter = true
translate2Animation.duration = 1000
tv_view2.startAnimation(translate2Animation)

可见fillAfter的作用是在动画结束时,是否保持在最后的位置

在看看fillBefore 配合 fillEnabled的效果

val translate1Animation = TranslateAnimation(100f, 300f, 0f, 0f)
translate1Animation.isFillEnabled = true
translate1Animation.fillBefore = false
translate1Animation.fillAfter = true
translate1Animation.startOffset = 1000
translate1Animation.duration = 1000
tv_view1.startAnimation(translate1Animation)

val translate2Animation = TranslateAnimation(100f, 300f, 0f, 0f)
translate2Animation.isFillEnabled = true
translate2Animation.fillBefore = true
translate2Animation.fillAfter = true
translate2Animation.startOffset = 1000
translate2Animation.duration = 1000
tv_view2.startAnimation(translate2Animation)

可见fillBefore表示在startOffset阶段时是否应用动画属性的初始值

注意:视图动画只会在绘制视图的位置进行修改,而不会修改实际的视图本身;如果您为某个按钮添加了动画效果,使其可以在屏幕上移动,该按钮会正确绘制,但能够点击按钮的实际位置并不会更改

属性动画

属性动画没有上面视图动画的限制,几乎可以为任何内容添加动画效果;您可以定义一个随时间更改任何对象属性的动画,无论其是否绘制到屏幕上

属性动画没有fillAfterfillBeforefillEnabled; 动画结束后保持在最后一帧的位置(类似视图动画的fillAftertrue效果一样),动画真正开始前(即startDelay结束后) 才应用动画的初始值(类似视图动画的fillEnabledtruefillBeforefalse效果一样)

属性动画没有startOffset,而是使用startDelay代替

ValueAnimator

ValueAnimator类是属性动画的核心类,它继承自Animator;借助ValueAnimator类,您可以为动画播放期间某些类型的值添加动画效果,只需指定一组要添加动画效果的 intfloat颜色值即可, 可以使用ValueAnimator的任一工厂方法来获取它:ofInt()ofFloat()ofObject()ofArgb();

由于ValueAnimator类只提供计算添加动画效果之后的值,所以需要借助AnimatorUpdateListener实现修改view的属性,实现动画效果

// 平移
ValueAnimator.ofFloat(0f, 300f).apply {
    duration = 1000
    addUpdateListener {
        view.translationX = it.animatedValue as Float
        view.translationY = it.animatedValue as Float
    }
    start()
}

// 缩放
ValueAnimator.ofFloat(0.5f, 2f).apply {
    duration = 1000
    addUpdateListener {
        view.pivotX = 0f
        view.pivotY = 0f
        view.scaleX = it.animatedValue as Float
        view.scaleY = it.animatedValue as Float
    }
    start()
}

// 旋转
ValueAnimator.ofFloat(10f, 350f).apply {
    duration = 1000
    addUpdateListener {
        view.pivotX = 0f
        view.pivotY = 0f
        view.rotation = it.animatedValue as Float
    }
    start()
}

// 透明度
ValueAnimator.ofFloat(0.1f, 1f).apply {
    duration = 1000
    addUpdateListener {
        view.alpha = it.animatedValue as Float
    }
    start()
}

// 背景颜色
ValueAnimator.ofArgb(Color.parseColor("#ff0000"), Color.parseColor("#0000ff")).apply {
    duration = 1000
    addUpdateListener {
        view.setBackgroundColor(it.animatedValue as Int)
    }
    start()
}

可见ValueAnimator是给定一个数值的变化区间和时间段(默认 300 毫秒),计算播放期间的的动画值,然后由你自己实现修改view的属性,所以ValueAnimator可以根据需要实现修改view的任何属性,达到不同的效果

对于属性动画的pivotXpivotY 与 视图动画有一点不一样,只能设置数值,不能设置百分比等;pivotXpivotY默认是相对view的中心位置,一旦设置就是相对view的左上角的坐标

上面是使用代码创建属性动画,下面我们来使用xml定义属性动画

对于属性动画在xml中定义动画文件与视图动画不一样,视图动画是定义在res/anim文件夹下, 而属性动画定义在res/animator文件夹下

res/animator/view_translate.xml

<?xml version="1.0" encoding="utf-8"?>
// animator 标签 对应 ValueAnimator类
// objectAnimator 标签 对应下面要讲到的 ObjectAnimator类
<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueFrom="0"
    android:valueTo="300"
    android:valueType="floatType"
    android:duration="1000" />

在代码中使用如下

(AnimatorInflater.loadAnimator(this, R.animator.view_translate) as ValueAnimator).apply {
    addUpdateListener {
        view.translationX = it.animatedValue as Float
        view.translationY = it.animatedValue as Float
    }
    start()
}

ObjectAnimator

ObjectAnimatorValueAnimator的子类,它也有ofInt()ofFloat()ofObject()ofArgb()ofPropertyValuesHolder等工厂方法,但是与ValueAnimator不一样的是,新增了target、和 propertyName参数

其中targetpropertyName的关系是 target必须有一个publicsetXXX的方法, 其中XXX就是PropertyName的名字

上面的demo使用ObjectAnimator如下:

// 平移
ObjectAnimator.ofFloat(view, "translationX", 0f, 300f).apply {
    duration = 1000
    start()
}

// 缩放
ObjectAnimator.ofFloat(view, "scaleX", 0.5f, 2f).apply {
    duration = 1000
    view.pivotX = 0f
    view.pivotY = 0f
    start()
}

// 缩放(同时修改2个属性 方法一)
val path = Path().apply {
    moveTo(0.5f, 0.5f)
    lineTo(2f, 2f)
}
ObjectAnimator.ofFloat(view, "scaleX", "scaleY", path).apply {
    duration = 1000
    view.pivotX = 0f
    view.pivotY = 0f
    start()
}

// 缩放(同时修改2个属性 方法二)
val scaleXAnim = PropertyValuesHolder.ofFloat("scaleX", 0.5f, 2f)
val scaleYAnim = PropertyValuesHolder.ofFloat("scaleY", 0.5f, 2f)
ObjectAnimator.ofPropertyValuesHolder(view, scaleXAnim, scaleYAnim).apply {
    duration = 1000
    view.pivotX = 0f
    view.pivotY = 0f
    start()
}

// 旋转
ObjectAnimator.ofFloat(view, "rotation", 10f, 350f).apply {
    duration = 1000
    view.pivotX = 0f
    view.pivotY = 0f
    start()
}

// 透明度
ObjectAnimator.ofFloat(view, "alpha", 0.1f, 1f).apply {
    duration = 1000
    start()
}

// 背景颜色
ObjectAnimator.ofArgb(view, "backgroundColor", Color.parseColor("#ff0000"), Color.parseColor("#0000ff")).apply {
    duration = 1000
    start()
}

xml中定义动画资源文件如下

res/animator/view_scale.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="scaleX"
    android:valueFrom="0.5"
    android:valueTo="2.0"
    android:valueType="floatType" />

代码中加载动画文件如下

AnimatorInflater.loadAnimator(this, R.animator.view_scale).apply {
    setTarget(view)
    start()
}

大多数情况下 我们都是使用ObjectAnimator,而不是ValueAnimator

AnimatorSet

AnimatorSet 是 针对属性动画 用于播放一组动画效果的类;它支持同时播放(playTogether)顺序播放(playSequentially); 同时也可以按照喜欢使用before(anim)with(anim)after(anim)三个方法自由组合多个属性动画

with(anim)设置此动画play(anim)与anim同时执行

before(anim)设置此动画play(anim)在anim之前执行

after(anim)设置此动画play(anim)在anim之后执行

val translationXAnim = ObjectAnimator.ofFloat(tv_view, "translationX", 0f, 300f).apply { duration = 1000 }
val scaleXAnim = ObjectAnimator.ofFloat(tv_view, "scaleX", 0.5f, 2f).apply { duration = 1000 }
val scaleYAnim = ObjectAnimator.ofFloat(tv_view, "scaleY", 0.5f, 2f).apply { duration = 1000 }
AnimatorSet().apply {
    // 如果设置了duration,则会覆盖每个animator的duration,否则使用animator自己的duration
    // duration = 4000
    // 同时播放
	// playTogether(translationXAnim, scaleXAnim, scaleYAnim)
    // 按顺序播放
    // playSequentially(translationXAnim, scaleXAnim, scaleYAnim)
    // 先平移,然后在x轴和y轴同时缩放
    play(translationXAnim).before(scaleXAnim).before(scaleYAnim)
    start()
}

在xml中定义动画资源文件如下

res/animator/view_animator_set.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" 
    android:ordering="sequentially">  <!-- 定义按循序播放 -->

    <objectAnimator
        android:duration="1000"
        android:propertyName="translationX"
        android:valueType="floatType"
        android:valueFrom="0"
        android:valueTo="300" />

    <!-- 定义一起播放播放 -->
    <set android:ordering="together">
        <objectAnimator
            android:duration="1000"
            android:propertyName="scaleX"
            android:valueType="floatType"
            android:valueFrom="0.5"
            android:valueTo="2.0"/>

        <objectAnimator
            android:duration="1000"
            android:propertyName="scaleY"
            android:valueType="floatType"
            android:valueFrom="0.5"
            android:valueTo="2.0"/>
    </set>
</set>

使用代码加载动画资源文件如下

AnimatorInflater.loadAnimator(this, R.animator.view_animator_set).apply {
    setTarget(view)
    start()
}

LayoutTransition

LayoutTransition 类为 ViewGroup 内的布局更改添加动画效果, 当您向 ViewGroup添加视图或删除其中的视图时,或当您使用 VISIBLEINVISIBLEGONE 调用视图的 setVisibility() 方法时,这些视图可能会经历出现和消失动画。向 ViewGroup 添加视图或删除其中的视图时,其中剩余的视图还可能以动画形式移动到新位置

您可以调用 setAnimator() 并使用以下任一 LayoutTransition 常量传入 Animator对象,从而在 LayoutTransition 对象中定义相应动画

  • APPEARING: 定义在ViewGroup中添加或显示view的动画
  • CHANGE_APPEARING: 定义由于view的添加或显示,导致ViewGroup中其他view发生变化的动画
  • DISAPPEARING: 定义在ViewGroup中隐藏或移除view的动画
  • CHANGE_DISAPPEARING: 定义由于view的隐藏或移除,导致ViewGroup中其他view发生变化的动画

先看看下面的demo

第一步先定义layout.xml如下

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="onBlock1Show"
            android:text="显示"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="onBlock1Hide"
            android:text="隐藏"/>
    </LinearLayout>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/block1"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:textSize="30dp"
            android:textColor="#ff0000"
            android:gravity="center"
            android:text="block1"
            android:background="#dddd00"/>
        <TextView
            android:id="@+id/block2"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:textSize="30dp"
            android:textColor="#ff0000"
            android:gravity="center"
            android:text="block2"
            android:background="#00fdfd"/>
    </LinearLayout>
</LinearLayout>

然后在代码中使用LayoutTransition如下

先看看LayoutTransition.DISAPPEARINGLayoutTransition.APPEARING的效果

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_animator_property)

    // 定义block1隐藏的动画
    val scaleYOut = PropertyValuesHolder.ofFloat("scaleY", 1f, 0f)
    val scaleXOut = PropertyValuesHolder.ofFloat("scaleY", 1f, 0f)
    val alphaOut = PropertyValuesHolder.ofFloat("alpha", 1f, 0f)
    val rotationOut = PropertyValuesHolder.ofFloat("rotation", 0f, 360f)
    val disappearingAnim = ObjectAnimator.ofPropertyValuesHolder(null as? Any, scaleYOut, scaleXOut, alphaOut, rotationOut)

    // 定义block1显示的动画
    val scaleYIn = PropertyValuesHolder.ofFloat("scaleY", 0f, 1f)
    val scaleXIn = PropertyValuesHolder.ofFloat("scaleX", 0f, 1f)
    val alphaIn = PropertyValuesHolder.ofFloat("alpha", 0f, 1f)
    val rotationIn = PropertyValuesHolder.ofFloat("rotation", 360f, 0f)
    val appearingAnim = ObjectAnimator.ofPropertyValuesHolder(null as? Any, scaleYIn, scaleXIn, alphaIn, rotationIn)

    val layoutTransition = LayoutTransition()
    // 应用block1隐藏动画
    layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearingAnim)
    // 应用block1显示动画
    layoutTransition.setAnimator(LayoutTransition.APPEARING, appearingAnim)
    block_container.layoutTransition = layoutTransition
}

fun onBlock1Show(view: View) {
    block1?.parent ?: block_container.addView(block1, 0)
//        block1.visibility = View.VISIBLE
}

fun onBlock1Hide(view: View) {
    block_container.removeView(block1)
//        block1.visibility = View.GONE
}

再看看LayoutTransition.CHANGE_DISAPPEARINGLayoutTransition.CHANGE_APPEARING的效果

...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_animator_property)

    // 定义block1隐藏的动画
    ...

    // 定义block1显示的动画
    ...

    // 定义隐藏block1时,block2的动画
    // 一定要添加下面的动画(LayoutTransition 源码里是这么写的),否则添加的动画可能没效果, 暂时不知道为什么
    val pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1)
    val pvhTop = PropertyValuesHolder.ofInt("top", 0, 1)
    val pvhRight = PropertyValuesHolder.ofInt("right", 0, 1)
    val pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1)
    val pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1)
    val pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1)
    // 自定义缩放动画
    val scaleXInAnim = PropertyValuesHolder.ofFloat("scaleX", 1f, 0.2f, 1f)
    val scaleYInAnim = PropertyValuesHolder.ofFloat("scaleY", 1f, 0.2f, 1f)
    val changeDisappearingAnim = ObjectAnimator.ofPropertyValuesHolder(null as? Any, pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY, scaleXInAnim, scaleYInAnim)

    // 定义显示block1时,block2的动画
    val backgroundAnim = ObjectAnimator.ofArgb(null as? Any, "backgroundColor", Color.parseColor("#ff0000"), Color.parseColor("#0000ff"))
    val changingAppearingAnim = AnimatorSet().apply {
        //(偷个懒,直接克隆changeDisappearingAnim动画)
        playTogether(changeDisappearingAnim.clone(), backgroundAnim)
    }

	val layoutTransition = LayoutTransition()
    // 应用block1隐藏动画
    layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearingAnim)
    // 应用block1显示动画
    layoutTransition.setAnimator(LayoutTransition.APPEARING, appearingAnim)
    // 应用block2的changeDisappearingAnim动画
    layoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeDisappearingAnim)
    // 应用block2的changingAppearingAnim动画
    layoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, changingAppearingAnim)
    block_container.layoutTransition = layoutTransition
}

效果如下

一般情况不需要设置LayoutTransition.CHANGE_DISAPPEARINGLayoutTransition.CHANGE_APPEARING的动画,直接使用默认的动画即可;如果需要自定义动画,则一定要在默认的属性动画上,在加上你自己的动画,类似上面的代码

上面是使用代码定义动画资源,你也可以在res/animator文件夹下中定义xml动画资源,然后在代码中应用, 这里我就不举例了

除了自定义LayoutTransition之外,我们可以直接使用animateLayoutChanges属性快速的添加默认的动画效果

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

这样的话就不需要在代码里添加任何代码,自动为添加到 ViewGroup 或从中删除的视图以及该 ViewGroup 中剩余的视图添加动画效果

StateListAnimator

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

StateListAnimator最低支持Android 5.0

在drawable下定义资源文件如下(也可以定义在xml文件夹下,官方文档是定义在xml文件夹下)

res/drawable/animator_state_list

<?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="@android:integer/config_shortAnimTime"
                android:valueTo="1.5"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="scaleY"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1.5"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="rotation"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueFrom="0"
                android:valueTo="360"
                android:valueType="floatType"/>
        </set>
    </item>
    <item android:state_pressed="false">
        <set>
            <objectAnimator android:propertyName="scaleX"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="scaleY"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="rotation"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueFrom="360"
                android:valueTo="0"
                android:valueType="floatType"/>
        </set>
    </item>
</selector>

然后在使用stateListAnimator属性设置animator_state_list资源文件即可

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <Button
	    android:id="@+id/btn_state_list"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="StateListAnimator"
        android:stateListAnimator="@drawable/animator_state_list"/>
</LinearLayout>

也可以在代码中设置

val stateListAnimator = AnimatorInflater.loadStateListAnimator(this, R.drawable.animator_state_list)
btn_state_list.stateListAnimator = stateListAnimator

效果如下

AnimatedStateListDrawable

AnimatedStateListDrawable的作用是 在状态更改间播放一组关键帧动画,达到最终的动画效果

AnimatedStateListDrawable最低支持Android 5.0

由于没有那么多图片,所以下面的demo以color为例(drawable也支持设置color)

首先在在res/drawable文件夹下新建animator_state_list_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 定义 按下 状态的 drawable -->
    <item android:id="@+id/pressed" android:state_pressed="true" android:drawable="@color/color_00ff00"/>
    <!-- 定义 松开 状态的 drawable -->
    <item android:id="@+id/no_pressed" android:state_pressed="false" android:drawable="@color/color_ffff00"/>

    <!-- 定义 由按下到松开 的动画转换过程(类似帧动画, 一帧一帧的播放) -->
    <transition
        android:fromId="@id/pressed"
        android:toId="@id/no_pressed">
        <animation-list>
            <item android:duration="30" android:drawable="@color/color_00ff00"/>
            <item android:duration="30" android:drawable="@color/color_11ff00"/>
            <item android:duration="30" android:drawable="@color/color_22ff00"/>
            <item android:duration="30" android:drawable="@color/color_33ff00"/>
            <item android:duration="30" android:drawable="@color/color_44ff00"/>
            <item android:duration="30" android:drawable="@color/color_55ff00"/>
            <item android:duration="30" android:drawable="@color/color_66ff00"/>
            <item android:duration="30" android:drawable="@color/color_77ff00"/>
            <item android:duration="30" android:drawable="@color/color_88ff00"/>
            <item android:duration="30" android:drawable="@color/color_99ff00"/>
            <item android:duration="30" android:drawable="@color/color_aaff00"/>
            <item android:duration="30" android:drawable="@color/color_bbff00"/>
            <item android:duration="30" android:drawable="@color/color_ccff00"/>
            <item android:duration="30" android:drawable="@color/color_ddff00"/>
            <item android:duration="30" android:drawable="@color/color_eeff00"/>
            <item android:duration="30" android:drawable="@color/color_ffff00"/>
        </animation-list>
    </transition>

    <!-- 定义 由松开到按下 的动画转换过程(类似帧动画, 一帧一帧的播放) -->
    <transition
        android:fromId="@id/no_pressed"
        android:toId="@id/pressed">
        <animation-list>
            <item android:duration="30" android:drawable="@color/color_ffff00"/>
            <item android:duration="30" android:drawable="@color/color_eeff00"/>
            <item android:duration="30" android:drawable="@color/color_ddff00"/>
            <item android:duration="30" android:drawable="@color/color_ccff00"/>
            <item android:duration="30" android:drawable="@color/color_bbff00"/>
            <item android:duration="30" android:drawable="@color/color_aaff00"/>
            <item android:duration="30" android:drawable="@color/color_99ff00"/>
            <item android:duration="30" android:drawable="@color/color_88ff00"/>
            <item android:duration="30" android:drawable="@color/color_77ff00"/>
            <item android:duration="30" android:drawable="@color/color_66ff00"/>
            <item android:duration="30" android:drawable="@color/color_55ff00"/>
            <item android:duration="30" android:drawable="@color/color_44ff00"/>
            <item android:duration="30" android:drawable="@color/color_33ff00"/>
            <item android:duration="30" android:drawable="@color/color_22ff00"/>
            <item android:duration="30" android:drawable="@color/color_11ff00"/>
            <item android:duration="30" android:drawable="@color/color_00ff00"/>
        </animation-list>
    </transition>
</animated-selector>

然后在layout布局中使用

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <View
        android:id="@+id/v_state_list_drawable"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:clickable="true"
        android:background="@drawable/animator_state_list_drawable"/>
</LinearLayout>

效果如下

相信大家对于在res/drawable下定义和使用普通的drawable应该都知道,而AnimatedStateListDrawable 只是添加了一组关键帧的过渡动画,让视图的状态变化体验更友好,不显得那么僵硬而已

TimeInterpolator(插值器)和TypeEvaluator(估值器)

TimeInterpolator(插值器)指定了如何根据时间计算动画中的因子(fraction),它的取值范围一般是0到1

TimeInterpolator(插值器)会接受来自Animator提供的已播放动画的时间片段(input)时间片段(input)的取值范围是0到1;而TimeInterpolator(插值器)就是修改这个值达得到最终的因子(fraction),从而达到LinearInterpolator(线性)AccelerateInterpolator(加速)DecelerateInterpolator(减速)AccelerateDecelerateInterpolator(先加速再减速)等不同的动画效果

LinearInterpolator源码

override fun getInterpolation(input: Float): Float = input

AccelerateDecelerateInterpolator源码

override fun getInterpolation(input: Float): Float =
            (Math.cos((input + 1) * Math.PI) / 2.0f).toFloat() + 0.5f

TimeInterpolator(插值器)都是基于数学中一个二维坐标系来计算的;如果你要实现一个非常『真实、自然』的动画,那你必须知道对应的数学公式

TypeEvaluator(估值器)就是根据TimeInterpolator(插值器)计算出来的因子(fraction),然后根据初始值(startValue)结束值(endValue), 计算一个最终的属性值

系统只提供了IntEvaluatorFloatEvaluatorArgbEvaluator三种类型的估值器,如果要为 Android 系统无法识别的类型添加动画效果,则可以通过实现TypeEvaluator接口来创建您自己的估值器

FloatEvaluator源码

// 由于fraction 取值范围是0~1
// 当 fraction = 0时,计算出来的属性值就是 startValue
// 当 fraction = 1时,计算出来的属性值就是 endValue
// 当 fraction 在 0~1中间时,计算出来的属性值就是 startValue~endValue的中间值
override fun evaluate(fraction: Float, startValue: Number, endValue: Number): Float {
    val startFloat = startValue.toFloat()
    return startFloat + fraction * (endValue.toFloat() - startFloat)
}

Keyframe关键帧

Keyframe 对象由<时间因子, 值>对组成,用于在动画的特定时间定义特定的状态, 每个关键帧还可以用自己的插值器控制动画在上一关键帧时间和此关键帧时间之间的时间间隔内的行为

Keyframe 提供了 ofInt()ofFloat()ofObject()工厂方法创建实例

// 定义刚开始时 0度
val kf0 = Keyframe.ofFloat(0f, 0f)

// 定义时间因子factor=0.5时,value = 360
// 即factor在[0f, 0.5f]变化过程中,rotation从0变到360
val kf1 = Keyframe.ofFloat(0.5f, 360f)

// 定义时间因子factor=1时,value = 0
// 即factor在[0.5f, 1f]变化过程中,rotation从360变到0
val kf2 = Keyframe.ofFloat(1f, 0f)

val pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2)
ObjectAnimator.ofPropertyValuesHolder(target, pvhRotation).apply {
    duration = 3000
    start()
}

ViewPropertyAnimator

ViewPropertyAnimator是使用单个Animator对象轻松为 View 的多个属性并行添加动画效果;

ViewPropertyAnimator它会修改视图属性的实际值,但在同时为多个属性添加动画效果时,它更为高效

ViewPropertyAnimator代码更加简洁,也更易读

下面我们看看它跟ObjectAnimator的使用区别

多个ObjectAnimator对象

val animX = ObjectAnimator.ofFloat(myView, "x", 50f)
val animY = ObjectAnimator.ofFloat(myView, "y", 100f)
AnimatorSet().apply {
    playTogether(animX, animY)
    start()
}

一个ObjectAnimator对象

val pvhX = PropertyValuesHolder.ofFloat("x", 50f)
val pvhY = PropertyValuesHolder.ofFloat("y", 100f)
ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvhY).start()

ViewPropertyAnimator

myView.animate().x(50f).y(100f)

不知不觉,瞎扯了这么多,好吧 就到这里了