Android属性动画探索

众所周知,良好的动画不仅是APP的UI效果看着特别舒服,还会让用户得到及时的操作返回。这篇博客就带领大家来玩玩Android的属性动画。

属性动画如何在工作

这里就借官网的UML图来进行解释。注意这里以ValueAnimator来进行说明其它的属性动画类都是继承自ValueAnimator的,因此原理是一样的。

valueanimator

重要的类说明

Animator

此类用于为创建属性动画主要类。其中ValueAnimator(以及其它的属性动画类直接或间接)是Animator的子类。

直接或间接实现Animator抽象类的类:

TimeInterpolator

时间插值器。这个类是用来根据时间的流逝来计算出当前动画完成的比例(其中动画的比例为0f到1f)

直接或间接实现TimeInterpolator接口的类:

TypeEvaluator

类型估值器。这个类根据所要进行动画的属性的值类型而不同,它用来根据拿到的属性开始值、结束值和动画完成比例来计算出属性真正需要改变的值

直接或间接实现TypeEvaluator接口的类:

如何工作

看上面的UML图可以发现ValueAnimator中包含了TimeInterlator和TypeEvaluator。

动画开始会通过TimeInterpolator进行进行动画完成比例的计算。之后在通过估值器来计算出真正改变了的属性值。最后回调UpdateListener来进行对应对象的属性值改变。

下面是ValueAnimator的start方法调用后进行上面说的过程的源码。有兴趣的可以自己去看看。

属性动画执行原理

属性动画的特点

属性动画通过在一定的时间内改变一个对象的属性来实现。它几乎可以对任何东西进行动画。

下面是属性动画可以定义的内容:

  • Duration:表示属性动画的持续时间。默认是300ms
  • Time Interpolation:可以根据时间的流逝定义属性值如何改变
  • Repeat count and behavior:你可以定义属性动画是否重复以及重复的次数;以及是否一次属性动画结束后,下次属性动画刚好做反向的动画。
  • Animator sets:你可以设置属性动画集。在这个集中你可以让这些属性动画一起执行或者自定义它们的先后顺序

属性动画和View动画的不同

如果对View动画不了解的话,请跳过这一节。

View动画的缺点:

  • View动画只能作用在View对象上。而属性动画可以作用在几乎任何的对象上。
  • View动画只能对View的某些属性进行动画。比如说View的background color就无法进行View动画

上面可以看到View动画有很多的缺点,这些缺点在属性动画中都没有。那是不是View动画就一无是处了?其实不然。View动画需要更少的时间设置、代码也更少。因此在进行动画选择的时候请根据具体的情况来进行选择。

Animator的使用

下面是使用ValueAnimator、ObjectAnimator和AnimatorSet来进行动画的例子:

xml如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/cl"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true"
    tools:context=".animation.AnimationActivity">


    <Button
        android:id="@+id/animator_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Animator"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/target_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Animation"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

代码如下:

class AnimationActivity : AppCompatActivity() {

    private val TAG = "AnimationActivity"

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

        //ValueAnimator
        val translationYAnimation = ValueAnimator.ofFloat(300f)
        .apply {
            duration = 2000
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                    Log.e(TAG, "onAnimationRepeat")
                }

                override fun onAnimationEnd(animation: Animator?) {
                    Log.e(TAG, "onAnimationEnd")
                }

                override fun onAnimationCancel(animation: Animator?) {
                    Log.e(TAG, "onAnimationCancel")
                }

                override fun onAnimationStart(animation: Animator?) {
                    Log.e(TAG, "onAnimationStart")
                }

            })
            addUpdateListener {
                target_btn.translationY = it.animatedValue as Float
            }
        }
        
        //ObjectAnimator
        val translationXAnimation = ObjectAnimator.ofFloat(target_btn, "translationX", 
            100f)
        .apply {
            duration = 2000
        }

        //AnimatorSet
        val animatorSet = AnimatorSet()
        .apply {
            play(translationXAnimation).with(translationYAnimation)
        }

        animator_btn.setOnClickListener {
            animatorSet.start()
        }
    }
}

第一处使用了ValueAnimator:

首先,通过调用ofFloat静态方法来实例化了一个ValueAnimator对象。这里传入一个值表示结束值,初始值默认为0f,传入两个值第一个是初始值,第二个是结束值,你还可以设置更多的值,这样就会进行多个动画变化。这里举个例子:当有3个值的时候:先是1值为初始值,2值为结束值;再是2值为初始值,3值为结束值。如果使用ofObject那么不能像这样省略初始值。当然还有很的ofXX方法可以实例画ValueAnimator,这里就不一一介绍了。

然后,通过setDuration方法给动画设置了持续时间

之后,通过addListener方法添加给动画添加了AnimatorListener用于监听动画的执行情况。

最后,通过addUpdateListener方法给ValueAnimator设置了更新监听器。用于做具体的更新。这里通过获取的更新值来对target_btn这Button控件进行translationY属性的更新。

第二处使用了ObjectAnimator:

首先,和ValueAnimator一样通过ofFloat方法来实例化了一个ObjectAnimator对象。

不同的是这里的第一个参数传入的是动画作用的对象,第二个参数表示需要变换的属性名。传入初始结束值、结束值和ValueAnimator是一样的。

然后,ObjectAnimator设置duration和ValueAnimator也是一样的

可以看到ObjectAnimator比ValueAnimator简单很多,它可以不用设置监听器。但是是不是无法设置监听器?当然ObjectAnimator要设置上面的两个监听器仍然可以进行设置。

但是这是有一定的条件要求的:

  • 首先,这个属性必须要有一个setter方法(以驼峰命名法进行命名)。比如:有个属性叫customX那么必须要有一个setCustomX方法,ObjectAnimator才能对customX进行设置。
  • 然后,如果你传入的只有结束值。那么你需要有个对应属性的getter方法才能获取对应属性的初始值。
  • 有些属性需要重绘,但是本身的View并没有进行重绘的无法进行动画。比如说:setRotation。

下面是一个自定义一个CustomButton。可以通过customY来对Button的y属性进行设置。

class CustomButton : Button {

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) :
            super(context, attrs)

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
            super(context, attrs, defStyleAttr)

    fun setCustomY(customY: Float) {
        y = customY
    }

    fun getCustomY(): Float{
        return y
    }
}

这样就可以像下面一样调用了:

ObjectAnimator.ofFloat(target_btn, "customY", 200f).apply {
    duration = 2000
    start()
}

这里可以只设置一个结束值,因为我提供了getter方法。

第三处使用AnimatorSet

这个Animator直接通过构造器来实例化对象就行。

然后,通过play方法来添加Animator。像上面这样两个动画会同时进行。如果想要一个动画在另一个动画之前(之后)执行,可以调用before(after)。就像下面一样:

val animatorSet = AnimatorSet().apply {
    play(translationXAnimation).before(translationYAnimation)
}

使用TypeEvaluator和TimeInterpolator

对于AnimatorSet是不存在TypeEvaluator的。类型估值器大家知道这是根据初始值、结束值、动画完成比例来计算得到我们想要的结果的类,所以对于每个动画都是有自己独有的TypeEvaluator的。

但是AnimatorSet是可以设置TimeInterpolator。如果AnimatorSet设置了TimeInterpolator那么所有动画将会使用AnimatorSet的TimeInterpolator。默认AnimatorSet的TimeInterpolator是为null的

AnimatorSet已经在这里说了。在下面的具体说明中将只会对ValueAnimator和ObjectAnimator进行说明了。而且不会讨论ofPropertyValuesHolder方法,这个方法将会在后面进行讲解。

TypeEvaluator

ValueAniamtor和ObjectAnimator中ofFloat的默认TypeEvaluator是FloatEvaluator、ofInt的默认TypeEvaluator是IntEvaluator、ofArgb的默认TypeEvaluator是ArgbEvaluator。

ObjectAnimator中还有ofMultiInt方法默认的TypeEvaluator是IntArrayEvaluator、ofMultiFloat默认的TypeEvaluator是FloatArrayEvaluator、ofFloat关于Path的重载默认TypeEvaluator是PointFTypeEvaluator。

这些方法生成的Animator的TypeEvalautor都是可以通过setEvaluator方法自定义的。但是大多数都是没有必要进行自定义的。

对于ValueAnimator以及ObjectAnimator的没有包含Path参数的重载的ofObject方法必须提供自定义的TypeEvaluator。

自定义TypeEvaluator也比较简单,重要的还是中间你需要完成什么样的逻辑。下面是个简单的调用ValueAnimator的ofObject完成上面ofFloat相同逻辑的代码:

//ValueAnimator
val translationYAnimation = ValueAnimator.ofObject(TypeEvaluator<Float> { 
fraction, startValue, endValue -> startValue + (endValue - startValue) * fraction}, 0f, 400f)
.apply {
...
}

TimeInterpolator

对于ValueAnimator、ObjectAnimator默认的TimeInterpolator都是AccelerateDecelerateInterpolator。改变这个很简单。调用它们的setInterpolator就行(对于AnimatorSet是一样的)。当然你可以通过自己实现TimeInterpolator来进行自定义TimeInterpolator。

简单虽然简单还是给一个例子吧:

//ValueAnimator
val translationYAnimation = ValueAnimator.ofObject(TypeEvaluator<Float> { 
fraction, startValue, endValue ->
    startValue + (endValue - startValue) * fraction}, 0f, 400f)
    .apply {
    interpolator = DecelerateInterpolator()
...
}

使用KeyFrame

KeyFrame是用于保存动画在某个具体的完成比例处对应动画执行值。

然后,将定义好的动画值保存在PropertyValuesHolder类中。

最后,调用ValueAnimator或者ObjectAnimator的ofPropertyValuesHolder方法来生成对应的Animator类。

其实,ValueAnimator、ObjectAnimator其它的ofXXX方法最终都是会通过keyFrame来实现的。因此对于KeyFrame的ofXXX方法是ValueAnimator、ObjectAnimator的ofXXX方法调用的类型估值器是一样的。

下面是简单使用KeyFrame的示例代码:

val kf0 = Keyframe.ofFloat(0f, 1f)
val kf1 = Keyframe.ofFloat(0.5f, 0.7f)
val kf2 = Keyframe.ofFloat(1f, 0.3f)
val alphaPropertyValuesHolder = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, 
kf2)

val alphaAnimation = ObjectAnimator.ofPropertyValuesHolder(target_btn, 
alphaPropertyValuesHolder).apply {
    duration = 2000
}

animator_btn.setOnClickListener {
    alphaAnimation.start()
}

LayoutTransition的使用

LayoutTransition作用在某个ViewGroup上,当这个ViewGroup的添加了或删除了(View可见性发生变化)View的时候,会对其直接布局的View产生动画效果(注意:在这个ViewGroup中的子ViewGroup中的View控件不会发生动画效果)。

LayoutTransition有四种类型的动画

  • APPEARING:这个动画出现在ViewGroup中某个View被添加(由INVISIBLE、GONE变为VISIBLE)。这个View产生的动画
  • DISAPPEARING:这个动画出现在ViewGroup中某个View被移除(由VISIBLE变为INVISIBLE、GONE)。这个View产生的动画
  • CHANGE_APPEARING:这个动画出现在ViewGroup中某个View被添加(由INVISIBLE、GONE变为VISIBLE)。ViewGroup中其它Layout发生变化的View的动画变化
  • CHANGE_DISAPPEARING:这个动画出现在ViewGroup中某个View被移除(由VISIBLE变为INVISIBLE、GONE)。ViewGroup中其它Layout发生变化的View的动画变化。

然后需要注意的LayoutTransition只能ObjectAnimator。因为动画具体作用的目标对象会进行变化。你无法通过addUpdateListener来具体指明某个对象

下面给个例子,然后通过例子来进行解释:

xml如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/cl"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true"
    tools:context=".animation.AnimationActivity">


    <Button
        android:id="@+id/animator_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Animator"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/animator_two_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="AnimatorTwo"
        android:textAllCaps="false"
        app:layout_constraintLeft_toRightOf="@id/animator_btn"
        app:layout_constraintTop_toTopOf="parent" />


    <Button
        android:id="@+id/target_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#74ebd5"
        android:text="Target"
        android:textAllCaps="false"
        app:layout_constraintBottom_toTopOf="@id/target_two_btn"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <Button
        android:id="@+id/target_two_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#74ebd5"
        android:text="TargetTwo"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/target_btn" />

</androidx.constraintlayout.widget.ConstraintLayout>

代码如下:

class AnimationActivity : AppCompatActivity() {

    private val TAG = "AnimationActivity"
    
    lateinit var customAppearingAnim: Animator
    lateinit var customDisappearingAnim: Animator
    lateinit var customChangingAppearingAnim: Animator
    lateinit var customChangingDisappearingAnim: Animator

    /**
     * ofArgb需要LOLLIPOP才行
     */
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_animation)
        val transitioner = LayoutTransition()

        //创建动画
        createCustomAnimations(transitioner)

        //为LayoutTransition设置动画
        transitioner.setAnimator(LayoutTransition.APPEARING, customAppearingAnim)
        transitioner.setAnimator(LayoutTransition.DISAPPEARING, customDisappearingAnim)
        transitioner.setAnimator(LayoutTransition.CHANGE_APPEARING, 
        customChangingAppearingAnim)
        transitioner.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, 
        customChangingDisappearingAnim)
        
        //为这些动画设置时间
        transitioner.setDuration(LayoutTransition.APPEARING, 1000)
        transitioner.setDuration(LayoutTransition.DISAPPEARING, 1000)
        transitioner.setDuration(LayoutTransition.CHANGE_APPEARING, 1000)
        transitioner.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 1000)

        
        //为ViewGroup设置LayoutTransition
        cl.layoutTransition = transitioner
        
        //进行可见性的修改
        animator_btn.setOnClickListener {
            if (target_btn.visibility == View.GONE) {
                target_btn.visibility = View.VISIBLE
            } else {
                target_btn.visibility = View.GONE
            }
        }

        animator_two_btn.setOnClickListener {
            if (target_two_btn.visibility == View.GONE) {
                target_two_btn.visibility = View.VISIBLE
            } else {
                target_two_btn.visibility = View.GONE
            }
        }
    }

    /**
     * 此方法用于定义那四个自定义的动画
     */
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    private fun createCustomAnimations(transition: LayoutTransition) {

        //AppearingAnim
        customAppearingAnim = ObjectAnimator.ofFloat(null, "scaleX", 0f, 1f)
                .apply {
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator?) {
                            val view = (animation as ObjectAnimator).target as View
                            view.scaleX = 1f
                        }
                    })
                }

        //DisappearingAnim
        customDisappearingAnim = ObjectAnimator.ofFloat(null, "scaleX", 1f, 0f)
                .apply {
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator?) {
                            val view = (animation as ObjectAnimator).target as View
                            view.scaleX = 1f
                        }
                    })
                }
        
         //ChangingAppearingAnim
        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 mPropertyValuesHolder = PropertyValuesHolder.ofFloat(
                "rotation", 0f, 180f, -180f, 0f)
        customChangingAppearingAnim = ObjectAnimator.ofPropertyValuesHolder(
                null as Any?, pvhLeft, pvhTop, pvhRight, pvhBottom, mPropertyValuesHolder)


        //ChangingDisappearingAnim
        val color1 = Color.parseColor("#74ebd5")
        val color2 = Color.parseColor("#ACB6E5")
        customChangingDisappearingAnim = ObjectAnimator.ofArgb(null, "backgroundColor",
                color1, color2)
                .apply {
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator?) {
                            val view = (animation as ObjectAnimator).target as View
                            view.setBackgroundColor(color1)
                        }
                    })
                }
}

四种属性动画的设置

直接看到createCustomAnimations方法。

首先,设置了APPEARING、DISAPPEARING动画。这两个的动画我不太想多说就和普通的ObjectAnimator查动画差不多的。当然你也可以使用KeyFrame。

需要注意一点的就是object参数可以填任意参数。因为在LayoutTranslation进行动画的时候这对象会进行修改。因此这里直接设置为null。

其中duration也会进行在这里的动画设置也是无效的,他会被LayoutTranslation的duration设置覆盖掉。

然后,设置了CHANGE_APPEARING、CHANGE_DISAPPEARING动画。这两个动画比较坑。官方没有进行详细的说明。

通过看源码才发现,这两个View的动画的所有的属性值的初始值结束值都会针对View的添加(移除)前后通过反射获取get<属性名>方法来获取前后对应的属性进行设置。如果没有对应的get方法,那么将不会改变这两个对应的值。

因此对于CHANGE_APPEARING、CHANGE_DISAPPEARING属性的初始值结束值的设置是不重要的(有get<属性名>的属性)

重点还没完!!!这才是最重点的,在进行修改后如果所有属性中有某个属性的初始值结束值不一样,那么就进行动画

因此,在CHANGE_APPEARING动画化我们加了left、top、right、bottom的属性(其中那两个值可以任意设置,后面会根据View的对应属性来进行修改)。这样表示在某个View的添加后,其它的View如果位置发生了变化就进行这个动画

这里将着可能有些蒙。简而言之,就是加上left、top、right、bottom属性就表示在某个View添加(移除)后,ViewGroup中其它的View如果Layout发生了变化后,就进行这个动画。

对于CHANGE_DISAPPEARING和CHANGE_APPEARING是一样。但是这里例子没有设置left、top、right、bottom???因为前面讲过这里的backGround属性是没有对应的get方法的。因此只需要保证自己定义的backGround属性的初始值结束值不一样就行。

可能没有经过源码的解释,有点空。之后我想另外用一篇博客通过源码解释的方式来进行说明。

将动画设置给ViewGroup

创建完动画后。创建一个LayoutTranslation。通过像上面的示例代码一样调用LayoutTranslation的setAnmator、setDuration方法。来进行动画和持续时间的设置。

最后将自定义的LayoutTranslation对象设置为对应的ViewGroup就行了。

设置默认的LayoutTranslation

自定义麻烦?那用自带的LayoutTranslation吧!只需要想下面在xml中加一行代码就行。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    ...
    android:animateLayoutChanges="true">
...
</androidx.constraintlayout.widget.ConstraintLayout>

在xml中定义属性动画

前面讲的属性动画都是在代码中进行定义的。

但属性动画的定义还可以在xml中进行定义的。在xml中定义能够很容易在进行动画的重用以及更加容易的编辑动画执行的顺序。

定义的xml属性动画要放在res/animator/文件夹下。

Animator的三个直接(间接)子类的在xml中分别的对应:

下面通过一例子来走进xml的动画定义。

custom_animator.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">
    <set>
        <objectAnimator
            android:duration="1000"
            android:propertyName="scaleX"
            android:valueTo="1.5f"
            android:valueType="floatType" />
        <objectAnimator
            android:duration="1000"
            android:propertyName="scaleY"
            android:valueTo="1.5f"
            android:valueType="floatType" />
    </set>

    <objectAnimator
        android:duration="1000"
        android:propertyName="alpha"
        android:valueTo="0.5f"
        android:valueType="floatType" />
</set>

set标签可以添加ordering属性来定义set中的动画执行顺序默认是together也就是同时进行,sequentially就是依次进行。

然后其它的都是很简单的对应着代码中的属性定义来。

在代码中进行调用

AnimatorInflater.loadAnimator(this, R.animator.custom_animator)
.apply {
    setTarget(target_btn)
    start()

    addListener(object :AnimatorListenerAdapter(){
        override fun onAnimationEnd(animation: Animator?) {
            target_btn.scaleX = 1f
            target_btn.scaleY = 1f
        }
    })
}

在代码中,通过AnimatorInflator来实例化xml中动画。后面就是和前面animator使用是一样的了。

使用StateListAnimator

StateListAnimator用于根据View的不同状态的改变实现动画效果的类。

下面通过例子会更加深刻的理解。

首先,通过xml定义动画:

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

之后在布局中对要使用StateListAnimator动画的进行如下设置:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    ...>

    <Button
        ... />

    <Button
        .../>

    <Button
        android:id="@+id/target_btn"
        ...
        android:stateListAnimator="@animator/animate_scale"/>

    <Button
        android:id="@+id/target_two_btn"
        ...
        android:stateListAnimator="@animator/animate_scale"/>

</androidx.constraintlayout.widget.ConstraintLayout>

上面是在xml进行StateListAnimator的设置。

但StateListAnimator还可以在代码中进行设置

target_btn.stateListAnimator = AnimatorInflater.loadStateListAnimator(this, R.animator.animate_scale)
target_two_btn.stateListAnimator = AnimatorInflater.loadStateListAnimator(this, R.animator.animate_scale)

这样设置后的效果是一样的。

使用ViewPropertyAnimator

ViewPropertyAnimator以一种更简单、更有效方式进行常用的View动画。

当同时有多个动画在一起进行的时候。使用ViewPropertyAnimator的话View只会调用一次invalidate方法。

还有个优点就是ViewPropertyAnimator使用更简单。

target_btn.animate().apply { 
    duration = 2000
    scaleX(1.5f)
    scaleY(1.5f)
    start()
}

ViewPropertyAniamator虽然有点很多。但是限制也很大。它只能作用于基本的一些属性。自定义View的自定义属性不能使用。只能作用在单个View上。

总结

现在总结一下这篇博客讲了些啥。

  • Animator的几个子类的使用、以及其它的代码中定义属性动画的内容,比如说:ViewPropertyAnimator
  • TypeEvaluator、TimeInterpolator的使用
  • LayoutTranslation的使用
  • xml中定义代码以及StateListAnimator的使用
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 修行就是在生活当中觉察,真实,简单,自然。物来则应,物去不留。(转朋友)
    初露倪儿阅读 519评论 0 0
  • 文/图:泡沫 卢梭说:“大自然塑造了我,然后模子碎了。” 这话听起来自负,其实用于每一个人。可惜的是,多数人忍受不...
    持续行动的泡沫阅读 135评论 0 0
  • 笔记本里写下的名字 我不记得谁的生日 当初狠心刻意涂掉的你 现在也没能好好忘记 我常常会问自己 要攀到什么样的高度...
    比克阅读 250评论 0 0