RecyclerView的布局原理,你了解吗
前言
本文主要通过以下两个方面来讲解RecyclerView的布局原理:
布局放置:dispatchLayout() 布局填充:onLayoutChildren()
背景知识
RecyclerView的Adapter有几个notify相关的方法:
notifyDataSetChanged() notifyItemChanged(int) notifyItemInserted(int) notifyItemRemoved(int) notifyItemRangeChanged(int, int) notifyItemRangeInserted(int, int) notifyItemRangeRemoved(int, int) notifyItemMoved(int, int)
notifyDataSetChanged()与其他方法的区别:
会导致整个列表刷新,其它几个方法则不会; 不会触发RecyclerView的动画机制,其它几个方法则会触发各种不同类型的动画。
1. 布局放置
1.1 核心方法
dispatchLayout()
1.2 作用
把子View 添加并放置到RecyclerView的合适位置、处理动画。动画主要分为五种:
PERSISTENT:针对布局前和布局后都在手机界面上的View所做的动画 REMOVED:在布局前对用户可见,但是数据已经从数据源中删除掉了 ADDED:新增数据到数据源中,并且在布局后对用户可见 DISAPPEARING:数据一直都存在于数据源中,但是布局后从可见变成不可见状态 APPEARING:数据一直都存在于数据源中,但是布局后从不可见变成可见状态
1.3 源码解析
void dispatchLayout(){
...
dispatchLayoutStep1();
dispatchLayoutStep2();
dispatchLayoutStep3();
...
}
关于dispatchLayoutStepX方法介绍:dispatchLayoutStep1:before(布局前) dispatchLayoutStep2:布局中 dispatchLayoutStep3:after(布局后)。作用描述如下:
方法1:dispatchLayoutStep1()
判断是否需要开启动画功能 如果开启动画,将当前屏幕上的Item相关信息保存起来供后续动画使用 如果开启动画,调用mLayout.onLayoutChildren方法预布局 预布局后,与第二步保存的信息对比,将新出现的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的任意位置处开始填充。流程如下:
寻找填充的锚点(最终调用findReferenceChild方法) 移除屏幕上的Views(最终调用detachAndScrapAttachedViews方法) 从锚点处从上往下填充(调用fill和layoutChunk方法) 从锚点处从下往上填充(调用fill和layoutChunk方法) 如果还有多余的空间,继续填充(调用fill和layoutChunk方法) 非预布局,将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名 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」 点击“在看”就能升职 & 加薪水哦!
评论