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

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 优化系列专题

存储优化系列专题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350