RecyclerView的LinearLayoutManager分析

夕陽下的奔跑發表於2019-08-21

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

RecyclerView的LinearLayoutManager分析

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

RecyclerView的LinearLayoutManager分析

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

RecyclerView的LinearLayoutManager分析

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滑到到完全可見。

相關文章