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)
LinearLayoutManagervoid 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()等,对视图进行动画处理。
更新视图状态:在动画完成后,需要更新视图的状态,如动画结束标志、动画进度等。
触发动画事件:在动画完成后,可能需要触发一些与动画相关的事件,如动画完成事件、动画取消事件等。

你可能感兴趣的:(Android,组件,android,RecyclerView,动画,原理)