一、背景
作为 Android 运用开发中不可或缺的UI组件,RecyclerView 相关于传统的 ListView 在可定制性、功用和扩展性方面都有了巨大的进步。它选用了视图收回重用机制,能够展现极许多的数据,并支撑线性布局、网格布局和瀑布流布局等各种不同的布局办法。作为一名 Android 开发者,把握 RecyclerView 的技能是十分必要的。
想深化了解RecyclerView的完结原理和运用办法,咱们能够由浅入深地学习。首先要了解RecyclerView的全体结构,其间布局办理器是一个必不可少的组件。经过简略的介绍结构供给的自带布局办理器来作为学习RecyclerView的第一步。接着,当咱们要想创立一个自界说布局办理器,需求了解组件之间是怎么相互协作以及RecyclerView的完结原理,例如滑动、丈量、布局和缓存处理等细节问题;终究,经过以上的学习和了解,咱们能够对 RecyclerView 的全体结构有更深化的了解。话不多说,让咱们开端吧!
二、framework供给的布局办理器
为了在实践开发中满意不同的需求,RecyclerView供给对可复用的调集创立自界说的布局、动画能够愈加灵敏易用运用复用机制。能够看到咱们常见的运用RecyclerView代码中有必要供给一个布局办理器。为了应对研发中会遇到的常见场景,结构为咱们供给了几个自带的布局办理器,能处理大多数状况下的问题,咱们先对它们进行简略的了解,后续以LinearLayoutManager的源码为例展开咱们对LayoutManager完结原理的学习。
LinearLayoutManager
LinearLayoutManager是一个列表项均匀分布的单一列表,基本上能够当做ListView的替代品。与ListView不同的是,它具有设定笔直或水平方向参数的特性,只需求在实例时指定笔直或水平方向的参数即可。这使得开发者能够愈加灵敏地操控列表的排版办法
LinearLayoutManager manager = new LinearLayoutManager(
this,LinearLayoutManager.VERTIVAL,
false);//是否需求逆序布局
GridLayoutManager
GridLayoutManager由均分的网格布局列表项调集组成,和LinearLayoutManager相同具有指定方向的参数,除此之外供给了操控每行item所能占用的最大span数。例如鄙人图中span被界说为2,每行就被均分成了两部分。而经过span是无法将每行不平等分(1/3、2/3),GridLayoutManager供给了一个相当有意思的特性SpanSizeLook,能够让咱们对平等性质进行改动。除此之外,GridLayoutManager不管其运用何种的方向,其表项的高度(笔直)或宽度(水平)在所占的块中有必要是平等的,也意味着行高取决于最高的item的高度,那么当一行中有一个item比其他的高,则会留下空白。那么StaggeredGridLayoutManager正好处理了这个问题。
GridLayoutManager manager = new GridLayoutManager(
this,
2,
GridLayoutManager.VERTIVAL,
false,
);
manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLook(){
@Override
public int getSpanSize(int position){
return(position%3==0?2:1)
}
});
StaggeredGridLayoutManager
StaggeredGridLayoutManager具有更广泛的灵敏性和布局自由度,能够主动地调整不同item的高度,以便彻底填充整个布局。它能够为每个item设置不同的高度,使得在布局中构成一个错落有致的视觉作用。
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(
2,//网格列数
StaggeredGridLayoutManager.VERTIVAL,
);
三、中心组件
那除了以上供给的已有的,想完结一个在framework中找不到,就需求咱们自己手动去创立布局办理器了。在创立归于咱们自己的布局办理器之前咱们需求对其间运用到的各个组件有一个基本认知:
- Recycler:是构建自界说的布局办理器的中心协助类,简直干了一切的获取视图、缓存视图等和收回相关的活动。能让RecyclerView能快速的获取一个新视图来填充数据或许快速丢掉不再需求的视图。
- Adapter:是一切数据的来源,担任供给数据并创立ViewHolders以及将数据绑定到ViewHolders上的重要组件,能够视为是recycler对外的作业的对接者
- ViewHolder:存取状况信息,在recycler内部也对viewHolder进行了状况信息的存取(是否正在被改动,是否被删除或增加)
- LayoutManager:决议RecyclerView中Items的摆放办法,包含了Item View的获取与收回;当数据集发生改变时,LayoutManager会主动调整子view的方位和巨细,以保证RecyclerView的正常显现和功用。
Recycler
这个类体现了收回运用的概念,对视图的收回运用机制使设备能够展现十分大的数据调集的数据项给用户。但无需创立跟数据项个数相同多的视图来展现数据,仅仅用有限的几个视图便可到达意图。一旦用户经过上下滑开端跟数据集交互。当这个视图被划出屏幕,这个视图就不需求了。关于将要到来的新数据,不选用创立一个新视图的办法 而是把旧视图复用填充到一个新的方位,然后让它划入屏幕即可。在这个进程中一方面咱们从recycler中获取视图,也会将抛弃的视图丢给recycler。而recycler有两种级别的机制来对数据进行收回运用。
Scrap Heap能够视为是第一道防地,是一种轻量级的操作,会检索视图和丢掉视图,从中获取数据不需求从头绑定数据(也便是能够不走适配器),所以一般咱们都是先用Scrap Heap来存丢掉的视图,假如你确认某些视图丢掉后永远用不上时,Recycle Pool派上了用场,咱们能从中快速获取视图可是获取到的视图的meta-data丢失了,需求从适配器中从头绑定数据,更具体的收回流程将在后续的章节介绍。与之对应的, 假如你想要暂时收拾而且希望稍后在同一布局中从头运用某个 View 的话, 能够对它调用 detachAndScrapView()
,该办法的作用是将指定的View从RecyclerView中别离(Detach)而且增加到RecyclerView的收回池中(Scrap)。假如依据当时布局 你不再需求某个 View 的话,对其调用 removeAndRecycleView()
,该办法的作用是移除指定的View,并将该View增加到Recycler Pool中。
Adapter
Adapter经过承继RecyclerView.Adapter类,并重写其间的办法来完结RecyclerView的功用。其间有三个重要的办法:onCreateViewHolder
、onBindViewHolder
和getItemCount
。onCreateViewHolder办法在RecyclerView初始加载或许需求新的ViewHolder时被调用,它创立ViewHolder并回来。onBindViewHolder办法则将ViewHolder中的数据绑定到对应的视图中。getItemCount办法回来adapter数据列表的长度。
ViewHolder作为RecyclerView的视图保持者,其首要作用是进步RecyclerView的功用,它承继自RecyclerView.ViewHolder类,而且重写其间的办法,例如onBindViewHolder办法用于将数据与视图绑定。ViewHolder中的数据来自于Adapter中的model。经过ViewHolder的重用,RecyclerView不再需求重复创立视图,然后大大进步了功用。ViewHolder的数据绑定只需求更新数据,而不需求从头创立整个视图。这样就能够防止频频地调用findViewById等办法,然后进步RecyclerView的流畅度和响应速度。
LayoutManager
LayoutManager则决议了RecyclerView中子视图的摆放办法、方向、方位等等。它的作业进程涉及到measure和layout操作。在measure进程中,LayoutManager会遍历RecyclerView中的一切子视图,并确认它们的尺度巨细。LayoutManager运用子视图本身的measure办法取得它们的希望尺度,再对应的给子View分配合理的空间,保证子视图能够合理地显现出来。
在layout进程中,LayoutManager会确认子视图在RecyclerView中的方位,并将它们放置到相应的方位上。这个进程也涉及到指定LayoutManager的摆放办法,除此之外LayoutManager还支撑Decorations。Decorations能够在RecyclerView中增加额定的边距、分隔符和间隔等,然后进步了RecyclerView的外观和功用。Decorations是运用在一切RecyclerView元素上的,LayoutManager不需求关心子视图的边距问题,API会主动核算并运用Decorations的作用。
四、LayoutManager完结原理
制作流程
布局办理器 顾名思义,作用便是担任measure和layout的。RecyclerView作为ViewGroup的子类,也是经过 onMeasure() 来完结丈量作业的,那咱们就以onMeasure()
为切入点来了解LayoutManager的制作流程:
默许敞开了该主动丈量功用也便是mLayout.mAutoMeasure的值为true,此刻RecyclerView会在不影响功用的前提下主动丈量并更新一切可见item的巨细和方位。若有需求也能够在重写LayoutManager时经过setAutoMeasureEnabled(false)
关闭主动布局。能够看到在敞开主动布局的状况下,是经过dispatchLayoutStep1 ,dispatchLayoutStep2 两个办法,Setp1是进行预布局,Setp2循环对子布局进行丈量和布局,除这两个之外还有用来实践履行动画的dispatchLayoutStep3,经过mLayoutStep
判别布局履行的状况。
丈量完RecyclerView和子View的巨细,就要调用onLayout办法对一切子View进行布局。在onLayout()
中首要是经过dispatchLayout()
办法来对可见的item进行布局:
void dispatchLayout() {
if (mAdapter == null) {//没有设置adapter,回来
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {//没有设置LayoutManager,回来
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
//在onMeasure阶段,假如宽高是固定的,那么mLayoutStep == State.STEP_START
// 而且dispatchLayoutStep1和dispatchLayoutStep2不会调用
//所以这儿就会调用一下
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
//在onMeasure阶段,假如履行了dispatchLayoutStep1,可是没有履行dispatchLayoutStep2,就会履行dispatchLayoutStep2
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
//终究调用dispatchLayoutStep3
dispatchLayoutStep3();
}
从描绘中就能够看出来,在这3个过程中,step2便是履行了最重要的子View的丈量布局的一步,比较要害的就两点。第一点关系到当咱们在写自界说布局时有必要要重写的一个办法onLayoutChildren()
,来规定放置子 view 的算法寻找锚点填充 view;第二点便是将 mState.mLayoutStep
置为 State.STEP_ANIMATIONS,经过mLayoutStep就知道 layout 这个进程进行到哪一步了。
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
...
mLayout.onLayoutChildren(mRecycler, mState);
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
...
}
例如在快手的注册界面会有推荐昵称这样的流式布局,在网上查找Flowlayout完结会发现大多的最佳实践会告诉你完结过程:
- 承继ViewGroup
- 重写 onMeasure,核算子控件的巨细然后确认父控件的巨细
- 重写 onLayout ,确认子控件的布局 以上仅仅是完结丈量和布局,假如要完结动态的数据增加,参阅鸿洋的FlowLayout也是以setAdapter方式注入数据
而经过创立自界说布局办理器只需求重写的一个onLayoutChildren()
,此刻子View的丈量和布局作业都将由LayoutManager完结
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);//暂时别离并抛弃一切当时附加的子视图
int sumWidth = getWidth();
int curLineWidth = 0, curLineTop = 0;
int lastLineMaxHeight = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);//Recycler中获取合适的View
addView(view);
measureChildWithMargins(view, 0, 0);//度量子视图
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
//换行战略:标签放不下时的剩下宽度超越100dp时不换行,文字省掉
boolean exceedSumWidth = curLineWidth + width > sumWidth;
if (!exceedSumWidth || (sumWidth - curLineWidth) >= CommonUtil.dip2px(100)) {
//不需求换行
if (exceedSumWidth) {
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
layoutParams.width = sumWidth - curLineWidth;
view.setLayoutParams(layoutParams);
}
//依据边距等数据 制作给定的子View
layoutDecorated(view, curLineWidth, curLineTop, Math.min(curLineWidth + width, sumWidth),
curLineTop + height);
curLineWidth += width;
lastLineMaxHeight = Math.max(lastLineMaxHeight, height);
} else {
curLineWidth = width;
if (lastLineMaxHeight == 0) {
lastLineMaxHeight = height;
}
curLineTop += lastLineMaxHeight;
layoutDecorated(view, 0, curLineTop, width, curLineTop + height);
lastLineMaxHeight = height;
}
}
}
对以上办法进行拆分其实中心就做了2件事:
第一步:把一切的item所对应的view加进来:
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);//Recycler中获取合适的View
addView(view);
...
}
第二步:把一切的Item摆放在它应在的方位:
for (int i = 0; i < getItemCount(); i++) {
...
measureChildWithMargins(view, 0, 0);//度量子视图
//依据边距等数据 制作给定的子View
layoutDecorated(view, curLineWidth, curLineTop, Math.min(curLineWidth + width, sumWidth),
...
}
完结了子view的丈量和布局后,布局的填充其实是交由一个重要的办法fill()
,怎么去运转一次布局、怎么去构建一个布局办理器的概念都是依据它:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
//找到第一个可视元素(锚点)
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
...
//将滑出屏幕的View收回掉
recycleByLayoutState(recycler, layoutState);
}
//剩下制作空间=可用区域+扩展空间。
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环布局直到没有剩下空间了或许没有剩下数据了
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//*要点 增加一个child,然后将制作的相关信息保存到layoutChunkResult
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {//假如布局结束了(没有view了),退出循环
break;
}
//依据所增加的child消费的高度更新layoutState的偏移量。mLayoutDirection为+1或许-1,经过乘法来处理是从底部往上布局,还是从上往底部开端布局
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
//消费剩下可用空间
remainingSpace -= layoutChunkResult.mConsumed;
}
...
}
//回来本次布局所填充的区域
return start - layoutState.mAvailable;
}
简略来说fill便是对布局当时的子View进行枚举,依据视图的状况来决议哪个视图会是布局的第一个可见视图,能够运用第一个可视视图的方位和偏移量等初始信息,从初始方位开端顺次往后布局,核算Gap得知后边没有剩下的空间。期间在layoutChunk()
办法中不断的从recycler中取出视图,然后对视图进行layout,这样就无需考虑哪一个是第一个视图,以及后边的视图间隔第一个视图的间隔有多远:
- 找到第一个可视元素
- 核算Gap
- 将一切视图Scrap掉,丢给recycler
public void layoutChunk() {
...
View view = layoutState.next(recycler); //调用了getViewForPosition()
addView(view); //加入View
measureChildWithMargins(view, 0, 0); //核算View的巨细
layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View
...
}
layoutChunk()这儿首要做了5个处理:
- 经过 layoutState 获取要展现的View,
next()
实践上调用了getViewForPosition(currentPosition)
,该办法是从RecyclerView的收回机制完结类Recycler中获取合适的View - 经过
addView
办法将子View增加到布局中 - 调用
measureChildWithMargins
办法丈量子View - 调用
layoutDecoratedWithMargins
办法布局子View - 依据处理的结果,填充LayoutChunkResult的相关信息,以便回来之后,能够进行数据的核算。
总的来说onLayoutChildren()
是对RecyclerView进行布局的进口办法,也是咱们自界说布局办理器有必要重写的办法,而fill()
则是实践担任填充视图的中心办法。这样重写onLayoutChildren()后item就能简略的显现出来了,现在还不能滑动,假如咱们要给它增加上滑动,咱们接着往下看RecyclerView的滑动原理。
滑动原理
完结了布局的制作后,别忘了RecyclerView 是一个展现列表的控件,它的滑动原理也是咱们需求把握的。RecyclerView的滑动事情处理是经过onTouchEvent()
触控事情响应的。咱们就以RecyclerView.OnTouchEvent()为切入点来看看RecyclerView的滑动原理。onTouchEvent()办法中逻辑庞杂,且包含MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_CANCEL、MotionEvent.ACTION_SCROLL等多种事情,其间ACTION_MOVE事情是处理滑动事情的中心:
public class RecyclerView {
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_MOVE: {//手指移动
//依据mScrollPointerId获取接触点下标
final int index = e.findPointerIndex(mScrollPointerId);
//1.依据move事情发生的x,y来核算偏移量dx,dy
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) {
mReusableIntPair[0] = 0;//mReusableIntPair父view耗费的滑动间隔
mReusableIntPair[1] = 0;
//2.将嵌套的预滑动操作的一个过程分派给当时嵌套的翻滚父View,假如为true表明父View优先处理滑动事情。
//假如耗费,dx dy会别离减去父View耗费的那一部分间隔,mScrollOffset表明RecyclerView的翻滚方位
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];//减去父View耗费的那一部分间隔
dy -= mReusableIntPair[1];
//更新嵌套的偏移量
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
//开端滑动,防止父View被拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//3.终究完结的滚动效果
if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
//4.从缓存中预取一个ViewHolder
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
}
}
}
总结来说Move事情中只做了以下几件事:
- 依据move事情发生的x、y核算偏移量dx,dy;(当手指由下往上滑时dy>0,当手指由上往下滑时dy<0)
-
dispatchNestedPreScroll()
:触发嵌套翻滚,让嵌套翻滚中的父控件优先消费翻滚间隔 - 判别滑动方向,调用
scrollByInternal()
终究完结翻滚作用; - 调用
mGapWorker.postFromTraversal()
从RecyclerView缓存中预取一个ViewHolder。
中心办法scrollByInternal()
中先调用了scrollStep()
以触发列表本身的翻滚,紧接着还调用了dispatchNestedScroll()
将本身消费后剩下的翻滚余量持续交给其父控件消费。
public class RecyclerView {
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0
consumePendingUpdateOperations();
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 触发列表翻滚(手指滑动间隔被传入)
scrollStep(x, y, mReusableIntPair);
// 记录列表翻滚耗费的像素值和剩下未耗费的像素值
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
...
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 将列表未耗费的翻滚间隔持续留给其父控件耗费
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
...
}
}
而scrollStep()
中触发翻滚的任务委托给了LayoutManager,调用了它的scrollVerticallyBy()
:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
//符号正在翻滚
mLayoutState.mRecycle = true;
ensureLayoutState();
//确认翻滚方向
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
//1.更新layoutState,会更新其展现的屏幕区域,偏移量等。比方说当往上滑动的时分,底部会有dy间隔的空白区域,这时分,需求调用fill来填充这个dy间隔的区域
updateLayoutState(layoutDirection, absDy, true, state);
//2.调用fill进行填充将滑进来的view布局进来,并收回滑出去的view
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
//3.给一切子View增加偏移量,依照核算滑动的间隔动间隔移动View的方位
mOrientationHelper.offsetChildren(-scrolled);//移动
mLayoutState.mLastScrollDelta = scrolled;//记录本次滑动的间隔
return scrolled;
}
也便是当翻滚时首要做了三个处理:
- 经过
updateLayoutState
办法更新layoutState内部的相关特点的。 - 调用
fill()
进行数据的填充 - 经过
offsetChildren()
给一切子View增加偏移量
首先来看看LayoutState这个类中的几个变量:
- mOffset:布局起始方位的偏移量
- mAvailable:在布局方向上的能够填充的像素值,也便是闲暇的区域
- mScrollingOffset:表明在不创立新视图的状况下能够进行翻滚的间隔,运用这个变量来判别是否需求创立新的子View。比方某个View上半部分显现了一半,那么往上滑动一半间隔的话以内,是不需求创立新的子View的。
updateLayoutState内部完结:
//LinearLayoutaManager.java
private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
//获取当时显现的最底部的View
final View child = getChildClosestToEnd();
//设置当时显现的子View的底部的偏移量(包含了Decor的高度)
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
//底部锚点方位减去RecyclerView的高度的话,剩下的便是咱们翻滚scrollingOffset以内,不会制作新的View
//getEndAfterPadding=RecyclerView的高度-padding的高度
scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
}
....
前边提到fill()
办法会依据剩下空间来循环地调用layoutChunk()
向列表中填充表项,翻滚列表的场景中,剩下空间的值由翻滚间隔决议。scrollBy()
办法会依据翻滚间隔,在列表翻滚方向上填充额定的表项。填充完,再调用mOrientationHelper.offsetChildren()
将一切表项向翻滚的反方向平移,终究履行了View.offsetTopAndBottom()
。
所以总结来说RecyclerView 在处理 ACTION_MOVE 事情时核算出手指滑动间隔,以此作为翻滚偏移量,依据偏移量在翻滚方向上填充额定的表项,然后将一切表项向翻滚的反方向平移相同的位移值,以此完结翻滚。当咱们自界说LayoutManager时希望item能在屏幕上滑动时只需求做的便是:
第一步:设置对应方向上答应翻滚
//canScrollHorizontally()
@Override
public boolean canScrollVertically() {
return true;
}
第二步:重写对应的翻滚办法
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//鸿沟判定等操作
...
// 平移容器内的子View
offsetChildrenVertical(-dy);
return dy;
}
RecyclerView的滑动流程图如下:
收回复用
RecyclerView最强壮的功用之一是经过收回复用和绑定机制,无需创立与数据项相同多的视图来展现许多数据调集。一旦用户经过上下滑动开端与数据调集交互,当视图滑出屏幕时,它就不再需求。关于即将到来的新数据,不需求创立新的视图,而是经过复用旧视图,将其填充到新的方位,然后让其进入屏幕即可。这个强壮的功用是由前文提到过的Recycler类完结的。
RecyclerView 能够算作是四级缓存,这四个方针便是作为每一级缓存的结构的:
- mAttachedScrap
Scrap是RecyclerView中最轻量的缓存,它不参与滑动时的收回复用,仅仅作为从头布局时的一种暂时缓存。它的意图是,缓存当界面从头布局的前后都出现在屏幕上的ViewHolder,以此省去不必要的从头加载与绑定作业。Scrap实践上包含了两个ViewHolder类型的ArrayList。mAttachedScrap担任保存将会原封不动的ViewHolder,而mChangedScrap担任保存方位会发生移动的ViewHolder。
//包含mAttachedScrap 和 mChangedScrap
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
...
}
若绿色区域是可见区域,随着上划,itemB被移出屏幕,放进mAttachedScrap时,会被额定符号一个”REMOVED”符号表明这个ViewHolder现已被删除了,需求在之后被移除。C和D在删除B后会向上移动方位会被存储到mChangedScrap中,此刻itemE并没有出现在屏幕中,它不归于Scrap统辖的范围。
简略来说便是当RecyclerView移除一个ViewHolder时,它会被放进mAttachedScrap中,并符号为REMOVED,Scrap缓存了这些ViewHolder信息,以便在部分改写时快速运用。经过调用notifyItemRemoved()
、notifyItemChanged()
等办法来告诉RecyclerView哪些方位发生了改变。RecyclerView会依据这些信息,在处理改变时,运用Scrap来缓存其它内容没有发生改变的ViewHolder,以完结部分改写。
- mCachedViews
mCachedView是RecyclerView中的一个缓存池,它的作用是在列表项不可见时对ViewHolder进行缓存,以便在某些状况下能够快速地复用这些ViewHolder,防止频频创立新的ViewHolder方针和从头绑定数据的开支。mCachedView能够缓存多个ViewHolder方针,默许状况下最大约束是2个。当RecyclerView将ViewHolder从屏幕中移除时,它会将ViewHolder增加到mCachedView中,而且不会对ViewHolder进行任何更改,以便在需求时直接取出并快速运用。
public final class Recycler {
...
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
int mViewCacheMax = DEFAULT_CACHE_SIZE;
...
}
向上滑动itemB划出屏幕时被缓存到CachedViews 中,当向下滑itemB从头回到屏幕时,进行精准匹配,这个itemView还是 之前的itemView,那么会从CacheView中获取ViewHolder进行复用,由于DEFAULT_CACHE_SIZE默许状况下为2,所以假如咱们总是沿着同一方向滑动,mCachedView就不会供给很大的协助,而由于存储个数约束,那些被替换的ViewHolder就会丢进RecycledViewPool中
- mViewCacheExtension
是额定供给了一层缓存池给开发者。开发者视状况而定是否运用ViewCacheExtension增加一层缓存池,一般不用到。
- mRecyclerPool
能够说是Recycler中的一个终极收回站,RV中一切缓存都是以 LRU(Least Recently Used)办理 itemView 的,假如缓存池已满,在Srap和CacheView中不接收的就会丢进mRecyclerPool进行收回
public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;
private static final int DEFAULT_MAX_SCRAP = 5;
与前两者不同,RecycledViewPool在进行收回的时分,方针仅仅收回一个该viewType的ViewHolder方针,并没有保存下原来ViewHolder的内容,在复用时,将会调用bindViewHolder()从头绑定,然后变成了一个新的列表项展现出来。RecycledViewPool有一个默许的最大数量约束,即为5。当Recycler需求收回ViewHolder时,会尽可能将它们放入RecycledViewPool中,假如没有超越最大数量约束,那么这些ViewHolder能够被其他RecyclerView复用。值得注意的是,RecycledViewPool只依照ViewType进行区别,因此能够在不同的RecyclerView中同享一个RecycledViewPool,只要它们运用相同的ViewType就能够完结复用。
简略总结便是:
- 当RecyclerView需求更新数据的时分,假如当时数据在可视范围之内,就会直接从Scrap中获取,它们是不参与翻滚的收回和复用的。
- 上下小幅度的滑动的时分,会先在CachedView中找是否有viewType、position一致的 ViewHolder,而且ViewHolder未被remove的view复用
- 当所需的View在CachedView中没有对应的position,再从 RecyclePool 中拿到一个合适的view,然后运用Adapter将必要的数据绑定到它上面(
bindViewHolder()
)。假如Recycle Pool中也不存在有效的视图,就在绑定数据前创立新的视图(createViewHolder()
),最后回来数据
有了以上根底以后,接着回到 fill ()
中来看看RecyclerView是怎么来收回视图,在前边fill()代码中能够看到收回view首要是经过recycleByLayoutState(recycler, layoutState)完结,
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
//从End端开端收回视图
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
//从Start端开端收回视图
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
//从头部收回View
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
//既mScrollingOffset:在不创立新视图的状况下能够进行翻滚的间隔
final int limit = dt;
//回来附加到父视图的当时子View的数量
final int childCount = getChildCount();
...
//遍历子View
for (int i = 0; i < childCount; i++) {
//获取到子View
View child = getChildAt(i);
//假如当时的View的底部方位>limit,那么也便是会有View需求制作,顶部的View也就需求收回了
if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return;
}
}
}
终究会调用recycleView()这个办法进行收回作业
//RecyclerView.java
public void recycleView(View view) {
recycleViewHolderInternal(holder);
}
void recycleViewHolderInternal(ViewHolder holder) {
//判别各种无法收回的状况
...
if (forceRecycle || holder.isRecyclable()) {
//契合收回条件
if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
//滑动的视图,先保存在mCachedViews中
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//mCachedViews只能缓存mViewCacheMax个,那么需求将最久的那个移到RecycledViewPool
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
//将本次收回的ViewHolder放到mCachedViews中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {//假如现已缓存了。那么此处不会履行了。
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
因为收回机制的进口有许多,在mAttachedScrap,mCachedViews中都涉及到,以上这部分代码归于在滑动时调用scrollVerticallyBy()
办法然后调用 fill() 办法进行数据填充时终究调用上述 recyclerView()
。能够看到收回时,都是把 ViewHolder 放在 mCachedViews 里面,假如 mCachedViews 满了依据LRU算法,那就移除最早的那个 ViewHolder 扔到 ViewPool 缓存里。而存放线程池 RecycledViewPool 会依照ViewType来缓存到不同的队列,每个类型的队列最多缓存5个。假如现已满了,则不再缓存
至于复用在前文中其完成已提起过fill()对填充视图时在layoutChunk()办法中不断的从recycler中取出视图,然后对视图进行layout。而关于复用的调用则是在 layoutChunk中的 layoutState.next(recycler)
来触发,终究关于ViewHolder的复用逻辑是由 tryGetViewHolderForPositionByDeadline
来处理的。
//RecyclerView.java
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
tryGetViewHolderForPositionByDeadline ()这个办法很长完好的展现了怎么在每个层级的缓存中,取出来 ViewHolde,可是其实逻辑很简略,这儿放上伪代码便于了解:
//RecyclerView.java
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
//1. 从 mChangedScrap 中测验取出缓存的 ViewHolder ,若不存在,holder赋空。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
//2.去 mAttachedScrap 中取,没有就顺次去mHiddenViews、mCachedViews中取缓存的ViewHolder
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
//查验获取的holder是否合法。不合法,就会将holder进行收回。假如合法,则符号fromScrapOrHiddenOrCache为true。表明holder是从这缓存中获取的。
if (!validateViewHolderForOffsetPosition(holder)) {
...
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
...
if (mAdapter.hasStableIds()) {
//依据id顺次测验从mAttachedScrap、mCachedViews中获取
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
...
}
//3. 测验从咱们自界说的mViewCacheExtension中去获取
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type);
...
}
//4. 从缓存池中依据 type 取 ViewHolder
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
return null;
}
//5. 假如仍然无法获取的话,调用Adatper的createViewHolder办法创立一个ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
//数据不需求绑定(一般从mChangedScrap,mAttachedScrap中得到的缓存Holder是不需求进行从头绑定的)
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
//holder没有绑定数据,或许需求更新或许holder无效,则需求从头进行数据的绑定
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//这儿会进行数据的绑定
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
概况来说tryGetViewHolderForPositionByDeadline办法中首要:
- 从缓存查找 ViewHolder
- 缓存没有,那么就创立一个 ViewHolder;
- 判别 ViewHolder需不需求更新数据,假如需求就会调用 tryBindViewHolderByDeadline (调用
bindViewHolder()
绑定数据;
总的来说RecyclerView 滑动场景下的收回复用涉及到的结构体两个:mCachedViews 和 RecyclerViewPool;mCachedViews 优先级高于 RecyclerViewPool复用时,也是先到 mCachedViews 里找 ViewHolder假如 mCachedViews 里没有,那么才去 ViewPool 里找。在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 相同,只要 type 相同,就能够从头绑定数据拿出来复用。
经过以上内容,将RecyclerView大致流程过了一遍,在最后看一下RecyclerView的结构:
最后咱们再回忆一下关于LayoutManager来说,比较重要的几个办法:
-
onLayoutChildren()
: 对RecyclerView进行布局的进口办法。 -
fill()
: 担任填充RecyclerView。 -
scrollVerticallyBy()
:依据手指的移动滑动一定间隔,并调用fill()填充。 -
canScrollVertically()
或canScrollHorizontally()
: 判别是否支撑纵向滑动或横向滑动。
RecyclerView作为Android中十分美丽的组件,底层涉及到的知识点十分复杂而广泛。由于篇幅原因本文中只介绍了RecyclerView自界说布局办理器的相关知识,包含基本概念、完结原理、重要办法等。可是ItemDecoration项装饰器、动画ItemAnimator、监听器、供给高效部分改写的能力的DiffUtil等都是RecyclerView底层完结中的中心,值得咱们进一步学习和把握。
hi, 我是快手交际的Seino
快手交际技能团队正在招贤纳士! 咱们是公司的中心事务线, 这儿云集了各路高手, 也充满了机会与挑战. 伴随着事务的高速开展, 团队也在快速扩张. 欢迎各位高手加入咱们, 一起创造世界级的产品~
热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品司理, 测试开发… 许多 HC 等你来呦~ 内部推荐请发简历至 >>>咱们的邮箱: social-tech-team@kuaishou.com <<<, 补白我的花名成功率更高哦~