ListView 布局復(fù)用原理學(xué)習(xí)筆記

ListView 的復(fù)用機(jī)制

ListView 的復(fù)用機(jī)制

實現(xiàn)復(fù)用機(jī)制最關(guān)鍵的類是AbsListView.RecycleBin 類
讓我們來看看它里面關(guān)鍵的方法

  • mActiveViews: View[] -> 緩存當(dāng)前展示在屏幕上的子View。在布局結(jié)束時,mActiveViews中的所有視圖都移動到mScrapViews。 mActiveViews中的視圖表示連續(xù)的視圖范圍,第一個視圖存儲的位置在mFirstActivePosition中

  • mCurrentScrapViews: ArrayList<View> -> 若當(dāng)前 mViewTypeCount==1,也即只有一種布局類型,則直接從該列表取廢棄緩存

  • mScrapViews: ArrayList<View> -> 緩存不同 mViewType 的廢棄View,也即是每種不同的 ViewType,對應(yīng)不同的 scrapViews 廢棄緩存列表

  • mViewTypeCount: int -> 加載的列表ViewType數(shù)量

  • mFirstActivePosition: int -> 緩存在mActiveViews中第一個View的position

  • setViewTypeCount() -> 為mViewTypeCount設(shè)置childView布局類型總數(shù),并為每種類型的childView單獨啟用一個RecycleBin緩存機(jī)制。

  • fillActiveViews(int childCount, int firstActivePosition) -> 此方法會將ListView中的指定元素存儲到mActiveViews數(shù)組當(dāng)中。

  • getActiveView(int position) -> 從mActiveViews中獲取指定的元素。取出view后,在mActiveViews里的該指定位置將被置空。所以這個mActiveViews只能使用一次,并不能復(fù)用。

  • getScrapView(int position) -> 從廢棄緩存中取出一個View。同理,如果childView的布局類型只有一項,就直接從mCurrentScrap中取。如果多種布局,則從mScrapViews找到相對應(yīng)的緩存ArrayList再取出view。

  • addScrapView(View scrap, int position) -> 將一個廢棄的view進(jìn)行緩存。如果childView的布局類型只有一項,就直接緩存到mCurrentScrap。如果多種布局,則從mScrapViews找到相對應(yīng)的廢棄緩存ArrayList并緩存view。

  • ListView復(fù)用流程圖


    ListView復(fù)用流程圖
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    final int childCount = getChildCount();
    if (childCount == 0) {
        return true;
    }
    final int firstTop = getChildAt(0).getTop();
    final int lastBottom = getChildAt(childCount - 1).getBottom();
    final Rect listPadding = mListPadding;
    final int spaceAbove = listPadding.top - firstTop;
    final int end = getHeight() - listPadding.bottom;
    final int spaceBelow = lastBottom - end;
    final int height = getHeight() - getPaddingBottom() - getPaddingTop();
    if (deltaY < 0) {
        deltaY = Math.max(-(height - 1), deltaY);
    } else {
        deltaY = Math.min(height - 1, deltaY);
    }
    if (incrementalDeltaY < 0) {
        incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    } else {
        incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    }
    final int firstPosition = mFirstPosition;
    if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
        // Don't need to move views down if the top of the first position
        // is already visible
        return true;
    }
    if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
        // Don't need to move views up if the bottom of the last position
        // is already visible
        return true;
    }
    final boolean down = incrementalDeltaY < 0;
    final boolean inTouchMode = isInTouchMode();
    if (inTouchMode) {
        hideSelector();
    }
    final int headerViewsCount = getHeaderViewsCount();
    final int footerViewsStart = mItemCount - getFooterViewsCount();
    int start = 0;
    int count = 0;
    if (down) {
        final int top = listPadding.top - incrementalDeltaY;
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child);
                }
            }
        }
    } else {
        final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child);
                }
            }
        }
    }
    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
    mBlockLayoutRequests = true;
    if (count > 0) {
        detachViewsFromParent(start, count);
    }
    offsetChildrenTopAndBottom(incrementalDeltaY);
    if (down) {
        mFirstPosition += count;
    }
    invalidate();
    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
    if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
        final int childIndex = mSelectedPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(getChildAt(childIndex));
        }
    }
    mBlockLayoutRequests = false;
    invokeOnItemScrollListener();
    awakenScrollBars();
    return false;
}
  1. 首先我們調(diào)用trackMotionScroll()
    這個方法接收兩個參數(shù),deltaY表示從手指按下時的位置到當(dāng)前手指位置的距離,incrementalDeltaY則表示據(jù)上次觸發(fā)event事件手指在Y方向上位置的改變量,那么其實我們就可以通過incrementalDeltaY的正負(fù)值情況來判斷用戶是向上還是向下滑動的了。如第34行代碼所示,如果incrementalDeltaY小于0,說明是向下滑動,否則就是向上滑動。
  2. 下滑過程通過if (child.getBottom() >= top)來判斷子view是不是移出屏幕外
    上滑過程通過if (child.getTop() <= bottom)來判斷
    如果所以會調(diào)用RecycleBin的addScrapView()方法將這個View加入到廢棄 緩存當(dāng)中,并
    將count計數(shù)器加1,計數(shù)器用于記錄有多少個子View被移出了屏幕
  3. 接下來在第76行,會根據(jù)當(dāng)前計數(shù)器的值來進(jìn)行一個detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念當(dāng)中,所有看不到的View就沒有必要為它進(jìn)行保存,因為屏幕外還有成百上千條數(shù)據(jù)等著顯示呢,一個好的回收策略才能保證ListView的高性能和高效率。緊接著在第78行調(diào)用了offsetChildrenTopAndBottom()方法,并將incrementalDeltaY作為參數(shù)傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的參數(shù)值進(jìn)行相應(yīng)的偏移,這樣就實現(xiàn)了隨著手指的拖動,ListView的內(nèi)容也會隨著滾動的效果。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}
  1. 接下來調(diào)用makeAndAddView進(jìn)行獲取新的列表子view,如果新的列表是在之前已經(jīng)緩存在mActiveViews中就直接取出來用,否則就會調(diào)用obtainView()
View obtainView(int position, boolean[] isScrap) {
    isScrap[0] = false;
    View scrapView;
    scrapView = mRecycler.getScrapView(position);
    View child;
    if (scrapView != null) {
        child = mAdapter.getView(position, scrapView, this);
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView);
            if (mCacheColorHint != 0) {
                child.setDrawingCacheBackgroundColor(mCacheColorHint);
            }
        } else {
            isScrap[0] = true;
            dispatchFinishTemporaryDetach(child);
        }
    } else {
        child = mAdapter.getView(position, null, this);
        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }
    }
    return child;
}
  1. obtainView先調(diào)用getScrapView可能返回null或者view通過判斷是不是null來用不同的參數(shù)調(diào)用getView,這個一般要求我們自己重寫
  2. 再回到makeAndAddView(),執(zhí)行setupchild重新將view attach到list上

總結(jié)

  • ActivityView其實就是在UI屏幕上可見的視圖(onScreenView),也是與用戶進(jìn)行交互的View,那么這些View會通過RecycleBin直接存儲到mActivityView數(shù)組當(dāng)中,以便為了直接復(fù)用,那么當(dāng)我們滑動ListView的時候,有些View被滑動到屏幕之外(offScreen) View,那么這些View就成為了ScrapView,也就是廢棄的View,已經(jīng)無法與用戶進(jìn)行交互了,這樣在UI視圖改變的時候就沒有繪制這些無用視圖的必要了。他將會被RecycleBin存儲到mScrapView數(shù)組當(dāng)中,但是沒有被銷毀掉,目的是為了二次復(fù)用,也就是間接復(fù)用。當(dāng)新的View需要顯示的時候,先判斷mActivityView中是否存在,如果存在那么我們就可以從mActivityView數(shù)組當(dāng)中直接取出復(fù)用,也就是直接復(fù)用,否則的話從mScrapView數(shù)組當(dāng)中進(jìn)行判斷,如果存在,那么二次復(fù)用當(dāng)前的視圖,如果不存在,那么就需要inflate View了。


    image.png

    -我們ListView一頁可以顯示10條數(shù)據(jù),那么我們在這個時候滑動一個Item的距離,也就是說把position = 0的Item移除屏幕,將position = 10 的Item移入屏幕,那么position = 1的Item是不是就直接能夠從mActivityView數(shù)組中拿到呢?這是可以的,我們在第一次加載Item數(shù)據(jù)的時候,已經(jīng)將position = 0~9的Item加入到了mActivityView數(shù)組當(dāng)中,那么在第二次加載的時候,由于position = 1 的Item還是ActivityView,那么這里就可以直接從數(shù)組中獲取,然后重新布局。這里也就表示的是Item的直接復(fù)用。
    如果我們在mActivityView數(shù)組中獲取不到position對應(yīng)的View,那么就嘗試從mScrapView廢棄View數(shù)組中嘗試去獲取,還拿剛才的例子來說當(dāng)position = 0的Item被移除屏幕的時候,首先會Detach讓View和視圖進(jìn)行分離,清空children,然后將廢棄View添加到mScrapView數(shù)組當(dāng)中,當(dāng)加載position = 10的Item時,mActivityView數(shù)組肯定是沒有的,也就無法獲取到,同樣mScrapView中也是不存在postion = 10與之對應(yīng)的廢棄View,說白了就是mScrapView數(shù)組只有mScrapView[0]這一項數(shù)據(jù),肯定是沒有mScrapView[10]這項數(shù)據(jù)的,那么我們就會這樣想,肯定是從Adapter中的getView方法獲取新的數(shù)據(jù)嘍,其實并不是這樣,雖然mScrapView中雖然沒有與之對應(yīng)的廢棄View,但是會返回最后一個緩存的View傳遞給convertview。那么也就是將mScrapView[0]對應(yīng)的View返回。總體的流程就是這樣。


    image.png
  • ListView始終只會在getView方法中inflate一頁的Item,也就是new View只會執(zhí)行一頁Item的次數(shù)。后續(xù)的Item通過直接復(fù)用和間接復(fù)用完成。
    注意一種情況:比如說還是一頁的Item,但是position = 0的Item沒有完全滑動出UI,position = 10的Item沒有完全進(jìn)入到UI的時候,那么position = 0的Item不會被detach掉,同樣不會被加入到廢棄View數(shù)組,這時mScrapView是空的,沒有任何數(shù)據(jù),那么position = 10的Item即無法從mActivityView中直接復(fù)用View,因為是第一次加載。mActivityView[10]是不存在的,同時mScrapView是空的,因此position = 10的Item只能重新生成View,也就是從getView方法中inflate
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 自 RecyclerView 出世后,ListView 慢慢退出歷史的舞臺。說實話,之后Android中的列表實現(xiàn)...
    Coralline_xss閱讀 2,225評論 0 3
  • RecyclerView是Android 5.0系統(tǒng)官方推出的一個代替listView的組件,那么究竟好在哪里呢?...
    niknowzcd閱讀 6,985評論 5 24
  • 由于 Android學(xué)習(xí)筆記之ListView復(fù)用機(jī)制 這篇文章總結(jié)性語句通俗易懂。我又是以學(xué)習(xí)總結(jié)為目的,所以用...
    CrazyCarrot閱讀 2,123評論 2 6
  • 我先解釋下,listView隨著滑動的復(fù)用邏輯! 首先:攔截先不用說;下面的文章會進(jìn)行說明,直接說listView...
    MrLgc閱讀 638評論 0 2
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學(xué)已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,788評論 16 22

友情鏈接更多精彩內(nèi)容