AndroidX RecyclerView总结-测量布局
文章目录
-
- 概述
- 源码探究
-
- 测量阶段
- 布局阶段
-
- dispatchLayout
- dispatchLayoutStep1
- dispatchLayoutStep2
- dispatchLayoutStep3
- LinearLayoutManager
- 总结
概述
通过博文记录RecyclerView的源码学习过程有助于巩固自己的记忆和加深整体实现机制的理解。
RecyclerView中通过Adapter将数据源各item转换成各ViewHolder和监听数据变化。ViewHolder顾名思义持有View,利用ViewHolder将item数据和持有的View进行绑定设置。RecyclerView对各item View的布局排列交由LayoutManager的子类处理,而在布局过程中,又会借助Recycler进行ViewHolder的缓存和复用,达到优化目的。RecyclerView还把item View的动画逻辑解耦至ItemAnimator。
这里从RecyclerView的测量和布局过程入手,了解内部实现机制。
源码探究
文中源码基于 ‘androidx.recyclerview:recyclerview:1.1.0’
测量阶段
打开onMeasure方法:
[RecyclerView.java]
protected void onMeasure(int widthSpec, int heightSpec) { // mLayout为开发者设置的LayoutManager子类 if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; } // ···}
首先判断如果未设置LayoutManager,则采用默认测量规则。
看一下defaultOnMeasure方法:
[RecyclerView.java]
void defaultOnMeasure(int widthSpec, int heightSpec) { // calling LayoutManager here is not pretty but that API is already public and it is better // than creating another method since this is internal. // chooseSize方法中判断SpecMode若为EXACTLY,则取SpecSize;若为AT_MOST,则取 // max(SpecSize, min(Padding和, MinimumWidth));否则取max(Padding和, MinimumWidth) final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), ViewCompat.getMinimumWidth(this)); final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), ViewCompat.getMinimumHeight(this)); // 设置RecyclerView的尺寸 setMeasuredDimension(width, height);}
默认规则会以父容器给定的SpecSize或RecyclerView的Padding与MinimumWidth来计算尺寸。
回到onMeasure方法中:
[RecyclerView.java]
protected void onMeasure(int widthSpec, int heightSpec) { // ··· // 判断LayoutManager是否启用自动测量,isAutoMeasureEnabled默认返回false,但是 // 通常LayoutManager需要重写该方法以返回true,例如LinearLayoutManager、StaggeredGridLayoutManager。 if (mLayout.isAutoMeasureEnabled()) { // ··· } else { // ··· // onMeasure方法中默认有调用了RecyclerView的defaultOnMeasure方法 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); // ··· }}
判断自定义LayoutManager是否重写isAutoMeasureEnabled以返回true。若返回false,则会执行RecyclerView默认测量规则。
继续看onMeasure中isAutoMeasureEnabled为true的流程部分:
[RecyclerView.java]
protected void onMeasure(int widthSpec, int heightSpec) { // ··· if (mLayout.isAutoMeasureEnabled()) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); /** * This specific call should be considered deprecated and replaced with * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could * break existing third party code but all documentation directs developers to not * override {@link LayoutManager#onMeasure(int, int)} when * {@link LayoutManager#isAutoMeasureEnabled()} returns true. */ // 按照默认规则设置一次尺寸(当SpecMode非EXACTLY且数据源不为空时,尺寸未必准确) mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; // 当指定尺寸是明确的或还没有设置数据源时,默认规则设置的尺寸满足,结束测量阶段 if (measureSpecModeIsExactly || mAdapter == null) { return; } // mLayoutStep初始状态为STEP_START if (mState.mLayoutStep == State.STEP_START) { // 执行第一阶段布局---【1】 dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency // 从Spec中获取size和mode,赋值给对应mWidth、mWidthMode、mHeight、mHeightMode成员。 // 当mode为UNSPECIFIED时,若API版本低于23,会将对应对应mWidth、mHeight设为0。 mLayout.setMeasureSpecs(widthSpec, heightSpec); // mIsMeasuring用于标记RecyclerView当前正在计算布局边界 mState.mIsMeasuring = true; // 执行第二阶段布局---【2】(在该阶段中会对child进行测量) dispatchLayoutStep2(); // now we can get the width and height from the children. // 遍历child,计算child布局边界(包含分隔线),设置RecyclerView自身尺寸 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); // if RecyclerView has non-exact width and height and if there is at least one child // which also has non-exact width & height, we have to re-measure. // 判断是否需要二次测量,shouldMeasureTwice默认返回false。 // 自定义LayoutManager可根据自身布局特性重写该方法,当宽高SpecMode都非EXACTLY且有一个child的宽高尺寸也都不是EXACTLY时需要返回true。 if (mLayout.shouldMeasureTwice()) { mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } } else { // ··· }}
可以看到,RecyclerView在测量阶段若启用AutoMeasure就会开始进行一部分布局操作(dispatchLayoutStep1、dispatchLayoutStep2)。
布局分三个阶段,分别对应dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3三个方法。
mState实例为State,用于保存当前RecyclerView状态的有用信息,例如目标滚动位置或视图焦点,还可以保留由资源ID标识的任意数据。它的mLayoutStep用于标记当前布局阶段状态,初始状态为STEP_START,执行dispatchLayoutStep1方法后变更为STEP_LAYOUT,执行dispatchLayoutStep2后变为STEP_ANIMATIONS,执行dispatchLayoutStep3后又变回STEP_START。
布局阶段
进入RecyclerView的onLayout方法:
[RecyclerView.java]
protected void onLayout(boolean changed, int l, int t, int r, int b) { TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); // 派发布局流程 dispatchLayout(); TraceCompat.endSection(); // 标记首次布局完成 mFirstLayoutComplete = true;}
dispatchLayout
接着进入dispatchLayout方法:
[RecyclerView.java]
void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; } mState.mIsMeasuring = false; // 判断布局阶段状态(onMeasure中有可能先执行dispatchLayoutStep1、dispatchLayoutStep2) if (mState.mLayoutStep == State.STEP_START) { // 若测量阶段未进行任何布局操作,则从阶段一开始执行 dispatchLayoutStep1(); // 将生成EXACTLY的MeasureSpec,并调用setMeasureSpecs传入 mLayout.setExactMeasureSpecsFrom(this); // 执行布局阶段二 dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) { // First 2 steps are done in onMeasure but looks like we have to run again due to // changed size. // 若有item的变更或尺寸变化,则再执行一次布局阶段二 mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { // always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } // 执行布局阶段三 dispatchLayoutStep3();}
dispatchLayout方法中会视状态和变化情况依次调用布局阶段。接下来依次看布局的三个阶段方法。
dispatchLayoutStep1
RecyclerView在onMeasure方法中有可能先执行dispatchLayoutStep1和dispatchLayoutStep2,首先看dispatchLayoutStep1方法:
[RecyclerView.java]
private void dispatchLayoutStep1() { // 断言检查当前状态 mState.assertLayoutStep(State.STEP_START); // 更新mState中保存的滚动偏移量 fillRemainingScrollValues(mState); mState.mIsMeasuring = false; // 在requestLayout前调用,用于判断冗余的布局操作,和stopInterceptRequestLayout成对出现 startInterceptRequestLayout(); // ViewInfoStore用于保存动画相关数据,此处先清空数据 mViewInfoStore.clear(); // mLayoutOrScrollCounter递增1,可用于判断当前是否在计算布局,避免在布局和滚动时更改适配器数据源 onEnterLayoutOrScroll(); // 处理适配器数据更新和设置mState动画相关变量 processAdapterUpdatesAndSetAnimationFlags(); // 查找焦点view并设置mState焦点相关变量 saveFocusInfo(); // item动画相关变量设置 mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; mState.mInPreLayout = mState.mRunPredictiveAnimations; // 数据个数 mState.mItemCount = mAdapter.getItemCount(); // 获取最小和最大的ViewHolder布局索引位置保存在mMinMaxLayoutPositions数组中 findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); // 记录item动画相关数据,保存在ViewInfoStore中。 // mRunSimpleAnimations为true时,当适配器数据变化时可能执行item view的淡入/淡出动画。 if (mState.mRunSimpleAnimations) { // Step 0: Find out where all non-removed items are, pre-layout // ··· } // 记录item动画相关数据,保存在ViewInfoStore中。 // mRunPredictiveAnimations为true时,会保存各ViewHolder的旧适配器索引位置,并进行预布局 if (mState.mRunPredictiveAnimations) { // Step 1: run prelayout: This will use the old positions of items. The layout manager // is expected to layout everything, even removed items (though not to add removed // items back to the container). This gives the pre-layout position of APPEARING views // which come into existence as part of the real layout. // Save old positions so that LayoutManager can run its mapping logic. // ··· // we don't process disappearing list because they may re-appear in post layout pass. clearOldPositions(); } else { clearOldPositions(); } onExitLayoutOrScroll(); // 在触发布局的代码之后调用,与startInterceptRequestLayout成对出现。当入参传入true时, // 若标记了推迟布局和未抑制布局则会执行布局操作。 stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT;}
该方法主要记录布局前的状态以及索引位置信息等数据。
dispatchLayoutStep2
接着看dispatchLayoutStep2方法:
[RecyclerView.java]
private void dispatchLayoutStep2() { startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; // Step 2: Run layout mState.mInPreLayout = false; // 真正的布局操作,交由LayoutManager实现,默认为空方法,需要子类重写来实现。 mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; // onLayoutChildren may have caused client code to disable item animations; re-check mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); stopInterceptRequestLayout(false);}
该方法中会通过LayoutManager的onLayoutChildren方法来进行真正的child布局操作,该方法需要由子类重写实现,例如LinearLayoutManager、GridLayoutManager等根据自身排列特性来实现具体的布局逻辑。
dispatchLayoutStep3
进入dispatchLayoutStep3方法:
[RecyclerView.java]
private void dispatchLayoutStep3() { mState.assertLayoutStep(State.STEP_ANIMATIONS); startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.mLayoutStep = State.STEP_START; 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 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); // 若存在变更的ViewHolder(例如添加、移除、变换位置),则需要执行item动画 if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { // run a change animation // If an Item is CHANGED but the updated version is disappearing, it creates // a conflicting case. // Since a view that is marked as disappearing is likely to be going out of // bounds, we run a change animation. Both views will be cleaned automatically // once their animations finish. // On the other hand, if it is the same view holder instance, we run a // disappearing animation instead because we are not going to rebind the updated // VH unless it is enforced by the layout manager. final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); if (oldDisappearing && oldChangeViewHolder == holder) { // run disappear animation instead of change // 将动画信息添加至ViewInfoStore的mLayoutHolderMap成员中保存 mViewInfoStore.addToPostLayout(holder, animationInfo); } else { final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( oldChangeViewHolder); // we add and remove so that any post info is merged. mViewInfoStore.addToPostLayout(holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); if (preInfo == null) { handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); } else { // 提交动画任务执行 animateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); } } } else { mViewInfoStore.addToPostLayout(holder, animationInfo); } } // Step 4: Process view info lists and trigger animations // 遍历ViewInfoStore的mLayoutHolderMap集合中的动画信息,提交动画任务执行 mViewInfoStore.process(mViewInfoProcessCallback); } // 清理、回收、复位等操作 mLayout.removeAndRecycleScrapInt(mRecycler); mState.mPreviousLayoutItemCount = mState.mItemCount; mDataSetHasChangedAfterLayout = false; mDispatchItemsChangedEvent = false; mState.mRunSimpleAnimations = false; mState.mRunPredictiveAnimations = false; mLayout.mRequestedSimpleAnimations = false; if (mRecycler.mChangedScrap != null) { mRecycler.mChangedScrap.clear(); } if (mLayout.mPrefetchMaxObservedInInitialPrefetch) { // Initial prefetch has expanded cache, so reset until next prefetch. // This prevents initial prefetches from expanding the cache permanently. mLayout.mPrefetchMaxCountObserved = 0; mLayout.mPrefetchMaxObservedInInitialPrefetch = false; mRecycler.updateViewCacheSize(); } // 完整的布局流程完成时通知LayoutManager进行相应的清理善后操作 mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mViewInfoStore.clear(); if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { // 若索引位置发生改变,则执行onScrollChanged和onScrolled回调方法 dispatchOnScrolled(0, 0); } // 重新获取item view焦点 recoverFocusFromState(); resetFocusInfo();}
该方法为布局的最后一步,主要处理item动画相关和缓存信息的清理和回收。
LinearLayoutManager
RecyclerView的布局逻辑是委托给LayoutManager来实现,开发者可以自定义LayoutManager来实现自定义布局方式,而Android也提供了几个自定义实现,例如LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,这里以LinearLayoutManager为例,看看布局实现过程。
RecyclerView在布局阶段会调用LayoutManager的onLayoutChildren方法,该方法布局流程大致分为两个步骤:
- 更新锚点索引位置和坐标位置等信息
- 以锚点为基准向顶部和底部方向布局排列
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ··· // 在onSaveInstanceState回调中,会把锚点相关信息保存在SavedState(继承自Parcelable)中, // 在onRestoreInstanceState回调中恢复数据赋值给mPendingSavedState。 // mPendingScrollPosition表示要滚动到的item数据索引位置。 if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler); return; } } if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { mPendingScrollPosition = mPendingSavedState.mAnchorPosition; } ensureLayoutState(); mLayoutState.mRecycle = false; // resolve layout direction resolveShouldLayoutReverse(); // 获取当前ViewGroup中焦点view final View focused = getFocusedChild(); // mAnchorInfo实例为AnchorInfo,用于保存锚点索引位置和坐标位置等信息 if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) { // 若当前锚点信息无效或存在待滚动的目标位置或存在待恢复的数据 mAnchorInfo.reset(); // 标记排列方向是自底向顶还是自顶向底 mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate // 更新锚点信息,保存在mAnchorInfo中 updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused) >= mOrientationHelper.getEndAfterPadding() || mOrientationHelper.getDecoratedEnd(focused) <= mOrientationHelper.getStartAfterPadding())) { // This case relates to when the anchor child is the focused view and due to layout // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows // up after tapping an EditText which shrinks RV causing the focused view (The tapped // EditText which is the anchor child) to get kicked out of the screen. Will update the // anchor coordinate in order to make sure that the focused view is laid out. Otherwise, // the available space in layoutState will be calculated as negative preventing the // focused view from being laid out in fill. // Note that we won't update the anchor position between layout passes (refer to // TestResizingRelayoutWithAutoMeasure), which happens if we were to call // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference // child which can change between layout passes). // 用焦点view位置更新锚点信息 mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); } // ···}
updateAnchorInfoForLayout方法中首先查找待滚动目标位置和待恢复数据更新锚点信息。若不满足,再用焦点view更新锚点信息。仍不满足,则以最靠近布局顶部或底部的可见的item作为锚点。
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ··· // LLM may decide to layout items for "extra" pixels to account for scrolling target, // caching or predictive animations. mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; // 当有滚动时,计算额外的布局空间,顶部/左侧空间存于mReusableIntPair[0],底部/右侧空间存于mReusableIntPair[1] calculateExtraLayoutSpace(state, mReusableIntPair); int extraForStart = Math.max(0, mReusableIntPair[0]) + mOrientationHelper.getStartAfterPadding(); int extraForEnd = Math.max(0, mReusableIntPair[1]) + mOrientationHelper.getEndPadding(); // mPendingScrollPositionOffset表示item view的起始边缘和RecyclerView的起始边缘的偏移量 // 通过scrollToPosition或scrollToPositionWithOffset使滚动到目标索引位置时,会设置mPendingScrollPosition和mPendingScrollPositionOffset if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION && mPendingScrollPositionOffset != INVALID_OFFSET) { // if the child is visible and we are going to move it around, we should layout // extra items in the opposite direction to make sure new items animate nicely // instead of just fading in // 查找要滚动到的目标索引位置的view final View existing = findViewByPosition(mPendingScrollPosition); if (existing != null) { final int current; final int upcomingOffset; // mShouldReverseLayout默认为false if (mShouldReverseLayout) { current = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(existing); upcomingOffset = current - mPendingScrollPositionOffset; } else { // 计算滚动目标view的起始(top/left)边界(包含分割线和margin)和RecyclerView起始端内边距之间的距离 current = mOrientationHelper.getDecoratedStart(existing) - mOrientationHelper.getStartAfterPadding(); // 计算实际偏移量 upcomingOffset = mPendingScrollPositionOffset - current; } if (upcomingOffset > 0) { extraForStart += upcomingOffset; } else { extraForEnd -= upcomingOffset; } } } // ···}
这里主要是当有滚动到指定位置时,预计算布局item用于平滑滚动,计算分配额外空间。
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ··· int startOffset; int endOffset; final int firstLayoutDirection; if (mAnchorInfo.mLayoutFromEnd) { firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; } else { firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; } onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); // Recycler暂时回收ViewHolder和View detachAndScrapAttachedViews(recycler); mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mIsPreLayout = state.isPreLayout(); // noRecycleSpace not needed: recycling doesn't happen in below's fill // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN mLayoutState.mNoRecycleSpace = 0; if (mAnchorInfo.mLayoutFromEnd) { // 先从锚点往上排布item,再从锚点往下排布item // fill towards start // 更新mLayoutState中顶部空间相关信息 updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForStart; // 布局item填充空间 fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } // fill towards end // 更新mLayoutState中底部空间相关信息 updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForEnd; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; // 布局item填充空间 fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; // 判断是否还有剩余空间可以填充item if (mLayoutState.mAvailable > 0) { // end could not consume all. add more items towards start extraForStart = mLayoutState.mAvailable; updateLayoutStateToFillStart(firstElement, startOffset); mLayoutState.mExtraFillSpace = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; } } else { // 先从锚点往下排布item,再从锚点往上排布item // fill towards end // 更新mLayoutState中底部空间相关信息 updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForEnd; // 布局item填充空间 fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } // fill towards start // 更新mLayoutState中顶部空间相关信息 updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; // 布局item填充空间 fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; // 判断是否还有剩余空间可以填充item 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; // 布局item填充空间 fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; } } // ···}
这部分计算锚点距RecyclerView顶部和底部的空间,从锚点开始往两端进行item的布局填充,核心布局方法在fill方法中。
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ··· // changes may cause gaps on the UI, try to fix them. // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have // changed if (getChildCount() > 0) { // because layout from end may be changed by scroll to position // we re-calculate it. // find which side we should check for gaps. // 计算gap, if (mShouldReverseLayout ^ mStackFromEnd) { int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } else { int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } } // 执行item动画相关的布局 layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); if (!state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); } else { mAnchorInfo.reset(); } mLastStackFromEnd = mStackFromEnd; // DEBUG ···}
接下来看fill方法:
[LinearLayoutManager.java]
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // max offset we should set is mFastScroll + available final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; // 用于存储布局执行结果 LayoutChunkResult layoutChunkResult = mLayoutChunkResult; // 还有剩余空间和item数据就不断执行布局 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.beginSection("LLM LayoutChunk"); } // 布局单个View layoutChunk(recycler, state, layoutState, layoutChunkResult); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.endSection(); } if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } } if (DEBUG) { validateChildOrder(); } return start - layoutState.mAvailable;}
该方法中通过循环不断调用layoutChunk方法一个一个View布局。
[LinearLayoutManager.java]
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { // 通过Recycler的getViewForPosition方法获取View View view = layoutState.next(recycler); if (view == null) { if (DEBUG && layoutState.mScrapList == null) { throw new RuntimeException("received null view when unexpected"); } // if we are laying out views in scrap, this may return null which means there is // no more items to layout. result.mFinished = true; return; } RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); // mScrapList默认为空,对于item动画布局时会设置mScrapList if (layoutState.mScrapList == null) { // 判断排布锚点顶部空间时是否反转布局或者锚点底部空间时不反转布局 if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { // 通常情况下在底部空间往RecyclerView添加view时,是逐渐靠近下边界,view添加在最后一个child位置 addView(view); } else { // 通常情况下在顶部空间往RecyclerView添加view时,是逐渐靠近上边界,view添加第一个child位置 addView(view, 0); } } else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { // 添加要消失的view,在动画完成后将会移除 addDisappearingView(view); } else { addDisappearingView(view, 0); } } // 若该child调用了requestLayout,或RecyclerView禁用测量缓存,或分配给child的空间 // 小于之前测量的空间,则调用child.measure再次测量(占用空间包括分隔线和margin) measureChildWithMargins(view, 0, 0); result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); int left, top, right, bottom; // 判断横向排列还是垂直排列 if (mOrientation == VERTICAL) { // 计算分配给child的左上右下四边界 if (isLayoutRTL()) { right = getWidth() - getPaddingRight(); left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); } else { left = getPaddingLeft(); right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { bottom = layoutState.mOffset; top = layoutState.mOffset - result.mConsumed; } else { top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; } } else { top = getPaddingTop(); bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { right = layoutState.mOffset; left = layoutState.mOffset - result.mConsumed; } else { left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; } } // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout position, we subtract margins. // 调用child.layout布局(占用空间包括分隔线和margin) layoutDecoratedWithMargins(view, left, top, right, bottom); if (DEBUG) { Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); } // Consume the available space if the view is not removed OR changed // 判断该view对应的适配器中数据集的item是否删除或者更改 if (params.isItemRemoved() || params.isItemChanged()) { // 标记mIgnoreConsumed为true,外层fill方法中不会减去该view消耗空间 result.mIgnoreConsumed = true; } result.mFocusable = view.hasFocusable();}
该方法首先通过Recycler获取缓存的ViewHolder中的View或Adapter创建ViewHolder绑定View。
获取到View后,通过addView或addDisappearingView添加view到RecyclerView中。通过addDisappearingView添加的view,若其对应的适配器中item数据已删除,则在播放完消失动画后也会被移除。addView或addDisappearingView中经过callback最终调用到ViewGroup的addView和attachViewToParent方法。
在处理完view添加后,会视情况再次调用child.measure进行测量,最后计算四边界调用child.layout进行布局。
总结
RecyclerView的布局核心流程分为三个阶段,对应dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3三个方法,而在测量阶段就会预先执行前两个阶段,Step2执行后RecyclerView会设置自身尺寸。
布局第一阶段主要是记录当前状态和ViewHolder索引位置以及item动画等数据。第二阶段进行真正的布局操作,其具体布局实现由LayoutManager的子类来完成。第三阶段主要进行item动画相关操作和缓存数据清理回收工作。
更多相关文章
- Failed to find provider info for com.example.databasetest.pr
- Flutter 和 Android(安卓)互相传递数据的实现
- 面试必备:Android(安卓)Activity启动流程源码分析
- android 中 Timer 的使用及源码分析
- ORMLite简介和增删改查方法的使用
- android listview setselection 失效解决办法
- Android(安卓)View添加Listener小技巧
- android经典面试题集锦
- android 睡眠和唤醒过程