本文緊接着上一篇文章來分析RecyclerView的滾動,從中咱們能夠窺探到RecyclerView緩存的回收與再利用。java
分析源碼,不能太盲目,不然目標過大容易迷失本身,所以我仍是選取上篇文章的例子所示用的代碼,做爲分析的目標緩存
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
複製代碼
爲了分析RecyclerView滾動,如今假設手指從下往上滑動,後面將以此爲根基進行分析。app
- 與前面文章約定的習慣一致,我將使用RV表示RecyclerView,用LM表示LayoutManager,用LLM表示LinearLayoutManager。
- 閱讀本文須要提早了解RecyclerView源碼剖析: 基本顯示。
根據事件分發的原理可知,RV的滾動由onTouchEvent()
完成,精簡代碼以下佈局
public boolean onTouchEvent(MotionEvent e) {
// 經過LayoutManager判斷是否能夠水平或者垂直滾動
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
switch (action) {
case MotionEvent.ACTION_MOVE: {
int dx = mLastTouchX - x;
// 1. 獲取手指移動的距離
// dy大於0,表明手指向上滑動
int dy = mLastTouchY - y;
// ... 省略判斷是否能執行滾動的代碼
if (mScrollState == SCROLL_STATE_DRAGGING) {
// ...省略nested scroll的代碼
// 2. 執行滾動
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
// 成功完成一次滾動,就請求父View再也不截斷後續事件
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;
}
return true;
}
複製代碼
例子中使用的LLM支持的是垂直滾動,而且手指從下往上滑動,所以dx
值爲0,dy
值大於0(注意,不是小於0)。post
計算完滑動距離後,調用scrollByInternal()
來完成滾動。動畫
這裏省略了事件處理的代碼,你們能夠自行分析。ui
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int consumedX = 0;
int consumedY = 0;
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 執行滾動,並把滾動消耗的距離放到第二個參數中。
scrollStep(x, y, mReusableIntPair);
// 計算滾動消耗的距離
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
// ...
}
// 若是有ItemDecoration當即刷新
if (!mItemDecorations.isEmpty()) {
invalidate();
}
// ...省略nested scroll和over scroll的代碼
// 通知監聽RV滑動的監聽者
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
// 若是沒有ItemDecoration,也須要刷新
if (!awakenScrollBars()) {
invalidate();
}
// 只要有滑動,就返回true
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
複製代碼
這裏的邏輯很清楚,執行滾動,通知監聽器,刷新界面。所以咱們把目光集中在scrollStep()
便可this
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
// ...
int consumedX = 0;
int consumedY = 0;
// 滾動交給LayoutManager執行
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
// ...
// 保存滾動的距離到第二個參數中
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
複製代碼
原來RV把滾動的邏輯交給了LM,例子中使用的是LLM,並且支持的是垂直滾動,所以咱們來分析LLM的scrollVerticallyBy()
方法。spa
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
複製代碼
LLM是經過scrollBy()
實現滾動的,因爲代碼邏輯跨度比較大,所以我將分佈講解。code
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
// 表示子View可被回收
mLayoutState.mRecycle = true;
// 手指從下往上滑動,delta大於0,取值LayoutState.LAYOUT_END
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
// 1. 更新mLayoutState信息
updateLayoutState(layoutDirection, absDelta, true, state);
// ...
return scrolled;
}
複製代碼
根據前面文章可知,updateLayoutState()
爲子View佈局更新mLayouState
信息
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
// 大部分狀況都爲false
mLayoutState.mInfinite = resolveIsInfinite();
// 保存佈局的方向
mLayoutState.mLayoutDirection = layoutDirection;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 計算額外的佈局空間,若是不是smooth scroll,通常爲0
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0]);
int extraForEnd = Math.max(0, mReusableIntPair[1]);
// 根據例子分析,值爲true
boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
// 表明佈局可用的額外空間
mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
// 表明不須要回收的空間
mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
int scrollingOffset;
if (layoutToEnd) {
// 增長RV底部的padding
mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
// 獲取最底部的一個子View
final View child = getChildClosestToEnd();
// Adapter數據遍歷方向,這裏取ITEM_DIRECTION_TAIL,表示從前日後遍歷
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
// 計算獲取的下一個View,在Adapter中的position
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
// 計算佈局的起始座標
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// 計算在不一樣添加子View的狀況下,RV能夠滾動的距離
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
}
// requiredSpace值爲dy
// mLayoutState.mAvailable表明佈局的可用空間
mLayoutState.mAvailable = requiredSpace;
// 此時分析的狀況canUseExistingSpace爲true
if (canUseExistingSpace) {
// 我的認爲,這段代碼放在這裏是無心義的
mLayoutState.mAvailable -= scrollingOffset;
}
mLayoutState.mScrollingOffset = scrollingOffset;
}
複製代碼
這裏有不少變量是在前面文章中介紹過的,可是重點要關注scrollingOffset
變量,用一副圖解釋下
由圖可知,滑動距離還沒達到scrollingOffset
時,RV是不須要填充子View的。
更新完信息後,接下來就要決定這次的滑動是否須要回收不可見子View,以及是否須要填充新View。
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
// 1. 更新mLayoutState信息
updateLayoutState(layoutDirection, absDelta, true, state);
// 2. 根據狀況回收,建立子View,
int added = fill(recycler, mLayoutState, state, false);
// ...
return scrolled;
}
複製代碼
fill()
方法的命名很差,它不只僅完成填充子View的任務,還完成了子View的回收任務
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// 1. 在添加子View前,回收那些預計不可見子View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// 我的以爲,將剛纔更新mLayout時,計算layoutState.mAvailable的代碼移動到這裏比較恰當
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
// 可用空間仍是要包括計算出來的額外空間
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 用來保存佈局的結果
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 2. 若是有可用空間,而且還有子View能夠填充,那麼就填充子View,並計算是否會輸不可見子View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 2.1 添加子View
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 這裏處理的是全部子View添加完畢的狀況
if (layoutChunkResult.mFinished) {
break;
}
// 計算下次佈局的座標偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
// 添加子View後,再次計算可用空間
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
// 2.2 添加子View後,在執行滾動前,回收那些預計不可見的子View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
// 表示添加子View使用了多少空間,若是沒有添加子View,值就是0
return start - layoutState.mAvailable;
}
複製代碼
這段代碼把View的回收以及建立,混合在一塊兒。好在前面的文章已經分析了填充子View的過程,那麼接下來我挑一種情形來分析子View的回收過程。這個情形以下圖
如圖所示,dy
表明手指向上滑動的距離差,很明顯dy
是小於scrollingOffset
(不添加子View能夠滾動的距離),可是卻大於第一個子View的底部距離。
在這種狀況下,咱們徹底能夠預計,即將到來的滾動,確定會讓第一個子View不可見,所以咱們能夠提早回收這個子View。
我把須要的代碼結合在一塊兒來分析提早回收子View的過程,代碼以下
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
// requiredSpace的值就是dy
mLayoutState.mAvailable = requiredSpace;
// canUseExistingSpace爲true
if (canUseExistingSpace) {
// dy小於scrollingOffset時,mLayoutState.mAvailable爲負值
mLayoutState.mAvailable -= scrollingOffset;
}
}
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
// 在添加子View前,回收那些預計不可見子View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// 處理負值的狀況
if (layoutState.mAvailable < 0) {
// layoutState.mScrollingOffset從新計算後值爲dy
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
// ...
}
複製代碼
根據前面圖片展現的狀況,dy
是小於mScrollingOffset
的,代碼中最終計算計算出的layoutState.mScrollingOffset
的值就爲dy
,後面的代碼將會比較頂部的子View在滾動dy
的狀況下,是否不可見,看下recycleByLayoutState()
如何實現的
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
// 此時值爲dy
int scrollingOffset = layoutState.mScrollingOffset;
// 不是smooth scroll,值爲0
int noRecycleSpace = layoutState.mNoRecycleSpace;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset, int noRecycleSpace) {
// 此時值爲dy
final int limit = scrollingOffset - noRecycleSpace;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
} else {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 遍歷獲取第一個底部座標大於dy的子View
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// 若是找到這個子View,就刪除前面的全部子View,由於它們都不可見
recycleChildren(recycler, 0, i);
return;
}
}
}
}
複製代碼
根據前面的狀況來分析這段代碼,能夠計算出limit
的值就是dy
,所以經過遍歷獲取的第一個底部座標大於limit
值的子View,就是前面圖片上的第二個子View,所以調用recycleChildren(recycler, 0, i)
,回收第二個子View前面的全部子View,也就是回收第一個子View,由於它立刻不可見。
recycleChildren()
經過removeAndRecycleViewAt()
方法逐個回收子View
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
// 獲取子View
final View view = getChildAt(index);
// 1. 從RV中移除子View
removeViewAt(index);
// 2. 使用Recycler回收子View
recycler.recycleView(view);
}
複製代碼
回收分爲兩步,首先從RV中移除子View,這是一個ViewGroup
移除子View的操做。而後利用RecyclerView.Recycler
進行回收子View。
如今咱們把目光聚焦到Recycler
是如何回收子View的!!!
此時此刻真是使人激動的時候,由於咱們能夠開始窺探Recycler的緩存。
public void recycleView(@NonNull View view) {
// 經過佈局參數獲取ViewHolder
ViewHolder holder = getChildViewHolderInt(view);
// ...
// 回收
recycleViewHolderInternal(holder);
}
複製代碼
首先獲取了View的ViewHolder
,而後調用recycleViewHolderInternal()
來回收ViewHolder
。
回收的是
ViewHolder
而不是View,有意思!
void recycleViewHolderInternal(ViewHolder holder) {
// ... 省略狀態判斷的代碼
// ViewHolder沒有設置FLAG_NOT_RECYCLABLE,而且View處於transient state時,這個變量值爲true
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
// Adapter#onFailedToRecycleView()作清理工做後,表明可強制刷新
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
// 1. 緩存最大空間大於0而且ViewHolder狀態正常,就把它添加到緩存中
// mViewCacheMax表明緩存空間大小,默認爲2
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
// 若是緩存空間已滿,就清理空間
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 移除第一個緩存的ViewHolder,並使用Recycler進行回收
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
// ... 省略預獲取信息的操做
// 緩存空間足夠,就緩存ViewHolder
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
// 2. 沒法使用緩存,就使用Recycler回收ViewHolder
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
}
// ...
}
複製代碼
咱們來分析下這裏使用的緩存回收策略
ArrayList mCachedViews
最大空間大於0,而且ViewHolder
狀態正常(沒設置過FLAG_INVALID
, FLAG_REMOVED
, FLAG_UPDATE
, FLAG_ADAPTER_POSITION_UNKNOWN
)。
Recycler
回收。ViewHolder
狀態不正常,那麼就用RecyclerView
進行回收。這裏用到兩個緩存,有什麼區別呢,後面揭曉。
如今來看下addViewHolderToRecycledViewPool()
如何回收ViewHolder
的
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
// ...省略Accessbility
// 通知監聽者ViewHolder被回收
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
// 被回收前,設置ViewHolder的mOwnerRecyclerView爲null
holder.mOwnerRecyclerView = null;
// 使用RecylerPool回收ViewHolder
getRecycledViewPool().putRecycledView(holder);
}
複製代碼
原來使用RecyclerPool
進行回收的
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
// 根據類型,獲取回收池中的集合
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// 若是集合已滿,就不進行這次回收(mMaxScrap默認爲5)
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
// 添加須要回收的ViewHolder
scrapHeap.add(scrap);
}
複製代碼
咱們都直到回收池是爲了節省內存而生,可是它也不能盲目的無限回收,RecyclerPool
的回收上限是5。若是達到上限,那麼提交給回收池的ViewHolder
就會被忽略掉。
若是沒有達到回收池的上限,首先根據須要被回收的ViewHolder
的類型獲取一個集合,而後把須要回收的ViewHolder
放到這個集合中。
如今咱們已經瞭解了Recycler
回收子View的方式,那麼如今咱們來看看如何再利用回收的ViewHolder。
根據前面文章的分析可知,獲取子View是在layoutChunk()
中發生的
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. 獲取子View
View view = layoutState.next(recycler);
// 2. 添加子View
// 3. 測量子View
// 4. 佈局子View
// ...
}
複製代碼
獲取子View是從Recycler
中獲取的
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
// ...
ViewHolder holder = null;
// 1. 從mAttachedScrap,hidden(正在消失的View), mCachedViews中獲取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
// 對於不是pre-layout過程,這裏主要就是檢查ViewHolder類型是否匹配
if (!validateViewHolderForOffsetPosition(holder)) {
} else {
romScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
// 2. 若是Adapter支持stable ids,就根據stable id從mAttachedScrap, mCachedViews中獲取
if (mAdapter.hasStableIds()) {
}
// 3. 從自定義的緩存mViewCacheExtension中獲取
if (holder == null && mViewCacheExtension != null) {
}
if (holder == null) { // fallback to pool
// 4. 從RecylerPool中獲取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
// 5. 若是都獲取不到,只能調用Adapter建立
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
}
// 6. 根據狀況決定是否從新綁定
else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 7. 更新佈局參數
// ....
}
複製代碼
第二步,涉及到Adapter
的stable id
功能,大部分狀況下用不到,因此暫時不考慮。
第三步使用的是自定義緩存,目前不分析,你們能夠本身去研究下自定義緩存如何使用。
第一步,從mAttachedScrap
,hidden view
(正在消失的View),mCachedViews
中獲取ViewHolder
。其中與咱們相關的就是緩存mCachedViews
,代碼以下
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 1. 從mAttachedScrap獲取
// 2. 從hidden views中獲取
// 3. 從mCachedViews中獲取
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// 獲取ViewHolder的條件
// 1. ViewHolder沒設置過FLAG_INVALID
// 2. ViewHolder的佈局位置要和添加的位置同樣
// 3. 沒有被過渡動畫所使用
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
// 本次分析中dryRun爲false,表明獲取ViewHolder以前,須要從緩存中移除
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
return null;
}
複製代碼
咱們發現從mCachedViews
獲取ViewHolder
仍是有條件的
第一個和第三個條件,對於此時的分析是知足的。第二個條件很顯然不必定知足,它是處理這樣的狀況,假如手指向上滑動致使第一個View被回收,此時手指立刻向下滑動,這個時候就須要讓剛被回收的第一個View再顯示出來,所以就能夠再利用剛回收的ViewHolder
。
從這裏咱們能夠明白,mCachedViews
是用來處理剛剛被回收的子View,而後又要讓這個子View再顯示的狀況。
從前面回收ViewHolder
的分析可知,回收還用到了RecyclerPool
,所以這裏咱們還要分析從RecyclerPool
獲取ViewHolder
的狀況,也就是第四步,它調用的是RecyclerPool#getRecyclerView()
方法
public ViewHolder getRecycledView(int viewType) {
// 根據類型獲取緩存的集合
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
// ViewHolder沒有被過渡動畫使用,那麼默認返回的就是第一個ViewHolder
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
複製代碼
這裏與前面分析RecyclerPool
的緩存原理一致,首先根據類型獲取緩存集合,而後返回一個沒有被過渡動畫使用的ViewHolder
。可是這個ViewHolder
中保存的數據是不可靠的,所以須要從新綁定一次,也就是要調用Adapter#bindViewHolder()
方法,從而會調用子類Adapter
的onBindViewHolder()
方法。
咱們來總結下mCachedViews
和RecyclerPool
兩個緩存的的特色
mCachedViews
適用於子View剛被回收,而後又立刻要顯示的狀況。從這個緩存中獲取的ViewHolder
不須要再綁定。RecyclerPool
是爲了節約內存。從這個緩存中獲取的ViewHolder
是須要從新綁定的。那麼若是緩存中獲取不到ViewHolder
呢?天然須要經過Adapter
建立了,這個過程在前面的文章已經分析過了。
如今,咱們已經明白了子View如何回收再利用,那麼如今咱們來進行最後一步,分析RV如何實現滾動
可能你已經忘記從哪裏分析了,不要緊,已經分析到了LLM的scrollBy()
中的第三步
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
// 1. 更新mLayoutState信息
updateLayoutState(layoutDirection, absDelta, true, state);
// 2. 根據狀況回收,建立子View,
int added = fill(recycler, mLayoutState, state, false);
// 3. 計算實際須要滾動的距離
final int consumed = mLayoutState.mScrollingOffset + added;
if (consumed < 0) {
return 0;
}
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
// 4. 執行RV的滾動
mOrientationHelper.offsetChildren(-scrolled);
return scrolled;
}
複製代碼
第三步,計算實際須要滾動的距離。這裏分多種狀況,請你們自行分析。
第四步,實現RV的滾動,這裏是經過基類LayoutManager#offsetChildrenVertical()
,再由RecyclerView#offsetChildrenVertical()
實現的
public void offsetChildrenVertical(@Px int dy) {
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
}
}
複製代碼
原來是經過位移RV的每個子View完成來完成RV的總體滾動。
RecyclerView
每個過程的分析,都須要極大的耐心和毅力,爲寫這篇文章,我醞釀了每個細節,只但願能講解明白。可是限於篇幅,文章中不少小細節只給出了註釋,須要你們在源碼中本身去分析去體會,才能真正作到融會貫通。
下一篇文章,我將講述ItemDecoration
的原理,以及實現一個很是有意思的案例,效果以下
不過,這個準備過程可能有點長,喜歡我文章的朋友能夠點個贊,關注一波。