LayoutAnimation 炫酷的布局动画及原理分析

作者: godliness | 来源:发表于2019-12-18 15:11 被阅读0次

    UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部分内容。


    UI 优化系列专题
    1. UI 渲染背景知识
    1. 如何优化 UI 渲染

    在 Android API 1.0 时,LayoutAnimation 就已经存在,利用 LayoutAnimation 我们可以快速实现布局的动画效果,提升产品的视觉体验。下面我们先通过它的自我介绍来了解下什么 LayoutAnimation:

    • LayoutAnimation 用于对布局或视图组的子项进行动画处理。每个子项都使用相同的动画,但是对于每个子项的动画在不同的时间开始。布局动画控制器,用于计算每个子项的动画开始执行的偏移时间

    这里引入一个新的名词:布局动画控制器,实际上 LayoutAnimation 只是为我们提供了一个 XML 标签,它的实现要依赖布局动画控制器来完成, Android 系统默认为我们提供了两种 LayoutAnimation 控制器:

    1. LayoutAnimationController

    2. GridLayoutAnimationController

    接下来,我先通过几个动画案例介绍下 LayoutAnimation 的应用效果以及扩展内容,最后再通过源码分析 LayoutAnimation 的实现原理。


    布局动画的使用

    和其他动画一样,LayoutAnimation 既可以定义 XML 动画文件,也可以直接通过代码的方式创建,下面我们分别通过示例来了解下他们的应用效果。

    创建布局动画文件

    1. LayoutAnimationController

    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        // 指定每项要执行的动画文件
        android:animation="@anim/item_animation_drop_down"
        android:animationOrder="normal"
        android:delay="15%" />
    

    LayoutAnimation 参数说明如下:

    应用到 ViewGroup:

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        // 为 RecycleView 的每个子项设置 LayoutAnimation
        android:layoutAnimation="@anim/layout_animation_fall_down" />
    

    2. GridLayoutAnimationController

    GridLayoutAnimationController 继承自 LayoutAnimationController,相比 LayoutAnimationController 只是增加了几个功能参数之外,几乎没有任何使用上的差异,它更多是针对 GridView 而设计的。

    <gridLayoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:rowDelay="75%"
        android:columnDelay="60%"
        android:directionPriority="none"
        android:direction="bottom_to_top|right_to_left"
        android:animation="@android:anim/slide_in_left"/>
    
    

    GridLayoutAnimation 参数使用说明如下:

    通过代码创建

    除了上述通过定义 XML 文件方式之外,我们也可以直接通过代码创建布局动画,这里仅以 LayoutAnimationController 为例:

    public void createLayoutAnimation(RecyclerView view) {
        final Animation animation = AnimationUtils.loadAnimation(this, R.anim.item_animation_drop_down);
        // 直接创建 LayoutAnimationController
        LayoutAnimationController layoutAnimation = new LayoutAnimationController(animation);
        layoutAnimation.setDelay(0.15f);       
        layoutAnimation.setOrder(LayoutAnimationController.ORDER_NORMAL);
        // 为 RecycleView 应用布局动画
        view.setLayoutAnimation(layoutAnimation);
    }
    

    也可以直接通过 AnimationUtils 加载 layoutAnimation 文件:

    public void setLayoutAnimation(RecyclerView view) {
        LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(this, R.anim.layout_animation_fall_down);
        // 为 RecycleView 应用布局动画
        view.setLayoutAnimation(controller);
    }
    

    其实,使用 XML 方式最终还是会通过 AnimationUtils 完成动画文件加载任务,关于这部分我们将在后面的原理部分进行分析。


    动画示例

    下面我通过几个动画案例,来欣赏下利用 LayoutAnimation 实现列表内容的过渡展示效果。

    LayoutAnimation
    1. 从顶部掉入
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="800"
        android:shareInterpolator="@android:anim/decelerate_interpolator">
    
        <translate
            android:fromYDelta="-20%"
            android:toYDelta="0" />
    
        <alpha
            android:fromAlpha="0"
            android:toAlpha="1" />
    
        <scale
            android:fromXScale="105%"
            android:fromYScale="105%"
            android:pivotX="50%"
            android:pivotY="50%"
            android:toXScale="100%"
            android:toYScale="100%" />
    </set>
    
    动画效果

    2. 从底部划入
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="500"
        android:shareInterpolator="@android:anim/accelerate_decelerate_interpolator">
    
        <translate
            android:fromYDelta="50%p"
            android:toYDelta="0" />
    
        <alpha
            android:fromAlpha="0"
            android:toAlpha="1" />
    </set>
    
    动画效果

    3. 从右侧进入
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="800">
    
        <translate
            android:fromXDelta="100%p"
            android:interpolator="@android:anim/decelerate_interpolator"
            android:toXDelta="0" />
    
        <alpha
            android:fromAlpha="0.5"
            android:interpolator="@android:anim/accelerate_decelerate_interpolator"
            android:toAlpha="1" />
    </set>
    
    
    动画效果

    4. 从左侧进入
    <set xmlns:android="http://schemas.android.com/apk/res/android" 
        android:duration="800">
    
        <translate 
            android:fromXDelta="-50%p" 
            android:toXDelta="0"/>
    
        <alpha 
            android:fromAlpha="0.0" 
            android:toAlpha="1.0"/>
    </set>
    
    动画效果

    GridLayoutAnimation
    1. 按行顺序
    <?xml version="1.0" encoding="utf-8"?>
    <gridLayoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:animation="@anim/item_anim_alpha"
        android:columnDelay="0.5"
        android:direction="top_to_bottom|left_to_right"
        android:directionPriority="row" />
    

    渐变效果

    <?xml version="1.0" encoding="utf-8"?>
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="500"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />
    
    动画效果

    2. 按列顺序
    <gridLayoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:rowDelay="75%"
        android:columnDelay="60%"
        android:directionPriority="column"
        android:animation="@anim/slide_in_left"/>
    

    从左侧划入

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="800">
        <translate
            android:fromXDelta="-50%p"
            android:toXDelta="0" />
        <alpha
            android:fromAlpha="0.0"
            android:toAlpha="1.0" />
    </set>
    
    动画效果

    2. 多行平行
    <?xml version="1.0" encoding="utf-8"?>
    <gridLayoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:rowDelay="75%"
        android:columnDelay="60%"
        android:directionPriority="none"
        android:animation="@anim/slide_in_left"/>
    
    动画效果

    扩展

    1. RecyclerView 与 GridLayoutAnimation

    我们知道 RecyclerView 通过布局管理器也可以实现 Grid (网格布局)效果 ,那它能否也可以使用 GridLayoutAnimation 为其添加网格布局动画呢?尝试过的朋友肯定会遇到下面的报错信息:

    java.lang.ClassCastException:
          android.view.animation.LayoutAnimationController$AnimationParameters 
          cannot be cast to android.view.animation.GridLayoutAnimationController$AnimationParameters
    

    从报错信息来看是说 “LayoutAnimationController.AnimationParameters” 不能转换为 “GridLayoutAnimationController.AnimationParameters”,这是怎么回事呢?它的报错位置发生 GridLayoutAnimationController 的 getDelayForView 方法,如下:

    /**
     * 重写自 LayoutAnimationController
     * */
    @Override
    protected long getDelayForView(View view) {
        ViewGroup.LayoutParams lp = view.getLayoutParams();
         // 重点在这里
         // 该 AnimatonParameters 是继承自 LayoutAnimationController内的 AnimationParameters。
         // 由于在 ViewGroup 内默认为其子项添加的是LayoutAnimationController.AnimationParameters,
         // 故此时 ClassCastException
        AnimationParameters params = (AnimationParameters) lp.layoutAnimationParameters;
    
        if (params == null) {
            return 0;
        }
    
        // ... 省略
    }
    

    实际上,在 ViewGroup 内会通过遍历,为每个(直接)子项(View)添加一个布局动画参数 AnimationParameters ,该对象保存在 View 的 LayoutParams 内。而该动画参数默认是 LayoutAnimatonController.AnimationParameters,所以此时会抛出 ClassCastException。关于该部分在后面的原理探索部分会详细分析。

    那 GridView 为什么可以呢?此时大家肯定也能够猜到,没错它通过重写相关方法实现 Grid 类型布局动画效果,如下我们只需要重写 attachLayoutAnimationParameters 方法判断当前是 GridLayoutManager 时返回 GridLayoutAnimationController.AnimationParameters 即可。

    /**
     * 在 RecyclerView 中重写
     */
    @Override
    protected void attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params, int index, int count) {
        // 判断是 GridLayoutManger,也就是网格布局
        if (getLayoutManager() != null && getLayoutManager() instanceof GridLayoutManager) {
            // 创建网格类型动画参数
            GridLayoutAnimationController.AnimationParameters animationParams =
                        (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
            if (animationParams == null) {
                animationParams = new GridLayoutAnimationController.AnimationParameters();
                params.layoutAnimationParameters = animationParams;
            }
            // 列数
            final int numColumns = ((GridLayoutManager) getLayoutManager()).getSpanCount();
            animationParams.count = count;
            animationParams.index = index;
            animationParams.columnsCount = numColumns;
            // 行数 总数除以列
            animationParams.rowsCount = count / numColumns;
            final int invertedIndex = count - 1 - index;
            // 第几列
            animationParams.column = numColumns - 1 - (invertedIndex % numColumns);
            // 第几行
            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / numColumns;
        } else {
            // 否则默认为 LayoutAnimationController.AnimationParameters
            // 该过程在 ViewGroup 中已默认实现
            super.attachLayoutAnimationParameters(child, params, index, count);
        }
    }
    
    2. animateLayoutChanges

    不知大家是否有注意过,Android 系统默认为 ViewGroup 已经实现了一个布局过渡(LayoutTransition)效果,我们只需在相应的 ViewGroup 下开启该配置即可。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        ... 
        // 开启该项
        android:animateLayoutChanges="true"
        android:orientation="vertical">
    

    示例效果如下:

    animateLayoutChanges 与今天分析的 LayoutAnimation 在使用上完全不同,不过它们的目标确是相同的,让布局改变变得更加平滑

    3. 扩展布局动画

    布局动画在设计之初,提供了很好的扩展性,以满足更多定制化需求场景。

    LayoutAnimationController 的 getTransformedIndex(),返回值表示子项(View)播放动画的顺序,该方法被设计成 protected,通过重写该方法实现自定义播放顺序。下面来看下该如何使用它:

    public final class CustomLayoutAnimation extends LayoutAnimationController {
    
        /**
         * LayoutAnimation 默认只有三种顺序,分别对应
         * ORDER_NORMAL  = 0 顺序
         * ORDER_REVERSE = 1 逆序
         * ORDER_RANDOM  = 2 随机
         */
        public static final int ORDER_CUSTOM = -1000;
    
        private CustomIndexListener mIndexCallback;
    
        public interface CustomIndexListener {
    
            int onIndex(CustomLayoutAnimation controller, int count, int index);
        }
    
        public CustomLayoutAnimation(Animation animation) {
            super(animation);
        }
    
        public CustomLayoutAnimation(Animation anim, float delay) {
            super(anim, delay);
        }
    
        public CustomLayoutAnimation(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public void setCustomIndexListener(CustomIndexListener indexCallback) {
            this.mIndexCallback = indexCallback;
        }
    
        /**
         * todo 重点在该方法,自定义每项执行动画的顺序
         */
        @Override
        protected int getTransformedIndex(AnimationParameters params) {
            if (getOrder() == ORDER_CUSTOM && mIndexCallback != null) {
                return mIndexCallback.onIndex(this, params.count, params.index);
            }
            return super.getTransformedIndex(params);
        }
    }
    

    通过复写 getTransformedIndex 方法,添加自定义执行顺序 ORDER_CUSTOM,让 callback 自行控制动画的播放顺序,如此便可以达到任何想要的效果。


    原理探索

    LayoutAnimation 只能应用到 ViewGroup,原因是布局动画属性标签(layoutAnimation/gridLayoutAnimation)只在 ViewGroup 的构造方法中被解析。

    布局动画的创建过程

    下面是 ViewGroup 的构造方法关于布局动画的解析过程:

    private void initFromAttributes(
                Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup, defStyleAttr,
                    defStyleRes);
    
        final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
    
                // ... 省略
    
                // 通过布局设置 LayoutAnimation
                case R.styleable.ViewGroup_layoutAnimation:
                    int id = a.getResourceId(attr, -1);
                    if (id > 0) {
                        // 内部调用了 setLayoutAnimation
                        // 也是通过 AnimationUtils.loadLayoutAnimation方法加载
                           setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, id));
                    }
                    break;
                 //...
                case R.styleable.ViewGroup_animateLayoutChanges:
                    boolean animateLayoutChanges = a.getBoolean(attr, false);
                    if (animateLayoutChanges) {
                        // 每个ViewGroup都有一个默认的LayoutAnimation
                        setLayoutTransition(new LayoutTransition());
                    }
                    break;
                 // ...
            }
        }
        a.recycle();
    }
    

    当解析属性名为 layoutAnimation 时,此时通过 AnimationUtils 加载并创建对应的布局动画,loadLayoutAnimation 方法如下:

    另外我们还可以看到 animateLayoutChanges 属性,如果我们在布局资源中开启,此时 ViewGroup 会默认关联一个 LayoutTransition。

    public static LayoutAnimationController loadLayoutAnimation(Context context, @AnimRes int id)
                throws NotFoundException {
    
        XmlResourceParser parser = null;
        try {
            // 获取资源解析器
            parser = context.getResources().getAnimation(id);
            // 创建布局动画控制器
            return createLayoutAnimationFromXml(context, parser);
        } catch (XmlPullParserException ex) {
            // ... 省略
        } finally {
            if (parser != null) parser.close();
        }
    }
    

    获取动画资源解析器,通过 createLayoutAnimationFromXml 方法解析并创建布局动画控制器:

    private static LayoutAnimationController createLayoutAnimationFromXml(Context c, 
                                                                              XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
        LayoutAnimationController controller = null;
    
        int type;
        int depth = parser.getDepth();
    
        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                    && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            // 获取节点名称
            String name = parser.getName();
            if ("layoutAnimation".equals(name)) {
                // 创建 LayoutAnimationController
                controller = new LayoutAnimationController(c, attrs);
            } else if ("gridLayoutAnimation".equals(name)) {
                // 创建 GridLaoutAnimationController
                controller = new GridLayoutAnimationController(c, attrs);
            } else {
                // 抛出异常
                throw new RuntimeException("Unknown layout animation name: " + name);
            }
        }
       return controller;
    }
    

    从这里我们可以看出,布局动画主要包含两种:layoutAnimation 和 gridLayoutAnimation,它们分别对应的控制器为:

    1. LayoutAnimationController

    2. GridLayoutAnimationController

    控制器有什么作用呢?其实在文章开篇 LayoutAnimation 的自我介绍中:布局动画控制器用于计算每个子项的动画开始执行的偏移时间,下面我以 LayoutAnimationController 为例,从它的构造方法入手分析其实现原理,如下:

    public LayoutAnimationController(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LayoutAnimation);
    
        Animation.Description d = Animation.Description.parseValue(
                    a.peekValue(com.android.internal.R.styleable.LayoutAnimation_delay));
        // 下一个动画的执行时机,如15%,duration=1000ms, 15% * 1000 = 150ms
        mDelay = d.value;
    
        // 执行顺序
        mOrder = a.getInt(com.android.internal.R.styleable.LayoutAnimation_animationOrder, ORDER_NORMAL);
    
        // 拿到动画资源
        int resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_animation, 0);
        if (resource > 0) {
            // 解析动画资源
            setAnimation(context, resource);
        }
        // 获取 Interpolator
        resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_interpolator, 0);
        if (resource > 0) {
            setInterpolator(context, resource);
        }
        a.recycle();
    }
    

    这里我们重点看下布局动画的解析过程 setAnimation 方法,仍然通过 AnimationUtils 完成动画文件的解析。

    public void setAnimation(Context context, @AnimRes int resourceID) {
        // 真正动画资源,还是通过AnimationUtils完成解析
        setAnimation(AnimationUtils.loadAnimation(context, resourceID));
    }
    

    createAnimationFromXml() 将完成动画文件的解析以及创建,具体解析过程如下:

    public static Animation loadAnimation(Context context, @AnimRes int id)
                throws NotFoundException {
    
        XmlResourceParser parser = null;
        try {
            // 获取动画资源解析器
            parser = context.getResources().getAnimation(id);
            // 解析动画资源
            return createAnimationFromXml(context, parser);
        } catch (XmlPullParserException ex) {
            // ... 省略
        } finally {
            if (parser != null) parser.close();
        }
    }
    

    如下,我们可以看到很多熟悉的动画节点名称:set、alpha、scale、rotate 和 translate 等。

    private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
                                                        AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
            Animation anim = null;
    
        // Make sure we are on a start tag.
        int type;
        int depth = parser.getDepth();
    
        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                    && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            // 获取节点名称
            String name = parser.getName();
            // set节点
            if (name.equals("set")) {
                anim = new AnimationSet(c, attrs);
                // 递归调用解析
                createAnimationFromXml(c, parser, (AnimationSet) anim, attrs);
            } else if (name.equals("alpha")) {
                // Alpha 动画
                anim = new AlphaAnimation(c, attrs);
            } else if (name.equals("scale")) {
                // Scale 动画
                anim = new ScaleAnimation(c, attrs);
            } else if (name.equals("rotate")) {
                // Rotate 动画
                anim = new RotateAnimation(c, attrs);
            } else if (name.equals("translate")) {
                // Translate 动画
                anim = new TranslateAnimation(c, attrs);
            } else if (name.equals("cliprect")) {
                // clip rect 动画
                anim = new ClipRectAnimation(c, attrs);
            } else {
                // 不支持的标签类型
                throw new RuntimeException("Unknown animation name: " + parser.getName());
            }
    
            if (parent != null) {
                // 将其添加到AnimationSet
                parent.addAnimation(anim);
            }
        }
        return anim;
    }
    

    至此,布局动画的解析和创建过程我们就已经清楚了。我们知道动画一般只能针对某个 View 操作的。而 LayoutAnimation 可以针对 ViewGroup 的所有(直接)子 View 进行动画操作,既同组 View 的每个 View 按照一定的规则展示动画。那么它是如何实现的呢?下面我们就一起来跟踪下这一过程。


    组动画实现原理

    其实在 ViewGroup 内,系统将动画在绘制阶段先分别设置给了每个子项(View)以实现同组 View 的动画效果。有关 View 的绘制流程你可以参考这里,下面我们来看下这一过程:

    protected void dispatchDraw(Canvas canvas) {
        // ... 省略
    
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    // 为Child设置动画Params
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    // 为每个子View绑定LayoutAnimation
                    bindLayoutAnimation(child);
                }
            }
    
            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }
    
            controller.start();
    
          // ... 省略
    }
    

    可以看到在分发绘制阶段,遍历所有的子 View,这里注意 for 循环内主要完成两项任务:

    • 为当前 View 创建布局动画参数 AnimationParamsters

    • 为每个子项绑定布局动画

    1. 为 View 绑定动画参数

    下面,先来看下布局动画参数 AnimationParameters 的创建过程。attachLayoutAnimationParamsters 方法,还记得上面扩展阶段让 RecycleView 支持 GridLayoutAnimation 吗?其原因就在这里,如下:

    protected void attachLayoutAnimationParameters(View child,
                                                       LayoutParams params, int index, int count) {
        LayoutAnimationController.AnimationParameters animationParams =
                    params.layoutAnimationParameters;
        if (animationParams == null) {
            // 为当前 Child LayoutParams 创建一个动画 Params
            // 注意其默认为:LayoutAnimationController.AnimationParameters
            // 当是 GridLayoutAnimation时,此时需要 GridLayoutAnimationController的AnimationParameters
            animationParams = new LayoutAnimationController.AnimationParameters();
            // 为每个子View设置动画参数,保存在 LayoutParams 内
            params.layoutAnimationParameters = animationParams;
        }
    
        // Child 数量
        animationParams.count = count;
        // 当前 Child的位置
        animationParams.index = index;
    }
    

    该方法主要是为当前 View 创建一个布局动画参数 AnimationParameters(保存在 LayoutParams 内)。内部包含两个参数,组内 View 数量当前 View 下标,AnimationParameters 声明如下:

    public static class AnimationParameters {
       /**
        * 组内 View 数量
        */
        public int count;
    
        /**
         * 当前 View 下标(位置)
         */
        public int index;
    }
    
    

    GrieLayoutAnimationController 继承自 LayoutAnimationController,相应的其内部也需要额外的布局动画参数,故对 LayoutAnimationController.AnimationParameters 进行了扩展,如下:

    public static class AnimationParameters extends
                LayoutAnimationController.AnimationParameters {
       /**
        * 第几列
        * */    
        public int column;
    
       /**
        * 第几行
        */
        public int row;
    
       /**
        * 列数
        */
        public int columnsCount;
    
       /**
        * 行数,总长度除以列数
        */
        public int rowsCount;
    }
    

    2. 为 View 绑定布局动画

    接下来,我们再看下为每个子 View 绑定布局动画的过程。通过 LayoutAniationController 的 getAnimationForView 方法为每个 View 的动画计算其执行的便宜时间 bindLayoutAnimation 方法如下:

    private void bindLayoutAnimation(View child) {
        // getAnimationForView计算动画的偏移时间
        Animation a = mLayoutAnimationController.getAnimationForView(child);
        // 为子View设置动画
        child.setAnimation(a);
    }
    

    还记得上面的扩展阶段,我们可以通过重写 getTransformedIndex() 实现动画任意顺序的执行效果,该部分内容的实现原理如下:

    public final Animation getAnimationForView(View view) {
        // 根据View数量和当前View索引位置,计算View执行动画的偏移时间
        // getStartOffset 是动画首次执行的延迟时间
        final long delay = getDelayForView(view) + mAnimation.getStartOffset();
        // 最大延迟时间
        mMaxDelay = Math.max(mMaxDelay, delay);
    
        try {
            final Animation animation = mAnimation.clone();
            // 设置该动画的开始执行时间
            animation.setStartOffset(delay);
            return animation;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    

    注意 getDelayForView 方法,该方法将完成计算每个子项(View)动画开始时间的偏移量,如下:

    protected long getDelayForView(View view) {
        ViewGroup.LayoutParams lp = view.getLayoutParams();
        // 获取当前View的动画参数
        AnimationParameters params = lp.layoutAnimationParameters;
    
        if (params == null) {
            return 0;
        }
    
        // 计算延迟时间,如总时间1000ms,延迟15%,则delay=150ms
        final float delay = mDelay * mAnimation.getDuration();
        // 根据View位置计算其延迟时间,如3,150ms * 2(下标为2) = 300ms
        final long viewDelay = (long) (getTransformedIndex(params) * delay);
        // 总延迟时间,例如长度为10,此时 150ms * 10 = 1500ms
        final float totalDelay = delay * params.count;
    
        if (mInterpolator == null) {
            // 默认线性差值器,差值器用于调整动画的执行时机
            mInterpolator = new LinearInterpolator();
        }
    
        // 300/1500 = 0.2
        float normalizedDelay = viewDelay / totalDelay;
        // 根据差值器重新计算延迟时间
        normalizedDelay = mInterpolator.getInterpolation(normalizedDelay);
    
        // 重新计算经过差值器调整后的延迟时间
        return (long) (normalizedDelay * totalDelay);
    }
    

    其中 getTransformedIndex 方法控制 View 动画的执行顺序,默认有三种类型:ORDER_REVERSE 顺序执行、ORDER_RANDOM 随机执行、ORDER_NORMAL 顺序执行(默认)。

    protected int getTransformedIndex(AnimationParameters params) {
        switch (getOrder()) {
            case ORDER_REVERSE:
                // 倒序执行
                return params.count - 1 - params.index;
            case ORDER_RANDOM:
                // 随机
                if (mRandomizer == null) {
                    mRandomizer = new Random();
                }
                return (int) (params.count * mRandomizer.nextFloat());
            case ORDER_NORMAL:
                // 默认顺序执行
            default:
                return params.index;
        }
    }
    

    至此,LayoutAnimation 实现组动画的原理我们就算是清楚了,正如开篇 LayoutAnimation 的自我介绍,用于对布局或视图组的子项进行动画处理。每个子项都使用相同的动画,每个子项的动画在不同的时间开始。布局动画控制器用于计算每个子项的动画开始执行的偏移时间


    最后

    布局动画本质仍然要为每个子项单独绑定动画,不过这一过程完全由 ViewGroup 帮我们完成,这样能够有效减少对每个子项(View)设置动画的冗余配置。

    今天所分析的内容虽然比较单一,但是在开发过程中,为快速实现“友好”的视觉效果确是非常实用的。另外一方面,即便是很小的一个功能点 Android 也为我们提供了良好的扩展性,这对我们自己的项目开发是很有指导意义的。

    在 Android 中类似这样的功能点还有很多,欢迎大家分享留言或指正。

    文章如果对你有帮助,请留个赞吧。如果你喜欢我的分析,还可以阅读专题的其他系列文章。


    扩展阅读

    UI 优化系列专题

    存储优化系列专题

    相关文章

      网友评论

        本文标题:LayoutAnimation 炫酷的布局动画及原理分析

        本文链接:https://www.haomeiwen.com/subject/cuzvgctx.html