RecyclerView 与 LayoutAnimation 实现的进入动画(二 ): Grid

原文:Enter animation using RecyclerView and LayoutAnimation Part 2: Grids 

介绍

这是本教程的第二部分,也是最后一部分。第一部分我们讲述了RecyclerView用于列表的例子,文章见:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0807/8348.html

在第一部分中我们演示了如何使用自定义的动画渲染一个RecyclerView的初始加载。采用的是LayoutAnimation,效果也不错。这篇文章我们谈谈如何用类似的方法去处理grid。

本教程的demo项目List和Grid的例子都有,在这里:

patrick-iv/Enter-animation-demo
Enter-animation-demo — Demo project for a blog postgithub.com

apk见 这里!

为什么grid场景下会有所不同?

首先采用第一部分中的方法完全可以在grid上很好的工作,没有crash,动画也是相同的运作方式。但是 LayoutAnimation与使用GridLayoutManager的RecyclerView得到的结果是这样的:

1503131496370933.gif

左边是LayoutAnimation的grid,右边是我们想要的效果。

之所以会这样是因为item的动画是基于它在grid中的线性位置来的(从左到右,从上到下),所以才会制造出奇怪的动画。我们想要的是不同列与行上的item同时运行,这样就能减短持续时间。概括起来就是我们需要对动画的顺序和延迟有更多的控制。

所以开始吧

为了解决这个问题,我们将使用GridLayoutAnimation。基本上它就是一个LayoutAnimation,但是可以为行与列定义delay,同时还允许设置 layout animation的方向。这就使得我们能更好的控制一个特定item的动画,更容易让多个item的动画同时运行。

首先在 res/anim/下创建一个名为grid_layout_animation_from_bottom.xml的文件,然后添加:

<?xml version="1.0" encoding="utf-8"?>
<gridLayoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/item_animation_from_bottom"
    android:animationOrder="normal"
    android:columnDelay="15%"
    android:rowDelay="15%"
    android:direction="top_to_bottom|left_to_right"
    />

其中:

  • android:animation="@anim/item_animation_from_bottom”
    定义布局中的每个item所要应用的动画

  • android:animationOrder="normal" 
    可以选择三种类型:normal, reverse 以及 random。它控制内容动画的顺序。Normal:遵循direction和delay所定义的顺序;Reverse的顺序恰好跟Normal相反;Random为随机的顺序。

  • android:columnDelay=”15%" 
    应用到每列上的动画延迟,定义为item动画持续时间的百分比。

  • android:rowDelay=”15%" 
    应用到每行上的动画延迟,定义为item动画持续时间的百分比。

  • android:direction=”top_to_bottom|left_to_right" 
    定义动画执行的方向。这里动画将从左上角开始,移动到右下角。如果定义为top_to_bottom|right_to_left,那么将从右上角开始移动到左下角。

对于一个GridLayoutAnimation来说,每个item最终的延迟是根据行和列的延迟以及方向计算出来的:

itemAnimationDuration = 300ms
rowDelay              = 10% (30ms)
columnDelay           = 10% (30ms)
direction             = top_to_bottom|left_to_right
   
 +------->
 | +---+---+---+
 | | 0 | 1 | 2 |
 | +---+---+---+
 V | 3 | 4 | 5 |
   +---+---+---+
   | 6 | 7 | 8 |
   +---+---+---+    ROW   COLUMN
0 = 0*30 + 0*30 = 0ms
1 = 0*30 + 1*30 = 30ms
2 = 0*30 + 2*30 = 60ms3 = 1*30 + 0*30 = 30ms
4 = 1*30 + 1*30 = 60ms
5 = 1*30 + 2*30 = 90ms6 = 2*30 + 0*30 = 60ms
7 = 2*30 + 1*30 = 90ms
8 = 2*30 + 2*30 = 120msFinal animation order by delay
+-----+-----+-----+
|  0  | 30  | 60  |
+-----+-----+-----+
| 30  | 60  | 90  |
+-----+-----+-----+
| 60  | 90  | 120 |
+-----+-----+-----+

把行和列的延迟设置为相同值可以让item动画的执行是对称的(沿对角线)。第一部分的item动画 item_animation_from_bottom.xml是如此定义的:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="@integer/anim_duration_long">
    <translate
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromYDelta="50%p"
        android:toYDelta="0"
        />
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        />
</set>

应用 GridLayoutAnimation

GridLayoutAnimation的使用方式跟普通的LayoutAnimation是一样的,可以在java代码中也可以在xml中:

java

int resId = R.anim.grid_layout_animation_from_bottom;
LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(ctx, resId);
recyclerview.setLayoutAnimation(animation);

xml

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"                                        
    android:layoutAnimation="@anim/grid_layout_animation_from_bottom"
    />

但是如果你把GridLayoutAnimation用到标准的RecyclerView中会得到如下的异常:

com.patrickiv.demo.enteranimationdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.patrickiv.demo.enteranimationdemo, PID: 19510
java.lang.ClassCastException: android.view.animation.LayoutAnimationController$AnimationParameters cannot be cast to android.view.animation.GridLayoutAnimationController$AnimationParameters
   at android.view.animation.GridLayoutAnimationController.getDelayForView(GridLayoutAnimationController.java:299)
   at android.view.animation.LayoutAnimationController.getAnimationForView(LayoutAnimationController.java:323)
   at android.view.ViewGroup.bindLayoutAnimation(ViewGroup.java:4584)
   at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3453)
   at android.view.View.draw(View.java:17240)
   at android.support.v7.widget.RecyclerView.draw(RecyclerView.java:3985)
   ...

这是因为RecyclerView是使用LayoutManager来布局自己的子view的,它并不知道LayoutManager如何放置子view。因此RecyclerView不知道到底该把AnimationParameter应用到list上还是grid上,而默认是list。为了修复这个问题我们需要一个自定义的RecyclerView,让它知道GridLayoutManager的存在。

/**
 * RecyclerView with support for grid animations.
 *
 * Based on:
 * https://gist.github.com/Musenkishi/8df1ab549857756098ba
 * Credit to Freddie (Musenkishi) Lust-Hed
 *
 * ...which in turn is based on the GridView implementation of attachLayoutParameters(...):
 * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/GridView.java
 *
 */
public class GridRecyclerView extends RecyclerView {
    /** @see View#View(Context) */
    public GridRecyclerView(Context context) { super(context); }
    /** @see View#View(Context, AttributeSet) */
    public GridRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); }
    /** @see View#View(Context, AttributeSet, int) */
    public GridRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); }
    @Override
    protected void attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params,
                                                   int index, int count) {
        final LayoutManager layoutManager = getLayoutManager();
        if (getAdapter() != null && layoutManager instanceof GridLayoutManager){
            GridLayoutAnimationController.AnimationParameters animationParams =
                    (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
            if (animationParams == null) {
                // If there are no animation parameters, create new once and attach them to
                // the LayoutParams.
                animationParams = new GridLayoutAnimationController.AnimationParameters();
                params.layoutAnimationParameters = animationParams;
            }
            // Next we are updating the parameters
            // Set the number of items in the RecyclerView and the index of this item
            animationParams.count = count;
            animationParams.index = index;
            // Calculate the number of columns and rows in the grid
            final int columns = ((GridLayoutManager) layoutManager).getSpanCount();
            animationParams.columnsCount = columns;
            animationParams.rowsCount = count / columns;
            // Calculate the column/row position in the grid
            final int invertedIndex = count - 1 - index;
            animationParams.column = columns - 1 - (invertedIndex % columns);
            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / columns;
        } else {
            // Proceed as normal if using another type of LayoutManager
            super.attachLayoutAnimationParameters(child, params, index, count);
        }
    }
}

现在唯一的事情就是在xml中把RecyclerView替换成新的GridRecyclerView:

<com.patrickiv.demo.enteranimationdemo.recyclerview.GridRecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layoutAnimation="@anim/grid_layout_animation_from_bottom"
    />

结语

使用GridLayoutAnimation和自定义的GridRecyclerView,我们得到了想要的效果:

左边是LayoutAnimation的grid,右边是我们想要的效果。

代码地址