Android Jetpack - 布局动画与布局过渡

本篇主题依然是动画,主角是Android系统的布局动画(Layout Animation)和布局过渡(Layout Transition)。

官方文档中,对于这两个概念其实有所混淆。按照官方笼统的说法,Android的“布局动画”是一个预加载的动画,每当布局改变的时候,动画都会执行。但是这个“改变”其实又得分作两部分来说:一方面,初始化时,从无到有的界面加载,是改变;另一方面,已加载完成的界面的布局改变,也是改变。虽然官方介绍的时候把这两个混在一起说,但是它们的实现和使用方法却并不相同。这也是本篇分作布局动画和布局过渡两方面来讨论的原因。

布局动画

布局动画是指ViewGroup首次加载布局完成时的动画,动画目标是ViewGroup的子View。一般常见于界面首次加载拉起的时候。

布局动画的设置,既可以用XML,也可以代码直接控制。

XML设置

XML方式设置布局动画是非常简单的,通过属性android:layoutAnimation来设置。来看看例子。

首先,定义一个线性布局作为布局动画的容器,然后include一个默认的子View,item_sun就是一个ImageView,很简单,这里就不写出来了。

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layoutAnimation="@anim/layout_anim_container"
        android:orientation="horizontal">

        <include layout="@layout/item_sun"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </LinearLayout>

其中layoutAnimation属性指定了布局动画的设置。

layou_anim.container.xml

<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/anim_translate"
    android:interpolator="@android:interpolator/accelerate_decelerate" />

布局动画的xml定义,以layoutAnimation为根标签。其中属性animation就是用于设置我们的预定义动画,这里是1000ms内右移25像素。

anim_translate.xml

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0"
    android:toXDelta="25"
    android:duration="1000"/>

至此,已经全部设置完毕,来看看效果:

《Android Jetpack - 布局动画与布局过渡》

【注】这里界面是已退出状态,从最近使用重新启动,也是首次加载。

可以看到,布局动画在布局加载完成后(界面完全显示)成功执行。那么,如果最初状态包含多个子View是什么效果呢?现在在容器中再增加同样的include,看看效果:

《Android Jetpack - 布局动画与布局过渡》

三个子View同时执行了布局动画。

在layoutAnimation中,还有其他属性可以让布局动画更加生动

  • delay:延迟每个子View动画起点的比例值
  • animationOrder:子View动画的执行顺序

delay是比例值,不是指实际动画时长。这个比例值的基数,才是动画时长。例如,如果delay是0.1,动画时长是800ms,那么实际的延时就是80ms。设容器中子View的位置为i(按动画执行顺序),那子View的动画启动延时就分别是i * 80ms。

animationOrder用于设置执行顺序,包括三个值:normal,reverse,random。很好理解,分别是顺序执行(默认)、反序执行和随机序执行。

现在添加delay属性,并设置值为1

<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="1"
    android:animation="@anim/anim_translate"
    android:interpolator="@android:interpolator/accelerate_decelerate" />

《Android Jetpack - 布局动画与布局过渡》

因为动画时长为1000ms,delay为1,所以每个子View都在它的前一个执行开始时延迟1000ms后执行自己的动画。

分别设置animationOrder为reverse和random,看看效果:

《Android Jetpack - 布局动画与布局过渡》
《Android Jetpack - 布局动画与布局过渡》

代码控制

通过代码来控制布局动画也很简单。

新增一个线性布局作为动画目标区域,主要使用LayoutAnimationController类来进行控制。将预定义的动画传入该控制器,然后通过View的setLayoutAnimation方法启用布局动画,最后调用控制器的start方法启动布局动画。

    // Use codes to set layout animation
    Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_rotation);
    LayoutAnimationController animationController = new LayoutAnimationController(animation);
    mContainer2.setLayoutAnimation(animationController);
    animationController.start();

这里的预定义动画是一个沿中轴的360度旋转。

anim_rotation.xml

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fromDegrees="0"
    android:toDegrees="360" />

效果如下,最初始的“月亮”按预期旋转了360度。

《Android Jetpack - 布局动画与布局过渡》 image

同样,控制器还可以设置延时和动画顺序

    animationController.setDelay(1);
    animationController.setOrder(LayoutAnimationController.ORDER_RANDOM);

《Android Jetpack - 布局动画与布局过渡》

值得注意的是,随机效果的每一个子View都是均等的,它们的执行时间完全随机,可以是执行序列i个位置的任一个 —— 所以,可能出现多个子View一起执行的情况(正如上面效果图所示)

布局过渡

初始加载的布局动画讲完,下面就该布局动态变化的动画 —— 布局过渡登场了。

过渡的分类

引起ViewGroup的布局变化,主要无外乎三类情况:。增,即添加子View;删,即删除子View;而改,即改变子View大小从而引起布局变化的连锁反应。在布局动画的世界里,增与删分别就是出场(appearing)和退场(disappearing),而改即改变(change)。而且,子View的可见性变化,也会引起出场与退场。

由此,就可以衍生出五种过渡类型,如下表所示。

类型说明
APPEARING出场动画,源于新增或可见
DISAPPEARING退场动画,源于删除或不可见
CHANGE_APPEARING出场联变动画,源于其他出场View
CHANGE_DISAPPEARING退场联变动画,源于其他退出View
CHANGE改变联变动画,源于非增删型布局变化

CHANGE_APPEARING和CHANGE_DISAPPEARING都是作用于非增、删项。也就是说,一个View被添加或删除,导致布局变化,引动其他兄弟子View。源是这个View,动画作用对象却是兄弟View。

DISAPPEARING和CHANGE_APPEARING是立即执行的动画,而其他动画则是时延动画,时延值是默认的出现、消失动画时长(300ms)。如果设置了自定义动画,且动画时长不等于300ms,那么延时动画APPEARING和CHANGE_DISAPPEARING的执行时间点就有可能先于或晚于那两个立即动画的结束点的。

为什么这样?举个例子,当有一个新的子View添加时,其他老的子View应该首先动起来(CHANGE_APPEARING),相当于给新View的腾位置(虽然不一定真的腾),然后新的View才能得以进来,并执行出场动画。同样道理,退场动画先执行,因退场而改变的其他View的动画才能执行。

出场和退场动画

系统默认

系统默认会提供一个渐显渐隐的出场和退场动画,但是得首先启用布局过渡功能。在目标容器的布局中设置即可,很简单。

    android:animateLayoutChanges="true"

嗯,如果使用系统默认的过渡效果,我们的工作就算完成了。不断增删子View,来看看效果:

《Android Jetpack - 布局动画与布局过渡》

效果还可以,至少,没有未开启过渡时的那种突兀感。

【注】字面上讲,animateLayoutChanges属性意思为“让布局改变动起来”,即把过渡动画作用于布局改变。需要注意的是,这个属性和前面的layoutAnimation属性没有半毛钱关系。而且,此属性开启是布局过渡得以执行的先决条件

自定义

下面,不用系统默认的渐变效果,自己实现一个简单的出场动画:右移一定距离后再归位。

首先,构造一个LayoutTransition对象,并设置给目标容器

    mLayoutTransition = new LayoutTransition();
    mContainer2.setLayoutTransition(mLayoutTransition);

接着,生成自定义动画(右移30像素再归位),调用LayoutTransitionsetAnimator方法设置过渡动画。

    ObjectAnimator animator = ObjectAnimator.ofFloat(null, "translationX", 0, 30, 0);
    animator.setInterpolator(new AccelerateInterpolator());
    mLayoutTransition.setDuration(LayoutTransition.APPEARING, 800);
    mLayoutTransition.setAnimator(LayoutTransition.APPEARING, animator);

添加子view观察效果

《Android Jetpack - 布局动画与布局过渡》

【注】出场动画设置的时长为800ms,是调用了LayoutTransitionsetDuration方法。在原始动画效果上调用setDuration是无效的。

类似的,增加DISAPPEARING类型,就是退场动画了。

    AnimatorSet animator1 = new AnimatorSet();
    animator1.playTogether(ObjectAnimator.ofFloat(null, "scaleX", 1, 0),
              ObjectAnimator.ofFloat(null, "scaleY", 1, 0));
    animator1.setInterpolator(new AccelerateInterpolator());
    mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator1);

退场动画为收缩子View尺寸至0,看看效果

《Android Jetpack - 布局动画与布局过渡》

出场联变和退场联变动画

出场联变和退场联变的设置方法类似于出场与退场。

定义一个旋转一周的CHANGE_APPEARING动画

    ObjectAnimator animator2 = ObjectAnimator.ofFloat(null, "rotation", 0, 360);
    animator2.setInterpolator(new AccelerateInterpolator());
    mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, animator2);

期望是,添加新View的时候,旧的兄弟View就按上面定义的一样,旋转360度。不过,这个效果暂时看不了了,因为,上面这段代码不起作用!

下面这段才是成功生效的代码

    AnimatorSet animator2 = new AnimatorSet();
    animator2.playTogether(ObjectAnimator.ofFloat(null, "rotation", 0, 360, 0));
    layoutTransition.setDuration(LayoutTransition.CHANGE_APPEARING, 800);
    layoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, animator2);

两段代码区别如下:

  • 属性值参数从”0, 360″变为”0, 360, 0″
  • 使用AnimatorSet动画组代替ObjectAnimator

《Android Jetpack - 布局动画与布局过渡》

正如前面所说,新增子View时,先执行CHANGE_APPEARING,然后才执行APPEARING。而且,APPEARING在CHANGE_APPEARING还未执行完的时候就已经开始了(因为默认延时300ms,而CHANGE_APPEARING时长为800ms)。

可是,为什么第一段代码不生效呢?

我们先来看看方法setAnimator的说明文档。

Sets the animation used during one of the transition types that may run. Any Animator object can be used, but to be most useful in the context of layout transitions, the animation should either be a ObjectAnimator or a AnimatorSet of animations including PropertyAnimators. Also, these ObjectAnimator objects should be able to get and set values on their target objects automatically. For example, a ObjectAnimator that animates the property “left” is able to set and get the left property from the View objects being animated by the layout transition. The transition works by setting target objects and properties dynamically, according to the pre- and post-layoout values of those objects, so having animations that can handle those properties appropriately will work best for custom animation. The dynamic setting of values is only the case for the CHANGE animations; the APPEARING and DISAPPEARING animations are simply run with the values they have.

It is also worth noting that any and all animations (and their underlying PropertyValuesHolder objects) will have their start and end values set according to the pre- and post-layout values. So, for example, a custom animation on “alpha” as the CHANGE_APPEARING animation will inherit the real value of alpha on the target object (presumably 1) as its starting and ending value when the animation begins. Animations which need to use values at the beginning and end that may not match the values queried when the transition begins may need to use a different mechanism than a standard ObjectAnimator object.

这么大的两段话,重点已被鄙人加粗。官方告诉我们,对于任意动画,起始和终止属性值都是根据布局前后的值来设置。如果一个自定义CHANGE_APPEARING动画是改变alpha值,那么动画开始时,它会继承动画目标的当前值作为起始和终止属性值 —— 也就是说,起始和终止值必须一样

官方也说了,如果你定义的起始值、终止值和动画开始时获取的值不一样,那么默认的系统机制就支持不了了。自己想办法吧。客观上来讲,这也是合理的,毕竟,这里不是真的属性变化动画,View的状态需要恢复到最初。

所以,rotation属性的值设置为“0, 360”不生效,只有“0, 360, 0”才能正常工作,因为要回到初始状态啊。不过,为什么又一定要AnimatorSet呢?这一点,我也没搞明白,看官您搞清楚的话,麻烦知会一声啊!

按照上面的思路,再定义一个CHANGE_DISAPPEARING动画,同样是旋转360度。

    AnimatorSet animator3 = new AnimatorSet();
    animator3.playTogether(ObjectAnimator.ofFloat(null, "rotation", 0, 360, 0));
    layoutTransition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 800);
    layoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, animator3);

《Android Jetpack - 布局动画与布局过渡》

【注】 如果在DISAPPEARING或CHANGE_APPEARING还没结束时,又来一个APPEARING动画;或者是,在APPEARING或CHANGE_DISAPPEARING还没结束时,又来一个DISAPPEARING,那本来的未结束动画将停止,且View的状态也会异常。具体现象就不贴出来了。

改变动画

改变动画即指CHANGE类型:由非增删引起的布局变化动画。关于CHANGE动画需要注意两点:

  1. CHANGE动画默认关闭,可调用enableTransitionType(int)方法开启
  2. CHANGE动画的作用目标是所有布局发生改变的View,因为无增删,所以动画包括改变源本身

增加第三个目标容器,默认添加一个文本控件和图片控件。文本控件的内容可改变,目的是模拟子View布局变化。设置自定义动画为透明度变化。

    LayoutTransition layoutTransition = new LayoutTransition();

    AnimatorSet animator = new AnimatorSet();
    ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 1, 0.5f, 1);
    animator.play(anim);
    layoutTransition.setDuration(LayoutTransition.CHANGING, 1000);
    layoutTransition.setAnimator(LayoutTransition.CHANGING, animator);

    layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
    mContainer3.setLayoutTransition(layoutTransition);

分别新增View及改变文本内容,观察效果

《Android Jetpack - 布局动画与布局过渡》

可以看到,新增View是无法引起CHANGE动画的,只有改变文本内容,从而使其布局变化,才能让所有的子View动起来。

小结

布局动画和布局过渡在官方文档中,是杂糅到一块来讲的,鄙人并不苟同。虽然都是旨在“让布局变化动起来”,但是从使用方法来看,它们又截然不同。

本篇还留下部分疑问尚待解决。例如,为什么CHANGE_APPEARING、CHANGE_DISAPPEARING和CHANGE的动画,必须通过AnimationSet作为载体,才能成功实现?这个问题,估计需要阅读源码才能解决了。
【附录】

《Android Jetpack - 布局动画与布局过渡》 资料图

需要资料的朋友可以加入Android架构交流QQ群聊:513088520

点击链接加入群聊【Android移动架构总群】:加入群聊

获取免费学习视频,学习大纲另外还有像高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)等Android高阶开发资料免费分享。

    原文作者:Android架构
    原文地址: https://www.jianshu.com/p/2ca7dafc2827
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞