Android RecyclerView 动画处理 流程 原理(源码分析第三篇)
零、本文主题
上篇文章 Android RecyclerView 动画处理 流程 原理(源码分析第二篇)讲了Recyclerview 动画的实现原理与主要流程。
本文接着上篇文章,分析两个具体一点的流程:
1. Recyclerview 动画执行前的view信息是如何进行保存的,保存了哪些信息
2. 怎样计算“动画执行后,View应该要处于的状态”的信息的?
一、动画执行前的view信息是如何进行保存的,保存了哪些信息
入口:RecyclerView.dispatchLayoutStep1()
调用栈如下:
dispatchLayout()
|
dispatchLayoutStep1()
1.1 dispatchLayout()
dispatchLayout()
主要用处就是 给所有子View做布局,并处理由布局引起的动画改变。
RecyclerView中有 5 种动画:
动画类别 | 布局前后变化 |
---|---|
1 PERSISTENT | visible -> visible |
2 REMOVED | visible -> removed |
3 ADDED | not-exist -> added |
4 DISAPPEARING | (data exist before&after) visible-> non-visible |
5 APPEARING | (data exist before&after) non-visible -> visible |
整体方法就是:梳理出布局前、布局后哪些items存在,并且推断出每个 item 的动画类别(上面的5种之一)然后设置相应的动画。
RecyclerView设置每个 Item 的动画,具体是通过 ItemAnimator 接口的 animateXXX() 系列方法。
1.2 ItemAnimator.animateXXX()
对应上面的五种动画,ItemAnimator接口中,定义了一组 abstract 方法(4个):
animateXXX()方法 | 对应的动画类别 |
---|---|
1. animateDisappearance() | 2 REMOVED 和 4 DISAPPEARING |
2. animateAppearance() | 3 ADDED 和 5 APPEARING |
3. animatePersistence() | 1 PERSISTENT |
4. animateChange() | Adapter.notifyItemChanged(int) 、notifyDataSetChanged() |
我们看下第一个方法:
animateDisappearance(ViewHolder viewHolder,
ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo)
viewHolder 就是目标子view,preLayoutInfo、postLayoutInfo分别对应布局前、布局后 View的信息。
我们梳理一下逻辑:
主要思路是:梳理出布局前、布局后哪些items存在,并且推断出每个 item的动画类别然后设置相应的动画。
具体实现是:执行 animateXXX方法,就能执行动画。但是 animateXXX需要preLayoutInfo, postLayoutInfo 这两个view的信息,只要提供了preLayoutInfo, postLayoutInfo流程就通了。
所以,我们的关键着眼点就在于 这个preLayoutInfo, postLayoutInfo 是怎么生成的。
搞清了这个问题,本文的主题就解决了。
1.3 preLayoutInfo、postLayoutInfo的生成
preLayoutInfo、postLayoutInfo 分别是由 ItemAnimator 里的这两个方法生成的:
- ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
RecyclerView.ViewHolder viewHolder,
int changeFlags,
List<Object> payloads )
- ItemHolderInfo recordPostLayoutInformation(RecyclerView.State state,
RecyclerView.ViewHolder viewHolder )
它的调用入口在:RecyclerView.dispatchLayoutStep1()
private void dispatchLayoutStep1() {
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
生成preLayoutInfo 并存入到 ViewInfoStore中,需要的时候再取出来。
至此,我们对接上了上一篇文章 的 2.3 章节。
1.4 ItemAnimator.recordPreLayoutInformation()
中间略过,最后调到 类:ItemAnimator.ItemHolderInfo:
public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder, int flags) {
final View view = holder.itemView;
this.left = view.getLeft();
this.top = view.getTop();
this.right = view.getRight();
this.bottom = view.getBottom();
return this;
}
其实就是存一下viewholder的 4 个顶点坐标。很朴素。
二、计算“动画执行后,View应该要处于的状态”的信息 postLayoutInfo
2.1 ItemAnimator.recordPostLayoutInformation()
这个方法在RecyclerView中,只有一次调用:
dispatchLayoutStep3()
|
recordPostLayoutInformation()
抽下核心方法:
void dispatchLayoutStep3() {
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
mViewInfoStore.addToPostLayout(holder, animationInfo);
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
怎样计算“动画执行后,View应该要处于的状态”的信息的?
2.2 dispatchLayoutStep2()
方法注释是这么写的:
第二个布局步骤,我们为最终状态做视图的实际布局。如果需要,此步骤可能会运行多次(例如测量)。
请注意加粗部分,这个 dispatchLayoutStep2() 方法就是真正布局的地方,在这个方法中所有的子View会被add到 RV中,布局完成后,我们就很容易得到一个item他的动画终点在哪里。
所以,我们上面的问题:怎样计算“动画执行后,View应该要处于的状态”的信息的?
> 这个提出问题的思路是对的,但是这个PostLayoutInformation 不是计算得来的,而是布局完成后,直接去取就行了。
我把这部分的核心方法拉出来,大家顺着这个思路追一下:
RV.dispatchLayoutStep2()
|-- mLayout.onLayoutChildren(mRecycler, mState);
LinearLayoutManager.onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
|--fill(recycler, mLayoutState, state, false);
|--layoutChunk(recycler, state, layoutState, layoutChunkResult)
【LinearLayoutManager】
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
这块调了addView方法,也是真正布局的地方。子view被添加后,它的坐标位置就出来了,直接取出来放到postLayoutInfo就完事了。
2.3 先布局,后执行动画?
当你看到这个地方的时候,不知道有这样的问题:
dispatchLayoutStep2() 方法中,调用了addView,子view就显示出来了。然后再在dispatchLayoutStep3() 执行动画。画面不是就乱了吗?
> 比如,添加一个View,View已经先显示出来了,然后又执行一个渐变动画显现出来,效果不就错了吗?
这里我犯了一个先入为主的错误,以前使用LinearLayout的时候,可能会直接addView添加一个子View。但是LinearLayout的addView与RV的addView实现是不一样的。
public class LinearLayout extends ViewGroup {
LinearLayout中没有addView方法,使用的是父类ViewGroup的方法:
public void addView(View child, int index, LayoutParams params) {
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
LinearLayout的 addView 调用了 invalidate(true) 使组件重绘,把子view显示出来。
再看一下RV的addView:
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
} else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
mRecyclerView.mLayout.moveView(currentIndex, index);
} else {
mChildHelper.addView(child, index, false)
}
if (lp.mPendingInvalidate) {
holder.itemView.invalidate();
lp.mPendingInvalidate = false;
}
}
上面也只抽了主要方法出来,我们可以看出:
调的是mViewInfoStore,mChildHelper.attachViewToParent,mLayout.moveView,mChildHelper.addView,itemView.invalidate()等等,并没有调用RV自己的invalidate。所以RV的addView执行完,并不会立即显示子View。
总结一下:
- 在 RecyclerView 中,当你使用 addView() 方法添加一个新的视图时,这个视图并不会立即显示在屏幕上。
- RecyclerView 的布局和动画流程是异步执行的。当你调用 addView() 方法时,这个视图会被添加到 RecyclerView 的内部,但并不会立即显示。实际的显示过程是在 RecyclerView 完成布局和动画计算之后进行的。
- 如果你希望新添加的视图立即显示在屏幕上,你可以调用 invalidate() 方法来强制 RecyclerView 重新绘制自己。这样,新的视图会立即显示出来,但需要注意的是,这可能会导致 RecyclerView
的重新布局和动画计算,可能会对性能产生一定的影响。
三、整体逻辑
计算布局参数:根据布局规则和数据,计算每个视图的布局参数,如位置、大小、方向等。
执行布局操作:根据计算出的布局参数,对每个视图进行布局操作,如测量视图尺寸、绘制视图等。
更新视图状态:在布局完成后,需要更新视图的状态,如可见性、焦点等。
触发布局事件:在布局完成后,可能需要触发一些与布局相关的事件,如滚动事件、动画事件等。
处理动画逻辑:在布局过程中,如果需要对视图进行动画处理,则需要执行以下步骤:
记录动画信息:使用ItemAnimator记录动画信息,包括动画类型、目标视图、动画持续时间等。
判断是否需要动画:根据动画信息和视图的状态,判断是否需要执行动画。
执行动画:如果需要执行动画,则调用相应的动画方法,如animateChange()、animateAdd()等,对视图进行动画处理。
更新视图状态:在动画完成后,需要更新视图的状态,如动画结束标志、动画进度等。
触发动画事件:在动画完成后,可能需要触发一些与动画相关的事件,如动画完成事件、动画取消事件等。