RecyclerView的布局原理,你了解吗

共 3175字,需浏览 7分钟

 ·

2021-02-19 17:10

前言

本文主要通过以下两个方面来讲解RecyclerView的布局原理:

  1. 布局放置:dispatchLayout()
  2. 布局填充:onLayoutChildren()

背景知识

RecyclerView的Adapter有几个notify相关的方法:

  • notifyDataSetChanged()
  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
  • notifyItemMoved(int, int)

notifyDataSetChanged()与其他方法的区别:

  1. 会导致整个列表刷新,其它几个方法则不会;
  2. 不会触发RecyclerView的动画机制,其它几个方法则会触发各种不同类型的动画。

1. 布局放置

1.1 核心方法

dispatchLayout()

1.2 作用

把子View 添加并放置到RecyclerView的合适位置、处理动画。动画主要分为五种:

  1. PERSISTENT:针对布局前和布局后都在手机界面上的View所做的动画
  2. REMOVED:在布局前对用户可见,但是数据已经从数据源中删除掉了
  3. ADDED:新增数据到数据源中,并且在布局后对用户可见
  4. DISAPPEARING:数据一直都存在于数据源中,但是布局后从可见变成不可见状态
  5. APPEARING:数据一直都存在于数据源中,但是布局后从不可见变成可见状态

1.3 源码解析

void dispatchLayout(){
  ...
  dispatchLayoutStep1();
  dispatchLayoutStep2();
  dispatchLayoutStep3();
  ...
}

关于dispatchLayoutStepX方法介绍:dispatchLayoutStep1:before(布局前) dispatchLayoutStep2:布局中 dispatchLayoutStep3:after(布局后)。作用描述如下:

方法1:dispatchLayoutStep1()

  1. 判断是否需要开启动画功能
  2. 如果开启动画,将当前屏幕上的Item相关信息保存起来供后续动画使用
  3. 如果开启动画,调用mLayout.onLayoutChildren方法预布局
  4. 预布局后,与第二步保存的信息对比,将新出现的Item信息保存到Appeared中
private void dispatchLayoutStep1() {
  ...
  //第一步 判断是否需要开启动画功能
  processAdapterUpdatesAndSetAnimationFlags();
  ...
  if (mState.mRunSimpleAnimations) {
    ...
    //第二步  将当前屏幕上的Item相关信息保存起来供后续动画使用
    int count = mChildHelper.getChildCount();
    for (int i = 0; i < count; ++i) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
        final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
        mViewInfoStore.addToPreLayout(holder, animationInfo);
    }
    ...
    if (mState.mRunPredictiveAnimations) {
          saveOldPositions();
          //第三步 调用onLayoutChildren方法预布局
          mLayout.onLayoutChildren(mRecycler, mState);
          mState.mStructureChanged = didStructureChange;

          for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
              final View child = mChildHelper.getChildAt(i);
              final ViewHolder viewHolder = getChildViewHolderInt(child);
              if (viewHolder.shouldIgnore()) {
                  continue;
              }
                        //第四步 预布局后,对比预布局前后,哪些item需要放入到Appeared中

              if (!mViewInfoStore.isInPreLayout(viewHolder)) {

                  if (wasHidden) {
                      recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                  } else {
                      mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                  }
              }
          }
          clearOldPositions();
      } else {
          clearOldPositions();
      }
  }

}

方法2:dispatchLayoutStep2

作用:根据数据源中的数据进行布局,真正展示给用户看的最终界面。

private void dispatchLayoutStep2() {
    ...
    // Step 2: Run layout
    mState.mInPreLayout = false;//此处关闭预布局模式
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}

方法3:dispatchLayoutStep3

作用:触发动画。

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        // traverse list in reverse because we may call animateChange in the loop which may
        // remove the target view holder.
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) {
                continue;
            }
            long key = getChangedHolderKey(holder);
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                // run a change animation
                ...
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // Step 4: Process view info lists and trigger animations
        //触发动画
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

  ...
    }

从代码我们可以看出dispatchLayoutStep1和dispatchLayoutStep2方法中调用了onLayoutChildren方法,而dispatchLayoutStep3没有调用。


2. 布局填充

2.1 核心方法

LinearLayoutManager#onLayoutChildren()

2.2 流程说明

以垂直方向的RecyclerView为例子,我们填充RecyclerView的方向有两种,从上往下填充和从下往上填充。开始填充的位置不是固定的,可以从RecyclerView的任意位置处开始填充。流程如下:

  1. 寻找填充的锚点(最终调用findReferenceChild方法)
  2. 移除屏幕上的Views(最终调用detachAndScrapAttachedViews方法)
  3. 从锚点处从上往下填充(调用fill和layoutChunk方法)
  4. 从锚点处从下往上填充(调用fill和layoutChunk方法)
  5. 如果还有多余的空间,继续填充(调用fill和layoutChunk方法)
  6. 非预布局,将scrapList中多余的ViewHolder填充(调用layoutForPredictiveAnimations)

「本文只讲解onLayoutChildren的主流程,具体的填充逻辑请参考https://juejin.cn/post/6904042952377499656」

2.3 源码解析

2.3.1 LinearLayoutManager#onLayoutChildren

  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    //1. 寻找填充的锚点
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    
    ...
    //2. 移除屏幕上的Views
    detachAndScrapAttachedViews(recycler);
    
    ...
    //3. 从锚点处从上往下填充
    updateLayoutStateToFillEnd(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForEnd;
    fill(recycler, mLayoutState, state, false);
    
    ...
    //4. 从锚点处从下往上填充
    // fill towards start
    updateLayoutStateToFillStart(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForStart;
    mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
    fill(recycler, mLayoutState, state, false);
    
    ...
    //5. 如果还有多余的空间,继续填充
    if (mLayoutState.mAvailable > 0) {
        extraForEnd = mLayoutState.mAvailable;
        // start could not consume all it should. add more items towards end
        updateLayoutStateToFillEnd(lastElement, endOffset);
        mLayoutState.mExtraFillSpace = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
    }
  }
    ...
    //6. 非预布局,将scrapList中多余的ViewHolder填充
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    ...

2.3.2 LinearLayoutManager#layoutForPredictiveAnimations

 private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
            RecyclerView.State state, int startOffset,
            int endOffset) {
        //判断是否满足条件,如果是预布局直接返回
        if (!state.willRunPredictiveAnimations() ||  getChildCount() == 0 || state.isPreLayout()
                || !supportsPredictiveItemAnimations()) {
            return;
        }
        // 遍历scrapList,步骤2中屏幕中被移除的View
        int scrapExtraStart = 0, scrapExtraEnd = 0;
        final List scrapList = recycler.getScrapList();
        final int scrapSize = scrapList.size();
        final int firstChildPos = getPosition(getChildAt(0));
        for (int i = 0; i < scrapSize; i++) {
            RecyclerView.ViewHolder scrap = scrapList.get(i);
            //如果被remove掉了,跳过
            if (scrap.isRemoved()) {
                continue;
            }
            //计算额外的控件
                scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);

        }

        mLayoutState.mScrapList = scrapList;
        ...
        // 步骤6 继续填充
        if (scrapExtraEnd > 0) {
            View anchor = getChildClosestToEnd();
            updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
            mLayoutState.mExtraFillSpace = scrapExtraEnd;
            mLayoutState.mAvailable = 0;
            mLayoutState.assignPositionFromScrapList();
            fill(recycler, mLayoutState, state, false);
        }
        mLayoutState.mScrapList = null;
    }

至此,关于RecyclerView的布局逻辑原理讲解完毕。


「Carson每天带你学习一个Android知识点」,长按扫描关注公众号,我们明天见哦!


最后福利:学习资料赠送

  • 福利:由本人亲自撰写 & 整理的「Android学习方法资料」
  • 数量:10名
  • 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」

    点击“在看”就能升职 & 加薪水哦!


浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐