RecyclerView剖析

RecyclerView剖析本文将从 RecyclerView 实现原理并结合源码详细分析这个强大的控件

protected void onMeasure(int widthSpec, int heightSpec) { ... if (mLayout.mAutoMeasure) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); if (skipMeasure || mAdapter == null) { return; } ... dispatchLayoutStep2(); mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); ... } else { ... } } 

这是RecyclerView的测量方法,再看下dispatchLayoutStep2()方法:

private void dispatchLayoutStep2() { ... mLayout.onLayoutChildren(mRecycler, mState); ... } 

上面的mLayout就是一个RecyclerView.LayoutManager实例。通过以上代码(和方法名称),不难推断出,RecyclerView的measure及layout过程委托给了RecyclerView.LayoutManager。接着看onLayoutChildren方法,在兼容包中提供了3个RecyclerView.LayoutManager的实现,这里我就只以LinearLayoutManager来举例说明:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom. ... mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate updateAnchorInfoForLayout(recycler, state, mAnchorInfo); ... if (mAnchorInfo.mLayoutFromEnd) { ... } else { // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; ... } ... } 

在上一段代码中,fill()方法的作用就是填充ItemView,而图(3)说明了,在上段代码中fill()方法调用2次的原因。虽然图(3)是更为普遍的情况,而且在实现填充ItemView算法时,也是按图(3)所示来实现的,但是mAnchorInfo在赋值过程(updateAnchorInfoForLayout)中,只会出现图(1)、图(2)所示情况。现在来看下fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... int remainingSpace = layoutState.mAvailable + layoutState.mExtra; LayoutChunkResult layoutChunkResult = new LayoutChunkResult(); while (...&&layoutState.hasMore(state)) { ... layoutChunk(recycler, state, layoutState, layoutChunkResult); ... if (...) { layoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } } ... } 

下面是layoutChunk()方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); ... if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } ... measureChildWithMargins(view, 0, 0); ... // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout position, we subtract margins. layoutDecorated(view, left + params.leftMargin, top + params.topMargin, right - params.rightMargin, bottom - params.bottomMargin); ... } 

这里的addView()方法,其实就是ViewGroup的addView()方法;measureChildWithMargins()方法看名字就知道是用于测量子控件大小的,这里我先跳过这个方法的解释,放在后面来做,目前就简单地理解为测量子控件大小就好了。下面是layoutDecoreated()方法:

public void layoutDecorated(...) { ... child.layout(...); } 
protected void onLayout(boolean changed, int l, int t, int r, int b) { ... dispatchLayout(); ... } 

这是dispatchLayout()方法:

void dispatchLayout() { ... if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); ... dispatchLayoutStep2(); } dispatchLayoutStep3(); ... } 
@Override public void draw(Canvas c) { super.draw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { 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); } } 
 public void measureChildWithMargins(View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; final int widthSpec = ... final int heightSpec = ... if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); } } 

这里是getItemDecorInsetsForChild()方法:

 Rect getItemDecorInsetsForChild(View child) { ... final Rect insets = lp.mDecorInsets; insets.set(0, 0, 0, 0); final int decorCount = mItemDecorations.size(); for (int i = 0; i < decorCount; i++) { mTempRect.set(0, 0, 0, 0); mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); insets.left += mTempRect.left; insets.top += mTempRect.top; insets.right += mTempRect.right; insets.bottom += mTempRect.bottom; } lp.mInsetsDirty = false; return insets; } 
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { Paint paint = new Paint(); paint.setColor(Color.RED); for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) { final View child = parent.getChildAt(i); float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4; float top = child.getTop(); float right = left + (child.getRight() - child.getLeft()) / 2; float bottom = top + 5; c.drawRect(left,top,right,bottom,paint); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(0, 0, 0, 0); } 
public boolean onTouchEvent(MotionEvent e) { ... if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } ... switch (action) { ... case MotionEvent.ACTION_MOVE: { ... final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f); final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f); int dx = mLastTouchX - x; int dy = mLastTouchY - y; ... if (mScrollState != SCROLL_STATE_DRAGGING) { ... if (canScrollVertically && Math.abs(dy) > mTouchSlop) { if (dy > 0) { dy -= mTouchSlop; } else { dy += mTouchSlop; } startScroll = true; } if (startScroll) { setScrollState(SCROLL_STATE_DRAGGING); } } if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } } } break; ... case MotionEvent.ACTION_UP: { ... final float yvel = canScrollVertically ? -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0; if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetTouch(); } break; ... } ... } 

这里我以垂直方向的滑动来说明。当RecyclerView接收到ACTION_MOVE事件后,会先计算出手指移动距离(dy),并与滑动阀值(mTouchSlop)比较,当大于此阀值时将滑动状态设置为SCROLL_STATE_DRAGGING,而后调用scrollByInternal()方法,使RecyclerView滑动,这样RecyclerView的滑动的第一阶段scroll就完成了;当接收到ACTION_UP事件时,会根据之前的滑动距离与时间计算出一个初速度yvel,这步计算是由VelocityTracker实现的,然后再以此初速度,调用方法fling(),完成RecyclerView滑动的第二阶段fling。显然滑动过程中关键的方法就2个:scrollByInternal()与fling()。接下来同样以垂直线性布局来说明。先来说明scrollByInternal(),跟踪进入后,会发现它最终会调用到LinearLayoutManager.scrollBy()方法,这个过程很简单,我就不列出源码了,但是分析到这里先暂停下,去看看fling()方法:

public boolean fling(int velocityX, int velocityY) { ... mViewFlinger.fling(velocityX, velocityY); ... } 

有用的就这一行,其它乱七八糟的不看也罢。mViewFlinger是一个Runnable的实现ViewFlinger的对象,就是它来控件着ReyclerView的fling过程的算法的。下面来看下类ViewFlinger的一段代码:

void postOnAnimation() { if (mEatRunOnAnimationRequest) { mReSchedulePostAnimationCallback = true; } else { removeCallbacks(this); ViewCompat.postOnAnimation(RecyclerView.this, this); } } public void fling(int velocityX, int velocityY) { setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); postOnAnimation(); } 

可以看到,其实RecyclerView的fling是借助Scroller实现的;然后postOnAnimation()方法的作用就是在将来的某个时刻会执行我们给定的一个Runnable对象,在这里就是这个mViewFlinger对象,这部分原理我就不再深入分析了,它已经不属于本文的范围了。并且,关于Scroller的作用及原理,本文也不会作过多解释。对于这两点各位可以自行查阅,有很多文章对于作过详细阐述的。接下来看看ViewFlinger.run()方法:

public void run() { ... if (scroller.computeScrollOffset()) { final int x = scroller.getCurrX(); final int y = scroller.getCurrY(); final int dx = x - mLastFlingX; final int dy = y - mLastFlingY; ... if (mAdapter != null) { ... if (dy != 0) { vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState); overscrollY = dy - vresult; } ... } ... if (!awakenScrollBars()) { invalidate();//刷新界面 } ... if (scroller.isFinished() || !fullyConsumedAny) { setScrollState(SCROLL_STATE_IDLE); } else { postOnAnimation(); } } ... } 

现在就来看看LinearLayoutManager.scrollBy()方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { ... final int absDy = Math.abs(dy); updateLayoutState(layoutDirection, absDy, true, state); final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); ... final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; mOrientationHelper.offsetChildren(-scrolled); ... } 

如上文所讲到的fill()方法,作用就是向可绘制区间填充ItemView,那么在这里,可绘制区间就是滑动偏移量!再看方法 mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整个滑动过程就分析完成了,当然RecyclerView的滑动还有个特性叫平滑滑动(smooth scroll),其实它的实现就是一个fling滑动,所以就不再赘述了。

View getViewForPosition(int position, boolean dryRun) { ... // 0) If there is a changed scrap, try to find from there if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrap = holder != null; } // 1) Find from scrap by position if (holder == null) { holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun); ... } if (holder == null) { ... // 2) Find from scrap via stable ids, if exists if (mAdapter.hasStableIds()) { holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); ... } if (holder == null && mViewCacheExtension != null) { final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); ... } } if (holder == null) { ... holder = getRecycledViewPool().getRecycledView(type); ... } if (holder == null) { holder = mAdapter.createViewHolder(RecyclerView.this, type); ... } } ... boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { // do not update unless we absolutely have to. holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { ... mAdapter.bindViewHolder(holder, offsetPosition); ... } ... } 
void recycleViewHolderInternal(ViewHolder holder) { ... if (forceRecycle || holder.isRecyclable()) { if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) { // Retire oldest cached view final int cachedViewSize = mCachedViews.size(); if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); } if (cachedViewSize < mViewCacheMax) { mCachedViews.add(holder); cached = true; } } if (!cached) { addViewHolderToRecycledViewPool(holder); recycled = true; } } ... } 

首先调用Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():

public void onItemRangeRemoved(int positionStart, int itemCount) { assertNotInLayoutOrScroll(null); if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { triggerUpdateProcessor(); } } 

这里的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待队列添加一个类型为REMOVE的UpdateOp对象, triggerUpdateProcessor()方法就是调用View.requestLayout()方法,这会导致界面重新布局,也就是说方法RecyclerView.onLayout()会随后调用,这之后的流程就和在绘制流程一节中所描述的一致了。但是动画在哪是执行的呢?查看之前所列出的onLayout()方法发现dispatchLayoutStepX方法共有3个,前文只解释了dispatchLayoutStep2()的作用,这里就其它2个方法作进一步说明。不过dispatchLayoutStep1()没有过多要说明的东西,它的作用只是初始化数据,需要详细说明的是dispatchLayoutStep3()方法:

private void dispatchLayoutStep3() { ... if (mState.mRunSimpleAnimations) { // Step 3: Find out where things are now, and process change animations. ... // Step 4: Process view info lists and trigger animations mViewInfoStore.process(mViewInfoProcessCallback); } ... } 

代码注释已经说明得很清楚了,这里我没有列出step 3相关的代码是因为这部分只是初始化或赋值一些执行动画需要的中间数据,process()方法最终会执行到RecyclerView.animateDisappearance()方法:

private void animateDisappearance(...) { addAnimatingView(holder); holder.setIsRecyclable(false); if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { postAnimationRunner(); } } 

这里的animateDisappearance()会把一个动画与ItemView绑定,并添加到待执行队列中, postAnimationRunner()调用后就会执行这个队列中的动画,注意方法addAnimatingView():

private void addAnimatingView(ViewHolder viewHolder) { final View view = viewHolder.itemView; ... mChildHelper.addView(view, true); ... } 

这里最终会向ChildHelper中的一个名为mHiddenViews的集合添加给定的ItemView,那么这个mHiddenViews又是什么东西?上节中的getViewForPosition()方法中有个getScrapViewForPosition(),作用是从scrapped集合中获取ItemView:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) { ... View view = mChildHelper.findHiddenNonRemovedView(position, type); ... } 

接下来是findHiddenNonRemovedView()方法:

View findHiddenNonRemovedView(int position, int type) { final int count = mHiddenViews.size(); for (int i = 0; i < count; i++) { final View view = mHiddenViews.get(i); RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved() && (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) { return view; } } return null; } 

Written with StackEdit.

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/207834.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午12:59
下一篇 2026年3月19日 下午12:59


相关推荐

  • jQuery cdn加速

    jQuery cdn加速jquery 1 11 0 min jscdnjQuery scriptsrc http libs baidu com jquery 2 0 0 jquery min js 百度 CDN 支持版本 2 0 3 2 0 2 2 0 1 2 0 0 1 11 1 1 10 2 1 10 1 1 10 0 1 9 1 1 9 0 1 8 3 1 8 2 1 8 1 1 8 0 1 7 2 1 7 1 1 7 0 scriptsrc http

    2026年3月18日
    2
  • html数组删除指定元素,js怎么删除数组中指定元素?

    html数组删除指定元素,js怎么删除数组中指定元素?js 中删除数组中指定元素的方法 首先通过循环遍历该数组得到指定元素的索引值 然后根据索引值使用 splice 方法即可删除元素 语法 array splice 索引值 1 JavaScript 中删除数组中指定元素的方法详解 删除数组中的某个元素 首先需要确定需要删除的指定元素的索引值 vararr 1 5 6 12 453 324 functioninde val for va

    2026年3月16日
    2
  • C 控制台应用程序

    C 控制台应用程序学习的第一步就是动手 我们学习 C 语言时 会频繁的使用控制台应用程序 如果一开始就使用带界面的桌面程序来学习 C 会显得比较麻烦 碍事 创建一个简单的控制台应用程序 ConsoleAppli Program cs 选择文件 新建 项目菜单项 创建一个控制台应用程序项目 如下图 在弹出窗口的左侧 选择 VisualC 在中间窗口中选择控制台

    2026年3月19日
    2
  • 操作系统实验报告一 进程调度

    操作系统实验报告一 进程调度实验一进程调度任务一一 实验名称进程调度 代码阅读并调试实验二 实验目的 1 阅读下面源代码 完善程序中填空处内容 2 阅读代码 写出调度算法 算法流程图和程序功能 3 解释数据结构 PCB 的定义和作用 4 为 main 写出每行的注释 5 调试并运行代码 写出结果 进程调度源程序如下 jingchendiao cpp include stdio h include stdlib h include conio h conio h stdlib h

    2026年3月17日
    2
  • 强力卸载OpenClaw:第一批“养虾人”已经卸载的干干净净

    强力卸载OpenClaw:第一批“养虾人”已经卸载的干干净净

    2026年3月12日
    2
  • javaee学生选课系统源码_学生选课管理系统流程图

    javaee学生选课系统源码_学生选课管理系统流程图基于javaweb的ssm学校教务管理系统(管理员,教师,学生)文章结构一、开发框架及业务方向1.开发环境2.开发框架3.整体业务二、项目结构及页面展示1.项目整体结构2.用户页面3.管理员页面***需要源码的加企鹅:671033846;备注CSDN即可******文章结构一、开发框架及业务方向1.开发环境操作系统不限:java特性,一套代码,导出运行jdk版本不限:推荐jdk1.8tomcat版本不限:推荐Tomcat8.0数据库mysql:版本不限,推荐mysql8.0以下开发工具:e

    2022年10月15日
    5

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号