RecyclerView 源码分析之一

简书无法编辑保存了?什么bug?还得回CSDN来写。

写在前面,看RecyclerView源码有点"老虎吃天,无从下口"的感觉。多次提笔想写关于RecyclerView的源码相关文章,最终也是作罢。最大的原因还是感觉RecyclerView的源码太过复杂,怕自己不能胜任。也是走马观花的看了一些网上的博客文章,有的文章看了也不止一遍。自己也就照虎画猫,来记录一下阅读源码的过程。

  • 2024.03.15更新,但是也不是太难下口,哈哈。

分析场景:

  1. RecyclerView 使用 LinearLayoutManager ,从上到下布局,RecyclerView 的 宽高都是 MATCH_PARENT。
  2. 正常设置了LayoutManager和Adapter以后,RecyclerView的measure、layout、draw流程。
  3. 第一次是如何填充子View的。
  4. 在滚动过程中(move和fling)的时候,是如何回收和填充子View的。我们这里不会看回收和填充子View的细节,只会看哪里发生了回收和填充子View调用操作。

先说下分析的结论:

  1. RecyclerView 的 宽高都是 MATCH_PARENT。那么在 onMeasure的时候,是不会调用 dispatchLayoutStep1 和 dispatchLayoutStep2 的。
  2. 第一次设置LayoutManager和Adapter是没有动画效果的,可以认为 dispatchLayoutStep1 和 dispatchLayoutStep3 的 这两个动画动没起作用。
  3. dispatchLayoutStep2 方法,真正起布局作用的方法。内部调用 fill 方法,获取ViewHolder(新创建的,或者从缓存中获取的),测量、布局、添加到 RecyclerView。
  4. 第一次布局的时候,RecyclerView 的 childCount 还是0,是没有ViewHolder的回收和复用的。
    5.就是在滑动和fling的时候,LayoutManager会调用 fill 方法,会获取ViewHolder填充的RecyclerView,并把滑出RecyclerView的ViewHolder回收。
示例代码
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(...);

使用RecyclerView的两部曲。setLayoutManager()setAdapter

RecyclerView的setLayoutManager方法。

public void setLayoutManager(@Nullable LayoutManager layout) {if (layout == mLayout) {return;}//停止滚动stopScroll();//改变LayoutManager依然会重用View。if (mLayout != null) {//...} else {//清除缓存mRecycler.clear();}// this is just a defensive measure for faulty item animators.mChildHelper.removeAllViewsUnfiltered();mLayout = layout;if (layout != null) {//...//LayoutManager关联RecyclerViewmLayout.setRecyclerView(this);if (mIsAttached) {mLayout.dispatchAttachedToWindow(this);}}mRecycler.updateViewCacheSize();//注释1处,调用requestLayout方法requestLayout();
}

注释1处,调用requestLayout方法。

RecyclerView的setAdapter方法。

public void setAdapter(@Nullable Adapter adapter) {// bail out if layout is frozensetLayoutFrozen(false);//注释1处setAdapterInternal(adapter, false, true);processDataSetCompletelyChanged(false);//注释2处,调用 requestLayout 方法requestLayout();
}

注释2处,调用 requestLayout 方法。

RecyclerView的setLayoutManager()setAdapter方法内部都会调用requestLayout方法,请求measure、layout、draw。

RecyclerView的onMeasure方法。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {if (mLayout == null) {//注释1处defaultOnMeasure(widthSpec, heightSpec);return;}//注释2处,是否开启自动测量,我们以LinearLayoutManager来分析,LinearLayoutManager默认是true。if (mLayout.isAutoMeasureEnabled()) {final int widthMode = MeasureSpec.getMode(widthSpec);final int heightMode = MeasureSpec.getMode(heightSpec);//LayoutManager的onMeasure方法默认还是调用了RecyclerView的defaultOnMeasure方法mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);//宽高的测量模式都是EXACTLY或者mAdapter为null直接return,就是使用defaultOnMeasure的测量结果final boolean measureSpecModeIsExactly =widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;if (measureSpecModeIsExactly || mAdapter == null) {//注释2.1处,Note: 注意,当我们在布局里设置 RecyclerView 的宽高为 match_parent 的时候,这里的 widthMode 和 heightMode 都是 MeasureSpec.EXACTLY,会直接returnreturn;}//注释3处,当前布局阶段,默认是State.STEP_STARTif (mState.mLayoutStep == State.STEP_START) {//注释4处,dispatchLayoutStep1();}//为LayoutManager设置测量模式mLayout.setMeasureSpecs(widthSpec, heightSpec);//将RecyclerView的状态置为正在测量mState.mIsMeasuring = true;//注释5处,第二步布局dispatchLayoutStep2();//在dispatchLayoutStep2之后,重新设置LayoutManager的宽高信息mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);// 如果RecyclerView没有准确的宽高信息,或者RecyclerView至少有一个子View没有确定的宽高则重新测量。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 {//自动测量为false,这种场景我们就不看了,一刀切是狠,哈哈。//...}
}

注释2处,是否开启自动测量,我们以LinearLayoutManager来分析,LinearLayoutManager默认是true。

注释2.1处,Note: 注意,当我们在布局里设置 RecyclerView 的宽高为 match_parent 的时候, 这里的 widthMode 和 heightMode 都是 MeasureSpec.EXACTLY,会直接return。

注释3处以及后面的内容先不看,这里留一个问题,为什么会在 onMeasure 的时候,调用 dispatchLayoutStep1(); 和 dispatchLayoutStep2(); 呢???后面再研究,这里先关注主流程。

接下来我们看看RecyclerView的onLayout方法。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);//注释1处,调用dispatchLayout方法。dispatchLayout();TraceCompat.endSection();//注释2处。标记第一次布局完成。mFirstLayoutComplete = true;
}

注释2处,标记第一次布局完成。

注释1处,调用dispatchLayout方法。

/*** 该方法可以看做是layoutChildren()方法的一个包装,处理由于布局造成的动画改变。* 动画的工作机制基于有5中不同类型的动画的假设:* PERSISTENT: 在布局前后,items一直可见。* REMOVED: 在布局之前items可见,在布局之后,items被移除。* ADDED: 在布局之前items不存在,items是被添加到RecyclerView的。* DISAPPEARING: 在布局前后items存在于数据集中,但是在布局过程中可见性由可见变为不可见。(这些items是由于其他变化的副作用而被移动到屏幕之外了)* APPEARING: 在布局前后items存在于数据集中,但是在布局过程中可见性由不可见变为可见。(这些items是由于其他变化的副作用而被移动到屏幕之中了)** 方法的大体逻辑就是计算每个item在布局前后是否存在,并推断出它们处于上述五种状态的哪一种,然后设置不同的动画。* PERSISTENT类型的Views 通过 ItemAnimator 的 animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) 方法执行动画* DISAPPEARING类型的Views 通过 ItemAnimator 的 animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) 方法执行动画* APPEARING类型的Views 通过 ItemAnimator 的 animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) 方法执行动画* REMOVED和ADDED类型 (notifyItemChange 的时候,其实是把老的 itemView 移除了,然后新添加了一个itemView。这个过程就是REMOVED和ADDED类型 )的Views 通过 ItemAnimator 的 animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) 执行动画。*/
void dispatchLayout() {//...mState.mIsMeasuring = false;if (mState.mLayoutStep == State.STEP_START) {//注释1处,调用dispatchLayoutStep1方法。dispatchLayoutStep1();mLayout.setExactMeasureSpecsFrom(this);//注释2处,调用dispatchLayoutStep2方法。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.mLayout.setExactMeasureSpecsFrom(this);dispatchLayoutStep2();} else {mLayout.setExactMeasureSpecsFrom(this);}//调用dispatchLayoutStep3方法。dispatchLayoutStep3();
}

注释1处,调用dispatchLayoutStep1方法。

/*** layout的第一步,在这个步骤会执行以下操作;* - 处理适配器更新* - 决定要运行哪种动画* - 保存当前views的信息* - 如果必要的话,运行预布局(layout)并保存相应的信息*/
private void dispatchLayoutStep1() {mState.assertLayoutStep(State.STEP_START);fillRemainingScrollValues(mState);mState.mIsMeasuring = false;//开始拦截requestLayout的请求,避免多次响应requestLayout的调用,造成多次布局startInterceptRequestLayout();//mViewInfoStore中存储的是要执行动画的Views的相关信息,这里清除mViewInfoStore.clear();onEnterLayoutOrScroll();//注释1处,处理适配器更新和设置动画的标志位。RecyclerView第一次布局,是没有什么动画效果的。processAdapterUpdatesAndSetAnimationFlags();saveFocusInfo();mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;mItemsAddedOrRemoved = mItemsChanged = false;mState.mInPreLayout = mState.mRunPredictiveAnimations;//这里保存了适配器中数据的数量mState.mItemCount = mAdapter.getItemCount();findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);if (mState.mRunSimpleAnimations) {//第一次条件不满足...}if (mState.mRunPredictiveAnimations) {//第一次条件不满足} else {clearOldPositions();}onExitLayoutOrScroll();//停止拦截requestLayout的请求,stopInterceptRequestLayout(false);//将当前布局步骤赋值为State.STEP_LAYOUTmState.mLayoutStep = State.STEP_LAYOUT;
}

第一次调用dispatchLayoutStep1的时候,此时RecyclerView还没有子View所以不会有什么动画执行。方法最后将mState.mLayoutStep置为了State.STEP_LAYOUT

dispatchLayout方法注释2处,调用dispatchLayoutStep2方法。

/*** layout的第二步,在这里我们为最终状态执行View的真正的布局操作。如果必要的话,这个步骤可能会执行多次。*/
private void dispatchLayoutStep2() {startInterceptRequestLayout();onEnterLayoutOrScroll();mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);//消耗所有的延迟更新,这里以后再看mAdapterHelper.consumeUpdatesInOnePass();//获取适配器中数据的数量mState.mItemCount = mAdapter.getItemCount();mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;// 注释1处,将预布局状态置为false。// mInPreLayout 为 false 的时候,不会从 mChangedScrap 中获取 ViewHoldermState.mInPreLayout = false;//注释2处,布局子ViewmLayout.onLayoutChildren(mRecycler, mState);mState.mStructureChanged = false;mPendingSavedState = null;//为falsemState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;//这里将状态置为了State.STEP_ANIMATIONSmState.mLayoutStep = State.STEP_ANIMATIONS;onExitLayoutOrScroll();stopInterceptRequestLayout(false);
}

注释1处,将预布局状态置为false。mInPreLayout 为 false 的时候,不会从 Recycler.mChangedScrap 中查找缓存的 ViewHolder。

注释2处,调用LayoutManager的onLayoutChildren方法。我们直接看LinearLayoutManager的onLayoutChildren方法。

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {// layout的算法:// 1) 通过检查children和其他变量,找到一个锚点坐标和锚点的位置(我认为应该是在adapter中数据对应的位置)。 // 2) 从锚点向上填充RecyclerView。// 3) 从锚点向下填充RecyclerView。// 4) 滚动RecyclerView,做一些显示上的调整。if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {if (state.getItemCount() == 0) {removeAndRecycleAllViews(recycler);return;}}if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {mPendingScrollPosition = mPendingSavedState.mAnchorPosition;}//注释1处,如果mLayoutState为null的话,则创建。ensureLayoutState();mLayoutState.mRecycle = false;//决定布局顺序,是否要倒着布局。LinearLayoutManager默认是从上到下布局。resolveShouldLayoutReverse();//...//这里做了精简,extraForStart和extraForEnd我们都认为是0int extraForStart = 0;int extraForEnd = 0;//...int startOffset;int endOffset;final int firstLayoutDirection;if (mAnchorInfo.mLayoutFromEnd) {firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL: LayoutState.ITEM_DIRECTION_HEAD;} else {//正常情况下,firstLayoutDirection为LayoutState.ITEM_DIRECTION_TAILfirstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD: LayoutState.ITEM_DIRECTION_TAIL;}onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);//注释3处,回收ViewHolder,第一次进来,RecyclerView是没有子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_NaNmLayoutState.mNoRecycleSpace = 0;if (mAnchorInfo.mLayoutFromEnd) {//正常情况为该条件不满足。我们分析else的情况。//...} else {//注释4处,向end方向填充的时候,先计算一些信息。updateLayoutStateToFillEnd(mAnchorInfo);mLayoutState.mExtraFillSpace = extraForEnd;//注释5处,从锚点开始向end方向填充fill(recycler, mLayoutState, state, false);//...}//...//如果必要的话,为预执行动画布局子View。第一次布局的时候,不会有预执行动画。layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);//...}

注释1处,如果 mLayoutState 为null 的话,则创建。布局过程中用来保存布局状态,在布局结束的时候,状态就被重置了。

注释2处,计算锚点位置和坐标。可以简单认为锚点就是RecyclerView的paddingTop,默认为0。

注释3处,回收ViewHolder,第一次进来,RecyclerView是没有子View的,没有回收动作。后面再看。

注释4处,向end方向填充的时候,先计算一些信息。

private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {//默认情况下,anchorInfo.mPosition 是0,anchorInfo.mCoordinate 是0,//在我们这个默认的例子中,anchorInfo.mCoordinate 其实就是 RecyclerView 的 paddingTop,默认是0updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}private void updateLayoutStateToFillEnd(int itemPosition, int offset) {//可填充的像素数,默认就是RecyclerView的高度减去paddingBottom,也就是RecyclerView的可用空间mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;//默认是  LayoutState.ITEM_DIRECTION_TAIL;mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :LayoutState.ITEM_DIRECTION_TAIL;//从前面的分析我们知道,默认itemPosition是0mLayoutState.mCurrentPosition = itemPosition;mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;mLayoutState.mOffset = offset;//注释1处,这里注意一下,mLayoutState将mScrollingOffset置为LayoutState.SCROLLING_OFFSET_NaNmLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}

注释1处,这里注意一下,mLayoutState将mScrollingOffset置为LayoutState.SCROLLING_OFFSET_NaN,后面会用到。

LayoutManager的onLayoutChildren方法的注释5处,调用 fill 方法从锚点开始向end方向(对于默认的LinearLayoutManager,就是从锚点向下)填充RecyclerView,传入的最后一个参数为false注意一下。

fill(recycler, mLayoutState, state, false);

LinearLayoutManager的fill方法。

/*** 神奇的方法:Fills the given layout, defined by the layoutState. * This is fairly independent from the rest of the {@link LinearLayoutManager} * and with little change, can be made publicly available as a helper class.* @param recycler        当前关联到RecyclerView的recycler。* @param layoutState     关于我们应该如何填写可用空间的配置。* @param state           Context passed by the RecyclerView to control scroll steps.* @param stopOnFocusable 如果为true的话,遇到第一个可获取焦点的View则停止填充。* @return 返回添加的像素,对滚动方法有用。*/
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;//...// 注释0处,记录剩余的可填充的空间int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;LayoutChunkResult layoutChunkResult = mLayoutChunkResult;//注释1处,只要还有剩余空间remainingSpace并且还有数据,调用layoutChunk方法,获取ViewHolder 填充RecyclerView。while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {layoutChunkResult.resetInternal();//注释2处,获取并添加子View,然后测量、布局子View并将分割线考虑在内。layoutChunk(recycler, state, layoutState, layoutChunkResult);//如果没有更多View了,布局结束,跳出循环if (layoutChunkResult.mFinished) {break;}//注释3处,增加偏移量,加上已经填充的像素layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null|| !state.isPreLayout()) {//注释4处,可用空间减去已经填充的像素        layoutState.mAvailable -= layoutChunkResult.mConsumed;// we keep a separate remaining space because mAvailable is important for recycling//注释5处,剩余空间,减去已经填充的像素remainingSpace -= layoutChunkResult.mConsumed;}if (stopOnFocusable && layoutChunkResult.mFocusable) {break;}}// 返回已经填充的空间,比如开始可用空间 start 是1920,填充完毕,可用空间 layoutState.mAvailable 是120,就返回 1800 。填充了1800像素。//返回结果有可能大于start,因为最后一个填充的View有一部分在屏幕外面。return start - layoutState.mAvailable;
}

注释0处,记录剩余的可填充的空间。
注释1处,只要还有剩余空间remainingSpace并且还有数据,调用layoutChunk方法,获取ViewHolder 填充RecyclerView。

注释2处,获取并添加子View,然后测量、布局子View并将分割线考虑在内。LinearLayoutManager的layoutChunk方法。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {//注释1处,获取子View,可能是从缓存中或者新创建的View。后面分析缓存相关的点的时候再看。//取数据的顺序 mScrapList -> mRecycler(Recycler#mAttachedScrap或者Recycler#mChangedScrap ->ChildHelper#mHiddenViews -> Recycler#mCachedViews Recycler#mViewCacheExtension -> Recycler#mRecyclerPool ) -> (createViewHolder)View view = layoutState.next(recycler);if (view == null) {//注释2处,如果获取到的子View为null,将LayoutChunkResult的mFinished置为true,没有更多数据了,用于跳出循环然后直接return。result.mFinished = true;return;}RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();if (layoutState.mScrapList == null) {//mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START) ,都等于false,判断两个 false 是否相等,返回trueif (mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START)) {//注释3处,默认是从上到下布局的时候,添加子ViewaddView(view);  } else {//注释4处addView(view, 0);}} //...//注释5处,测量子View的大小,包括margin和分割线。measureChildWithMargins(view, 0, 0);//注释6处,记录该View消耗的高度result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);int left, top, right, bottom;if (mOrientation == VERTICAL) {if (isLayoutRTL()) {right = getWidth() - getPaddingRight();left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);} else {//注释6.1处,获取子View的左右坐标left = getPaddingLeft();right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);}if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {bottom = layoutState.mOffset;top = layoutState.mOffset - result.mConsumed;} else {//注释6.2处,获取子View的上下坐标top = layoutState.mOffset;bottom = layoutState.mOffset + result.mConsumed;}} //...//注释7处,布局子View,并将margin和分割线也考虑在内。layoutDecoratedWithMargins(view, left, top, right, bottom);// Consume the available space if the view is not removed OR changedif (params.isItemRemoved() || params.isItemChanged()) {result.mIgnoreConsumed = true;}result.mFocusable = view.hasFocusable();
}

注释1处,获取子View,可能是从缓存中或者新创建的View。后面分析缓存相关的点的时候再看。

注释2处,如果获取到的子View为null,将LayoutChunkResult的mFinished置为true,用于跳出循环然后直接return。

注释3处,默认是从上到下布局的时候,添加子View。

注释5处,测量子View的大小,包括margin和分割线。

注释6处,记录该View消耗的高度。

注释7处,布局子View,并将margin和分割线也考虑在内。

我们回到 fill 方法的注释3处,增加偏移量,加上已经填充的像素。

注释4处,可用空间减去已经填充的像素。

注释5处,剩余空间,减去已经填充的像素。如果没有剩余空间了或者没有更多View了,fill方法结束。

我们回到LayoutManager的onLayoutChildren方法的注释6处, 向start方向填充的时候,计算一些信息,逻辑和updateLayoutStateToFillEnd类似,不再赘述。

LayoutManager的onLayoutChildren方法的注释7处,调用fill方法继续填充。注意:第一次布局的时候,正常来说我们是从0开始向下填充的,所以就不会有向上填充的情况。我们先忽略。

到这里,dispatchLayoutStep2算是分析完了。接下来是 dispatchLayoutStep3方法。

RecyclerView的dispatchLayoutStep3方法。

/*** 布局的最后一步,在这里我们会保存和动画相关的Views的信息,触发动画并执行必要的清理工作。*/
private void dispatchLayoutStep3() {mState.assertLayoutStep(State.STEP_ANIMATIONS);startInterceptRequestLayout();onEnterLayoutOrScroll();//将状态重置为State.STEP_STARTmState.mLayoutStep = State.STEP_START;//是否执行动画if (mState.mRunSimpleAnimations) {//第一次布局,这个条件不满足//...}//...//标记布局完成。mLayout.onLayoutCompleted(mState);onExitLayoutOrScroll();stopInterceptRequestLayout(false);//清除mViewInfoStoremViewInfoStore.clear();if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {dispatchOnScrolled(0, 0);}recoverFocusFromState();resetFocusInfo();
}

对于第一次布局来说,可以认为 dispatchLayoutStep3 只是标记布局完成,清除 mViewInfoStore(里面保存了动画信息),动画相关信息我们先不看,到这里layout过程结束,下面继续看绘制过程。

RecyclerView#draw(Canvas c)

@Override
public void draw(Canvas c) {super.draw(c);//注释1处,在绘制结束后,调用 onDrawOver 绘制分割线final int count = mItemDecorations.size();for (int i = 0; i < count; i++) {//注释2处mItemDecorations.get(i).onDrawOver(c, this, mState);}
}@Override
public void onDraw(Canvas c) {super.onDraw(c);//先绘制分割线final int count = mItemDecorations.size();for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDraw(c, this, mState);}
}

注释1处,RecyclerView调用了父类的draw方法。其中会先调用onDraw方法,RecyclerView重写了onDraw方法绘制了分割线。然后就是调用dispatchDraw方法在drawChild方法中绘制子View。然后在注释2处,再次绘制分割线。这也是为什么说我们自定义分割线的时候,只要重写ItemDecoration的onDraw或者onDrawOver一个方法就够了。两个都重写的话会导致绘制两次分割线。

第一次 measure,layout,draw 过程结束。平平无奇。

接下来我们看一看在滑动和fling的时候,RecyclerView的一些逻辑。

一句话概括这个过程:就是在滑动和fling的时候,会获取ViewHolder填充的RecyclerView,并把滑出RecyclerView的ViewHolder被回收。

RecyclerView的onTouchEvent方法。

@Override
public boolean onTouchEvent(MotionEvent e) {//...switch (action) {case MotionEvent.ACTION_DOWN: {//...} break;//...case MotionEvent.ACTION_MOVE: {final int index = e.findPointerIndex(mScrollPointerId);final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);int dx = mLastTouchX - x;int dy = mLastTouchY - y;if (mScrollState != SCROLL_STATE_DRAGGING) {boolean startScroll = false;if (canScrollHorizontally) {//横向滑动的忽略...}if (canScrollVertically) {if (dy > 0) {dy = Math.max(0, dy - mTouchSlop);} else {dy = Math.min(0, dy + mTouchSlop);}if (dy != 0) {startScroll = true;}}if (startScroll) {//到达了滑动的条件,将滑动状态置为SCROLL_STATE_DRAGGINGsetScrollState(SCROLL_STATE_DRAGGING);}}if (mScrollState == SCROLL_STATE_DRAGGING) {//...//注释1处,if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,e)) {getParent().requestDisallowInterceptTouchEvent(true);}}} break;case MotionEvent.ACTION_UP: {mVelocityTracker.addMovement(vtev);eventAddedToVelocityTracker = true;mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);final float xvel = canScrollHorizontally? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;final float yvel = canScrollVertically? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;//注释3处,如果速度够了,flingif (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {setScrollState(SCROLL_STATE_IDLE);}resetScroll();} break;}//...return true;
}

无论是滚动还是fling,最终都会调用LinearLayoutManager的scrollVerticallyBy方法。

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,RecyclerView.State state) {//...return scrollBy(dy, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {if (getChildCount() == 0 || delta == 0) {return 0;}ensureLayoutState();mLayoutState.mRecycle = true;//delta > 0,向下填充,layoutDirection为LayoutState.LAYOUT_ENDfinal int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;final int absDelta = Math.abs(delta);//注释0处,更新布局信息。updateLayoutState(layoutDirection, absDelta, true, state);//注释1处,注意,这里继续调用fill方法填充子View。final int consumed = mLayoutState.mScrollingOffset+ fill(recycler, mLayoutState, state, false);if (consumed < 0) {//return 0;}final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;//注释2处,偏移所有的子ViewmOrientationHelper.offsetChildren(-scrolled);if (DEBUG) {Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);}mLayoutState.mLastScrollDelta = scrolled;return scrolled;
}

scrollBy 方法的注释1处,注意,这里继续调用fill方法填充子View。这里现在涉及到到了 ViewHodler的回收和复用哟。关于回收和复用的细节,下篇文章分析。

注释2处,偏移所有的子View,保证RecyclerView的第一个子View的top坐标就是RecyclerView的top坐标减去RecyclerView的paddingTop。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/748588.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java学习笔记(12)

包 导包 Final 不能被改变的&#xff0c;修饰方法 类 变量 方法不能被重写 类不能被继承 变量&#xff0c;赋值一次&#xff0c;变成常量&#xff0c;不能再被赋值 final修饰引用数据类型&#xff0c;地址值不能变&#xff0c;里面的内容可以变 字符串是不可变的 源码中使…

ThreeJs 可视化大屏地图

效果图&#xff1a; 今天给各位提供一个可视化地图的案例和源码&#xff0c;关注下吧各位&#xff01;回复"大屏地图" 获取工程 1、案例分析&#xff1a;主要使用了UI 美工图片&#xff0c;然后获取地图的json 数据绘制图形&#xff0c;贴图使用该区域的地图纹理进行…

基于SpringBoot和Vue的宠物商城网站

目录 一、 绪论1.1 研究背景1.2 研究目的1.3 研究意义 二、相关技术2.1 Spring Boot简介2.2 Vue简介2.3 前后端分离架构概述2.4 其他技术和工具 三、需求分析3.1 功能需求3.1.1 用户模块功能需求3.1.2 商品模块功能需求3.1.3 论坛模块功能需求3.1.4 留言板模块功能需求 3.2 用户…

异常机制总结

异常的本质_调试错误 异常是什么&#xff1f; 软件程序在运行过程中&#xff0c;非常可能遇到刚刚提到的这些问题&#xff0c;我们称之为异常&#xff0c;英文是&#xff1a;Exception&#xff0c;意思是例外。遇到这些例外情况&#xff0c;或者叫异常。 伪代码说明异常机制…

会员项目定价卡css3特效

会员项目定价卡css3特效&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面 下载地址 会员项目定价卡css3特效代码

js静态分析,babel编译,vue的template编译

这三个概念——JavaScript 静态分析、Babel 编译和 Vue 的模板编译——在某种程度上都与代码转换相关&#xff0c;但它们具有不同的作用和实现原理。尽管如此&#xff0c;它们之间确实存在一些共同的地方&#xff1a; 预处理&#xff1a;所有这些技术在将代码或模板用于执行之前…

掌握Go语言:函数精髓,定义、参数、多返回值与应用(14)

在Go语言中&#xff0c;函数是一种独立的代码块&#xff0c;用于执行特定的任务或操作。函数可以接受输入参数并返回结果&#xff0c;也可以不接受任何参数或不返回任何结果。以下是关于Go语言函数的详细解释以及示例代码&#xff1a; 1. 函数的定义和调用 在Go语言中&#x…

谁将是RAG的头部玩家?

开篇 菩提修妙树&#xff0c;接引证法源&#xff0c;屠龙万仙阵&#xff0c;玉虚祭封神。 混战是国内技术圈的常态&#xff0c;在众仙跟风与追捧的大潮中&#xff0c;如何看清方向至关重要&#xff0c;决定谁最终将封神。 语言大模型(LLM)&#xff0c;多模态(MM)&#xff0c;…

代码随想录day22(1)二叉树:左叶子之和(leetcode404)

题目要求&#xff1a;计算二叉树所有左叶子之和 思路&#xff1a;本题应该满足两个条件&#xff0c;一是叶子节点&#xff0c;二是要求是左叶子。所以判断时&#xff0c;如果一个节点左孩子的左右孩子均为空&#xff0c;且其右孩子也为空&#xff0c;此节点的左孩子就为左叶子…

编程语言常识

看图区别编程语言 什么是强类型、弱类型语言&#xff1f;哪种更好&#xff1f; 强类型语言 强类型语言是一种强制类型定义的语言&#xff0c;即一旦某一个变量被定义类型&#xff0c;如果不经强制转换&#xff0c;那么它永远就是该数据类型。 在强类型语言中&#xff0c;变量的…

【洛谷 P8725】[蓝桥杯 2020 省 AB3] 画中漂流 题解(动态规划)

[蓝桥杯 2020 省 AB3] 画中漂流 题目描述 在梦境中&#xff0c;你踏上了一只木䇝&#xff0c;在江上漂流。 根据对当地的了解&#xff0c;你知道在你下游 D D D 米处有一个峡谷&#xff0c;如果你向下游前进大于等于 D D D 米则必死无疑。 现在你打响了急救电话&#xff…

使用sys给python运行程序传参

使用sys给python运行程序传参 import sys def main(argv1,argv2):img1 cv2.imread(argv1)img2 cv2.imread(argv2)finalImage_Stitching().blending(img1,img2)cv2.imwrite(panorama.jpg, final) if __name__ __main__:try: main(sys.argv[1],sys.argv[2])except IndexE…

【Eviews实战】——时序的平稳性检验

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

C++ 入门篇

目录 1、了解C 2、C关键字 2、命名空间 2.1 命名空间的定义 2.2 命名空间的使用 3. C输入与输出 4.缺省参数 4.1 缺省参数的概念 4.2 缺省参数的分类 5. 函数重载 5.1 函数重载的概念 5.2 C中支持函数重载的原理--名字修饰 6. 引用 6.1 引用概念 6.2 引用…

SQLiteC/C++接口详细介绍之sqlite3类(四)

快速跳转文章列表&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;三&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;五&#xff09; 编写不易&#xff0c;有用的朋友点个赞或加粉一下万分感…

day05-SpringBootWeb请求响应

请求响应&#xff1a; 请求&#xff08;HttpServletRequest&#xff09;&#xff1a;获取请求数据响应&#xff08;HttpServletResponse&#xff09;&#xff1a;设置响应数据 BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xf…

Python环境安装及Selenium引入

Python环境安装 环境下载 Download Python | Python.org 环境安装 需使用管理员身份运行 查看环境是否安装成功 python --version 如果未成功则检查环境变量配置 安装 Selenium 库 pip install selenium Selenium 可以模拟用户在浏览器中的操作&#xff0c;如点击按钮、填写…

轻松玩转消息通信:SimpleAmqpClient 和 RabbitMQ 在C++中的终极指南

Rabbmq服务端 安装 这里我使用docker安装rabbitmq服务端,没有安装的就先去看其他的博客安装rabbitmq或者docker #拉取rabbitmq docker pull rabbitmq:management #运行rabbitmq,记得打开防火墙端口 docker run -d --hostname rabbitsrv --name rabbit -p 5672:5672 -p 15672…

前端和后端权限控制【笔记】

前端权限设置【笔记】 前言版权推荐前端权限设置需求效果实现资源 后端权限控制1.给所有前端请求都携带token2.添加拦截器3.配置到WebMvcConfiguration4.更多的权限验证 最后 前言 2024-3-15 18:27:26 以下内容源自《【笔记】》 仅供学习交流使用 版权 禁止其他平台发布时删…

Seata 2.x 系列【11】多数据源分布式事务

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Seata 版本 2.0.0 本系列Spring Boot 版本 3.2.0 本系列Spring Cloud 版本 2023.0.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-seata-demo 文章目录 1. 概述2. 多数据…