Android属性动画探索

2019-03-12  本文已影响0人  anrikuwen

众所周知,良好的动画不仅是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方法调用后进行上面说的过程的源码。有兴趣的可以自己去看看。

属性动画执行原理

属性动画的特点

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

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

属性动画和View动画的不同

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

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要设置上面的两个监听器仍然可以进行设置。

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

下面是一个自定义一个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有四种类型的动画

然后需要注意的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上。

总结

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

上一篇下一篇

猜你喜欢

热点阅读