介绍
这是本教程的第二部分,也是最后一部分。第一部分我们讲述了RecyclerView用于列表的例子,文章见:
在第一部分中我们演示了如何使用自定义的动画渲染一个RecyclerView的初始加载。采用的是LayoutAnimation,效果也不错。这篇文章我们谈谈如何用类似的方法去处理grid。
本教程的demo项目List和Grid的例子都有,在这里:
apk见 这里!
为什么grid场景下会有所不同?
首先采用第一部分中的方法完全可以在grid上很好的工作,没有crash,动画也是相同的运作方式。但是 LayoutAnimation与使用GridLayoutManager的RecyclerView得到的结果是这样的:
左边是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"?>
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"?>
android:duration="@integer/anim_duration_long">
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromYDelta="50%p"
android:toYDelta="0"
/>
android:fromAlpha="0"
android:toAlpha="1"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
/>
应用 GridLayoutAnimation
GridLayoutAnimation的使用方式跟普通的LayoutAnimation是一样的,可以在java代码中也可以在xml中:
javaint resId = R.anim.grid_layout_animation_from_bottom;
LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(ctx, resId);
recyclerview.setLayoutAnimation(animation);
xml
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:
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,右边是我们想要的效果。
代码地址。