RecyclerView的LinearLayoutManager分析

1,194 阅读6分钟

LayoutState是LinearLayoutManager中管理布局的对象,记录了布局的方向,位置等信息

static class LayoutState {

    static final String TAG = "LLM#LayoutState";
	//布局方向,新增的子view放在前
    static final int LAYOUT_START = -1;
	//布局方向,新增的子view放在后
    static final int LAYOUT_END = 1;
    static final int INVALID_LAYOUT = Integer.MIN_VALUE;
	//取数据的方向,从头往后
    static final int ITEM_DIRECTION_HEAD = -1;
	//取数据的方向,从尾部往前
    static final int ITEM_DIRECTION_TAIL = 1;
    static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE;

    //布局过程中是否回收多余的子控件
    boolean mRecycle = true;
    //开始布局的位置,根据布局方向,距离参照物的左边界或者右边界
    int mOffset;
    //需要填充的距离,大于0才需要进行填充
    int mAvailable;
	//当前布局的子view在数据源中的位置
    int mCurrentPosition;
	//从源数据中取数据的方向,从头或者尾
    int mItemDirection;
	//布局的方向,往前或者往后
    int mLayoutDirection;
	//可滑动的距离,使得不需要创建新的子view
    int mScrollingOffset;
	//跟pre-layout有关
    int mExtraFillSpace = 0;
	//回收子view时,需要额外去掉的距离
    int mNoRecycleSpace = 0;
	//是否是pre-layout
    boolean mIsPreLayout = false;
	//scrollBy中实际滑动的距离
    int mLastScrollDelta;
	//来自Recycler中的mUnmodifiableAttachedScrap
    List<RecyclerView.ViewHolder> mScrapList = null;
	//只要源数据还有,就可以一直布局下去,不考虑布局上的空间限制
    boolean mInfinite;
}

手势滑动时的布局

没有创建新的子View,滑动已有的子View

即滑动的距离不足以使最后一个子View完全可见

以下图为例:

RV宽度200px,其中有3个子view,位置对应源数据分别是0,1,2

每个子view的宽度为40px,最后一个子view超出边界60px

RV及子view均未设置padding和margin

从右往左滑动40px

1.调用scrollBy()方法

其中delta=40

得到layoutDirection=LayoutState.LAYOUT_END

1.1.调用updateLayoutState()方法

​ 重新计算mLayoutState的属性:

​ mLayoutState.mInfinite=false

​ mLayoutState.mLayoutDirection=LayoutState.LAYOUT_END

1.1.1.调用calculateExtraLayoutSpace()

​ 计算extraForStart和extraForEnd,如果没有调用smoothScroll都为0

1.2.继续计算mLayoutState的各个属性值

​ mLayoutState.mExtraFillSpace=extraForEnd=0

​ mLayoutState.mNoRecycleSpace=0

​ 由于layoutToEnd为true,再计算

​ mLayoutState.mExtraFillSpace+=mRecyclerView.getPaddingRight(),此处为0

​ //getChildClosestToEnd()得到最右的child

​ mLayoutState.mItemDirection=LayoutState.ITEM_DIRECTION_TAIL

​ mLayoutState.mCurrentPosition=开始布局的子view的位置=2+1=3

​ mLayoutState.mOffset=最右的子view的右边界=260px

​ scrollingOffset=最右侧子view的右边界-RecyclerView的右边界=260-200=60px

​ mLayoutState.mAvailable=手势滑动的距离=40px

​ 因为canUseExistingSpace=true

​ 所以mLayoutState.mAvaliable -= scrollingOffset=40-60=-20px

​ mLayoutState.mScrollingOffset=scrollingOffset=60

2.consumed = mLayoutState.mScrollingOffset + fill()

2.1.调用fill()方法进行填充,并返回填充的长度(添加子view消耗的空间):

​ start=mLayoutState.mAvailable=-20px

​ mLayoutState.mScrollingOffset+=mAvailable=60-20=40px

2.1.1.调用recycleByLayoutState()回收会移出RecyclerView的子view

​ 回收了子view中right大于mScrollingOffset-mNoRecycleSpace = 40

​ 第一个就满足,但是不会被回收,因为start=end

2.2.继续填充

​ 计算剩余空间remainingSpace=mLayoutState.mAvailable+mLayoutState.mExtraFillSpace=-20px

​ 不满足while条件,所以不会调用layoutChunk(),即没有增加子view,

​ 并返回的start-mAvailable是消耗的空间,此处为0px

3.计算需要水平滑动的距离

consumed=60+0=60px

absDelta=40px

scrolled=40px,取实际增加的距离和手势滑动的距离的较小值,考虑方向

所以调用mOrientationHelper.offsetChildren(-scrolled);滑动40px

总结:水平滑动时,会计算滑动的距离是否超过了同方向上子view完全展示的距离,如果不超过,说明不需要增加新的子view,只需要控制已有的子view平移即可。

滑动距离较大,会把第3个子view完全展示出来,且会出现第4个

以下图为例:

RV宽200px,item每个都是100px,

当前第3个item滑动到只有10px不可见,

从右往左滑动14px

1.调用scrollBy()方法

其中delta=14px

得到layoutDirection=LayoutState.LAYOUT_END

1.1.调用updateLayoutState()方法

​ 重新计算mLayoutState的属性:

​ mLayoutState.mInfinite=false

​ mLayoutState.mLayoutDirection=LayoutState.LAYOUT_END

1.1.1.调用calculateExtraLayoutSpace()

​ 计算extraForStart和extraForEnd,如果没有调用smoothScroll都为0

1.2.继续计算mLayoutState的各个属性值

​ mLayoutState.mExtraFillSpace=extraForEnd=0

​ mLayoutState.mNoRecycleSpace=0

​ 由于layoutToEnd为true,再计算

​ mLayoutState.mExtraFillSpace+=mRecyclerView.getPaddingRight(),此处为0

​ //getChildClosestToEnd()得到最右的child

​ mLayoutState.mItemDirection=LayoutState.ITEM_DIRECTION_TAIL

​ mLayoutState.mCurrentPosition=开始布局的子view的位置=2+1=3

​ mLayoutState.mOffset=最右的子view的右边界=210px

​ scrollingOffset=最右侧子view的右边界-RecyclerView的右边界=210-200=10px

​ mLayoutState.mAvailable=手势滑动的距离=14px

​ 因为canUseExistingSpace=true

​ 所以mLayoutState.mAvaliable -= scrollingOffset=14-10=4px

​ mLayoutState.mScrollingOffset=scrollingOffset=10px

2.consumed = mLayoutState.mScrollingOffset + fill()

2.1.调用fill()方法进行填充,并返回填充的长度(添加子view消耗的空间):

​ start=mLayoutState.mAvailable=4px

2.1.1.调用recycleByLayoutState()回收会移出RecyclerView的子view

​ 回收了子view中right大于mScrollingOffset-mNoRecycleSpace =10px

​ 当前的子view中,第二个就满足,所以回收了第一个子view

2.2.继续填充

​ 计算剩余空间remainingSpace=mLayoutState.mAvailable+mLayoutState.mExtraFillSpace=4px

2.2.1.调用layoutChunk(),在尾部增加一个子view,并把它放在最后

​ 计算新增子view的位置:

​ left=mLayoutState.mOffset=210px

​ right=mLayoutState.mOffset+子view的宽=210px+100px=310px

2.3计算剩余空间

​ mLayoutState.mAvailable -= 宽 = -96px

​ remainingSpace -= 宽 = -96px

​ mLayoutState.mScrollingOffset += 宽 = 10 + 100 = 110px

​ 由于mLayoutState.mAvaliable<0,所以mLayoutState.mScrollingOffset += mLayoutState.mAvailable = 110-96=14px

​ 由于剩余空间小于0,不继续填充,并返回的start-mLayoutState.mAvailable=4-(-96)=100px

3.计算需要水平滑动的距离

consumed=10+100=110px

absDelta=14px

scrolled=14px,取实际增加的距离和手势滑动的距离的较小值,考虑方向

所以调用mOrientationHelper.offsetChildren(-scrolled);滑动14px

总结:水平滑动时,会计算滑动的距离是否超过了同方向上子view完全展示的距离,如果超过,说明需要增加新的子view,需要计算增加了子view后的滑动距离。

滑动到边界,无法继续滑动

以下图为例

数据源只有3条,

RV宽200px,item每个都是100px,

当前第3个item滑动到只有8px不可见

从右往左滑动24px

1.调用scrollBy()方法

其中delta=24px

得到layoutDirection=LayoutState.LAYOUT_END

1.1.调用updateLayoutState()方法

​ 重新计算mLayoutState的属性:

​ mLayoutState.mInfinite=false

​ mLayoutState.mLayoutDirection=LayoutState.LAYOUT_END

1.1.1.调用calculateExtraLayoutSpace()

​ 计算extraForStart和extraForEnd,如果没有调用smoothScroll都为0

1.2.继续计算mLayoutState的各个属性值

​ mLayoutState.mExtraFillSpace=extraForEnd=0

​ mLayoutState.mNoRecycleSpace=0

​ 由于layoutToEnd为true,再计算

​ mLayoutState.mExtraFillSpace+=mRecyclerView.getPaddingRight(),此处为0

​ //getChildClosestToEnd()得到最右的child

​ mLayoutState.mItemDirection=LayoutState.ITEM_DIRECTION_TAIL

​ mLayoutState.mCurrentPosition=开始布局的子view的位置=2+1=3

​ mLayoutState.mOffset=最右的子view的右边界=208px

​ scrollingOffset=最右侧子view的右边界-RecyclerView的右边界=208-200=8px

​ mLayoutState.mAvailable=手势滑动的距离=24px

​ 因为canUseExistingSpace=true

​ 所以mLayoutState.mAvaliable -= scrollingOffset=24-8=16px

​ mLayoutState.mScrollingOffset=scrollingOffset=8px

2.consumed = mLayoutState.mScrollingOffset + fill()

2.1.调用fill()方法进行填充,并返回填充的长度(添加子view消耗的空间):

​ start=mLayoutState.mAvailable=16px

2.1.1.调用recycleByLayoutState()回收会移出RecyclerView的子view

​ 回收了子view中right大于mScrollingOffset-mNoRecycleSpace =8px

​ 当前的子view中,第二个就满足,所以回收了第一个子view

2.2.继续填充

​ 计算剩余空间remainingSpace=mLayoutState.mAvailable+mLayoutState.mExtraFillSpace=16px

​ 由于没有更多的数据,所以不需要调用layoutChunk()进行填充子view

​ 并返回的start-mLayoutState.mAvailable=0px,即没有新增子view进行填充

3.计算需要水平滑动的距离

consumed=8+0=8px

absDelta=24px

scrolled=8px,取实际增加的距离和手势滑动的距离的较小值,考虑方向

所以调用mOrientationHelper.offsetChildren(-scrolled);滑动8px

总结:水平滑动时,会计算滑动的距离是否超过了同方向上子view完全展示的距离,如果超过,说明需要增加新的子view,但是如果没有数据了,则只需要把不可见的那部分子view滑到到完全可见。