Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> ListView 源碼分析

ListView 源碼分析

編輯:關於Android編程

前言

雖然 RecyclerView 出來很長時間了,ListView 似乎已經過時了,但 ListView 仍然有許多優秀的思想值得學習。講到 ListView,大家都會想到其復用機制,我這裡就不廢話說一大堆為什麼需要復用等這些廢話,直接進入正題。

源碼分析

首先,由於 ListView 是個極其復雜的 View,由於本人能力以及篇幅的原因,不可能面面俱到的把整個 ListView 進行分析,那麼這裡我就分析最普遍的情況,以下就是我使用 ListView 的代碼,我們將針對這段代碼來進行分析。

mLv = (ListView) findViewById(R.id.lv);
List datas = new ArrayList<>();
for (int i = 0; i < 30; i++) {
    datas.add("xixi:" + i);
}
mLv.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, datas));

復用機制

在展開源代碼之前,首先我們得對 ListView 的復用機制有一個了解,ListView 的復用邏輯主要由 ListView 的父類 AbsListView 的內部類 RecycleBin 來完成,這個內部類主要有如下屬性:

View[] mActiveViews = new View[0]:

Views that were on screen at the start of layout. This array is
populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
Views in mActiveViews represent a contiguous range of Views, with position of the first view store in
mFirstActivePosition.

這個是官方的解釋,我覺得很詳細了,我在簡單復述下,這個 mActiveViews ,主要在 layout 的時候使用,用於存儲所有子 View。mActiveViews 裡面的於元素只能夠使用一次,一旦取出一個元素,那麼對應的下標便被置為 null。

ArrayList[] mScrapViews;

這個就是復用機制的核心屬性了,所有移出屏幕的 view 將會被存儲在這個數組中。事實上,當 ViewType 只有一種的時候,廢棄掉的 View 會存儲在 private ArrayList mCurrentScrap 中。

再來看兩個主要的方法:

void fillActiveViews(int childCount, int firstActivePosition)

這個方法我目前只找到在 ListView#layoutChildren 方法中調用,主要是將當前的所有子 View 填充到 mActiveViews 數組中。

void addScrapView(View scrap, int position)

這個看方法名就知道了,很明顯,將 view 添加到廢棄數組 mCurrentScrap 中。(這裡假定 viewType 只有一種)

無論一個 View 如何復雜,說的多麼玄乎,他終究都得經過 onMeasure 以及 onLayout 方法,那麼我們就來從他的 onMeasure 方法入手。強調一下,這裡我只研究復用機制,以下源代碼我會剔除跟復用機制無關的代碼,例如 dataChange、selectView 等部分代碼。

## onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Sets up mListPadding
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int childWidth = 0;
    int childHeight = 0;
    int childState = 0;

    //這裡我們已經設置了 adapter,所以 mItemCount 的數量是不為0的。
    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
    // 一般情況下不會進入以下判斷條件,但當父布局為 ScrollView 時,以下條件是成立的。
    if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
            || heightMode == MeasureSpec.UNSPECIFIED)) {
        final View child = obtainView(0, mIsScrap);

        // Lay out child directly against the parent measure spec so that
        // we can obtain exected minimum width and height.
        measureScrapChild(child, 0, widthMeasureSpec, heightSize);

        childWidth = child.getMeasuredWidth();
        childHeight = child.getMeasuredHeight();
        childState = combineMeasuredStates(childState, child.getMeasuredState());

        if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                ((LayoutParams) child.getLayoutParams()).viewType)) {
            mRecycler.addScrapView(child, 0);
        }
    }

    if (widthMode == MeasureSpec.UNSPECIFIED) {
        widthSize = mListPadding.left + mListPadding.right + childWidth +
                getVerticalScrollbarWidth();
    } else {
        widthSize |= (childState & MEASURED_STATE_MASK);
    }

    // 當測量模式為 UNSPECIFIED 時,ListView 的高度取第一個子 View 的高度。
    if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }

    //當測量模式為 wrap_content 時,會根據 itemCount 的數量來 inflate 子 view,最大高度不能超過 heightSize
    if (heightMode == MeasureSpec.AT_MOST) {
        //該方法接收四個參數,第 2、3 個參數是一個范圍,表示使用 Adapter 哪個范圍的數據。
        //第四個參數為 ListView 的可用高度。
        heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
    }

    setMeasuredDimension(widthSize, heightSize);

    mWidthMeasureSpec = widthMeasureSpec;
}
基於以上測量規則,可以解釋兩個現象。
當 ScrollView 嵌套 ListView 時,ListView 只顯示第一項。
由於當 ListView 嵌套在 ScrollView 中時,高度的測量模式會被強制改為 UNSPECIFIED,則根據如下代碼,ListView 的高度只能是第一項子 View 的高度。

if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}

為什麼使用如下代碼可以解決 ListView 嵌套在 ScrollView 中只顯示一項的問題。
重寫 ListView,在 onMeasure 方法中實現代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}

這段是網上可以解決 ScrollView 嵌套 ListView 只顯示第一項問題的代碼,那麼為什麼可以解決呢?參考 ListView 如下代碼實現。

if (heightMode == MeasureSpec.AT_MOST) {
    //該方法接收四個參數,第 2、3 個參數是一個范圍,表示使用 Adapter 哪個范圍的數據。
    //第四個參數為 ListView 的可用高度。
    heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, Integer.MAX_VALUE >> 2, -1);
}

我們把 Integer.MAX_VALUE >> 2 代入到代碼中,大家都知道 MeasureSpec 為一個32位 int 值,高2位表示測量模式,後 30 位表示大小,那麼 Integer.MAX_VALUE >> 2 即表示一個 View 最大可以多大。放到這個方法中則表示,adapter 中有多少 item,就加載多少 item。那麼這樣復用機制就一點卵用都沒有了。

onMeasure 裡面的一些方法我之所以不展開細說,是因為裡面涉及到一部分的復用邏輯,等後面說清楚了後,在自己看,也不遲。同時,ListView 的復用邏輯,大部分還是在 onLayout 以及 onTouchEvent 中,onMeasure 並不是主要部分。

onLayout

在 ListView 中並沒有找到 onLayout 這個方法,他存在於 ListView 的父類,AbsListView 中,在 onLayout 中,會調用 ListView 的 layoutChildren 方法,那麼我們直接看 ListView#layoutChildren 方法就好。

layoutChidlren

由於 layoutChildren 方法偏長,這裡我只保留跟復用邏輯有關的代碼,且假定 itemCount 不為0.

@Override
protected void layoutChildren() {

    try {
        super.layoutChildren();
        invalidate();
        final int childrenTop = mListPadding.top;
        final int childrenBottom = mBottom - mTop - mListPadding.bottom;
        final int childCount = getChildCount();
        int index = 0;
        int delta = 0;

        // 需要記錄上一次 layout 時的第一個子 view
        //主要用到其 top 屬性來設置本次 layout 第一個子 view 的開始位置
        View oldFirst = null;


        // Remember the previous first child
        oldFirst = getChildAt(0);

        //這個我們只分析其為 false 的情況。
        boolean dataChanged = mDataChanged;


        // 注意這個 position 不是 ListView 中 child 的下標,而是在 adapter 中的下標 
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        // 這個會將當前所有子 View 存儲到 recycleBin 的 mActiveViews 數組中。
        recycleBin.fillActiveViews(childCount, firstPosition);

        // 這個方法很重要,把所有 View 從 ListView 中 detach (移除)。
        //因為後面涉及到對 ListView 的重新填充,如果沒調用這個方法,那麼就可能會有兩份數據了。
        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();

        // 這裡就是填充 ListView 的方法了。
        fillSpecific(mFirstPosition,
                oldFirst == null ? childrenTop : oldFirst.getTop());

        mLayoutMode = LAYOUT_NORMAL;
        mDataChanged = false;

        // 這個我沒仔細研究,不過粗略看下,應該是滾動到指定位置,跟復用邏輯無關
        if (mPositionScrollAfterLayout != null) {
            post(mPositionScrollAfterLayout);
            mPositionScrollAfterLayout = null;
        }

        //填充結束後,將 mActiveViews 中的元素移除到 mScrapViews 中
        recycleBin.scrapActiveViews();

        //更新滾動條
        updateScrollIndicators();

        invokeOnItemScrollListener();
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

上面的代碼是不是看起來覺得 so easy? 事實上你自己去看 layoutChildren 方法,一開始絕對暈的想吐,這個是被我刪除了很多無關代碼後留下的。為了避免引起混淆,將我剔除的部分說明下。以上代碼基於以下條件:

ViewTypeCount 只有一種。 itemCount > 0 忽略選擇的的 item (setSelection)部分的邏輯

focus 以及一些我不懂的東西。

上面的代碼一開始會將所有子 View 填充到 mActiveViews 數組中,然後進行填充 ListView。在填充 ListView 結束後,會將 mActiveViews剩余的元素移除到 mScrapViews 數組中以供復用。recycleBin.scrapActiveViews(); 注意這裡強調剩余。雖然說是剩余,但是一般到這步的時候,mActiveViews 裡的元素一般都被消費掉了,所以這一步好像也沒啥卵用,有人知道 mActiveViews 裡有可用元素的情況的話,請留言,謝謝。

那麼 layoutChildren 方法就看完了,似乎沒看到是如何進行填充 ListView 的,那麼答案顯而易見,肯定是在 fillSpecific() 方法中,那麼我們跟進去。

fillSpecific()

/**
   * 先根據 top 值填充指定位置 position 的 view,在填充兩端的數據(往上和往下)
   *
   * @param position 先填充這個位置的 view,然後再填充這個 view 往上以及往下的數據
   * @param top Pixel offset from the top of this view to the top of the
   *        reference view.
   *
   * @return The selected view, or null if the selected view is outside the
   *         visible area.
   */
  private View fillSpecific(int position, int top) {
      boolean tempIsSelected = position == mSelectedPosition;
        //先根據 top 值填充第一個 view,再根據這個 view 填充兩端的數據
        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
        // Possibly changed again in fillUp if we add rows above this one.
        mFirstPosition = position;

        View above;
        View below;

        final int dividerHeight = mDividerHeight;
        // 往上填充
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        // This will correct for the top of the first view not touching the top of the list
        adjustViewsUpOrDown();
        // 往下填充
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);

        if (tempIsSelected) {
            return temp;
        } else if (above != null) {
            return above;
        } else {
            return below;
        }
  }

這個方法依會根據 top 值,往 ListView 中添加 item。但其實此時 position 傳進來的是 firstPosition,top 也是第一個 view 的 top,所以並不會涉及到往上的填充,只會涉及到往下的填充。為了加深對這個方法的理解,我畫了張圖。這張圖主要畫了這個方法的邏輯,先根據 top 填充紅色的 item,再填充紅色 item 兩端的數據。enter description here

那麼由於此時往上填充(fillUp) 雖然有調用,但卻沒進行填充,那麼我們只需要分析往下填充(fillDown) 就好。關於上面的 makeAndAddView ,下面進行分析。

fillDown()

private View fillDown(int pos, int nextTop) {
     View selectedView = null;

     // 得到 ListView 的高度
     int end = (mBottom - mTop);
     if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
         end -= mListPadding.bottom;
     }
     // nextTop 為要填充的 view 的 top 值,如果 top 大於 ListView 的高度或者 pos 已經大於 itemCount 了,則跳出循環。
     // 每次循環都會累加 nextTop 以及 pos 的值。
     // 這裡也可以看到 ListView 為什麼不會占用太大內存的原因,因為只會填充 ListView 高度大小的 View。
     while (nextTop < end && pos < mItemCount) {
         // is this the selected item?
         boolean selected = pos == mSelectedPosition;
         View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

         nextTop = child.getBottom() + mDividerHeight;
         if (selected) {
             selectedView = child;
         }
         pos++;
     }

     setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
     return selectedView;
 }

上面的代碼只展示了 ListView 如何填充 View,而具體如何獲得一個 View 以及復用 View 還沒有說明,那麼點進去 makeAndAddView() 方法。

makeAndAddView

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {
    View child;

    if (!mDataChanged) {
        // 嘗試從 mActiveViews 中獲取 view,如果當前在 layoutChildren,則此時有可能可以獲得 view。
        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;
        }
    }

    // mActiveViews 沒有 view,則調用 obtainView 方法獲得一個 view,注意這個方法肯定會獲得一個 view。
    child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

此方法作用如名字,制造一個 view(可能從緩存獲得,也可能重新 inflate 獲得)並且添加到 ListView。當 ListView 調用 layoutChildren 進行布局的時候,會調用recycleBin.fillActiveViews(childCount, firstPosition); 對 mActiveViews 進行填充,並且整個 ListView 中,也只有 layoutChildren 的時候會對 mActiveViews 進行填充。如果從mActiveViews 獲取到 view,則直接調用setupChild()對子 view 進行設置,比如位置等信息。如果獲取不到,則調用obtainView(position, mIsScrap) 方法獲取一個 view,這個方法肯定會獲得一個 view,只不過可能是從廢棄數組 mCurrenScraps 獲得或者重新 inflate。獲得 view 後,再調用setupChild()對 view 進行設置。注意 setupChild() 的最後一個參數,這個參數決定是否對 view 進行 measure 以及 layout。

那麼從上面看來,獲取緩存 view 的邏輯就在這個 obtainView()方法上了,趕緊跟進去看一下。

obtainView()

View obtainView(int position, boolean[] isScrap) {
    //標識是否使用緩存的 view,將作為 setupChild 的最後一個參數傳入
    isScrap[0] = false;

    //這裡就是 ListView 的核心了,獲取廢棄的 view。
    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else {
            isScrap[0] = true;
            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }


    setItemViewLayoutParams(child, position);

    return child;
}

上面的代碼展示了 ListView 的核心,這也是我們重寫 adapter 的 getView 方法時,為什麼 converView 有時為空,有時不為空的原因了,關鍵就是 mRecycler.getScrapView(position) 是否為空,也就是是否有廢棄的 view。這裡注意 isScrap[0],該方法標明是否有使用廢棄的 view,將決定後續是否對 view 進行 measure 以及 layout。

那麼到這裡,我們已經大概了解了 ListView 如何得到一個 view,以及它的復用機制,那麼摟一眼獲得 view 之後對 view 會進行怎樣的操作。回到makeAndAddView()方法,找到 setupChild()方法,跟進去。

setupChild()

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
                        boolean selected, boolean recycled) {

    //如果 child 是從緩存(mActiveViews 或者 mCurrentScrape)中獲得,則 recycled 為 true。
    //如果 recycled 為 false,則證明 view 是重新 inflate 出來的,則 needToMeasure 為 true。
    final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    p.viewType = mAdapter.getItemViewType(position);

    //如果是從緩存中獲得的 view,則重新將其與 parent(ListView) 進行關聯即可
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
            && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
    //如果是重新 inflate 出來的 view,則添加到 layout 中,其內部同樣會使其跟 ListView 進行關聯
        p.forceAdd = false;
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }


    // child 為重新 inflate 出來的,需要重新 measure
    if (needToMeasure) {
        final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = View.MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
                    View.MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }

    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;

    //child 為重新 inflate 出來的,需要重新 layout
    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
    // 如果是從緩存中獲取的 view,則代表此 view 已經添加到布局中了,將其偏移到新位置即可。
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }

}

setupChild 主要是根據 view 是否從緩存中獲取的來執行相應的邏輯。注意到attachViewToParent() 方法以及addViewInLayout()方法。個人能力有限,且網上資料也沒過多說明,僅從官方注釋來解釋下這兩個方法。attachViewToParent()內部其實沒有對 view 進行添加的代碼,而僅僅是使 view 的 parent 的屬性指向了父布局,也就是說,只是完成了一個關聯關系而已,從而使得可以通過 viewGroup.getChildAt(index) 來獲得這個 view。
addViewInLayout()方法,其內部會調用addViewInner()來添加 view 以及關聯 parent ,平時我們調用viewGroup.addView()的時候,其實到後面也會調用addViewInner()方法。
那麼我的個人猜測就是,attachViewToParent()僅僅只是完成了關聯,並沒有真正的把 view 添加到父布局中。而addViewInLayout(),是真真正正的把 view 添加到 layout 中。
那麼結合上面代碼,假設 recycled 為 true,那麼 child 是從緩存中獲得,也就是說,child 之前已經完成過 添加到 layout addViewInLayout()操作,那麼我們要做的僅僅就只是重新關聯一遍即可。
同時後面源碼我們可以看到,當 view 移出屏幕,添加到廢棄 view 數組 mCurrentScraps 中時,也並沒有調用 removeView()將 view 從布局中移除,而僅僅是調用detachViewsFromParent() 解除關聯關系而已。

我們可以(意)假(淫)設一下這樣設計的好處。由於 ListView 的所有子 View 只會是顯示在屏幕中的 item,所以當 item 移出屏幕時,肯定不能讓其繼續存在於 parent 中,但是 addView 以及 removeView 又是個相對較重的操作,那麼采用關聯這種操作,將會對性能的提升有一定的好處。

好了,ListView 的復用機制我們已經分析的七七八八了,結合以下,我們可以對接下來 ListView 最牛逼的部分進行分析了,那就是滑動時的復用機制。

說到滑動,那肯定存在於 onTouchEvent#move 中了,不過這種機制屬於 GridView 以及 ListView 共有的,所以寫在其父類 AbsListView 中。onTouchEvent 關於 move 的操作只是調用了onTouchMove(),那麼我們直接看這個方法。

onTouchMove()

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
     int pointerIndex = ev.findPointerIndex(mActivePointerId);
     if (pointerIndex == -1) {
         pointerIndex = 0;
         mActivePointerId = ev.getPointerId(pointerIndex);
     }

     final int y = (int) ev.getY(pointerIndex);

     scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);

 }

事實上,onTouchMove()方法是有一堆 switch case 判斷的,我們簡單起見,只走正常流程。跟進scrollIfNeeded()方法。

scrollIfNeeded()

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
    //總共滑動了多少
    int rawDeltaY = y - mMotionY;
    int scrollOffsetCorrection = 0;
    int scrollConsumedCorrection = 0;
    if (mLastY == Integer.MIN_VALUE) {
        rawDeltaY -= mMotionCorrection;
    }
    final int deltaY = rawDeltaY;
    //每個 event 產生時相比上個 event 時的偏移量。
    //rawDeltaY 指的是一個觸摸周期的偏移
    //incrementalDeltaY 是一個 event 周期的偏移
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    int lastYCorrection = 0;

    //這裡是有分支的,我刪除了,因為滑動的時候是會進入這個分支
    //並且這裡有復用的邏輯
    if (mTouchMode == TOUCH_MODE_SCROLL) {
        if (y != mLastY) {
            // We may be here after stopping a fling and continuing to scroll.
            // If so, we haven't disallowed intercepting touch events yet.
            // Make sure that we do so in case we're in a parent that can intercept.
            if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
                    Math.abs(rawDeltaY) > mTouchSlop) {
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }

            // 事件觸發在 ListView items 的下標
            final int motionIndex;
            //mMotionPosition 是觸摸事件在哪個 item 的位置上, 在 down 的時候完成賦值
            if (mMotionPosition >= 0) {
                motionIndex = mMotionPosition - mFirstPosition;
            } else {
                // If we don't have a motion position that we can reliably track,
                // pick something in the middle to make a best guess at things below.
                motionIndex = getChildCount() / 2;
            }

            int motionViewPrevTop = 0;
            View motionView = this.getChildAt(motionIndex);
            if (motionView != null) {
                motionViewPrevTop = motionView.getTop();
            }

            // No need to do all this work if we're not going to move anyway
            //是否到達了邊界,即不能上滑或下滑
            boolean atEdge = false;
            if (incrementalDeltaY != 0) {
                //檢測滑動,並回收 view 以及復用 view
                atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
            }

            // Check to see if we have bumped into the scroll limit
            motionView = this.getChildAt(motionIndex);
            if (motionView != null) {
                // Check if the top of the motion view is where it is
                // supposed to be
                final int motionViewRealTop = motionView.getTop();
                mLastY = y + lastYCorrection + scrollOffsetCorrection;
            }
        }
    }
}

scrollIfNeeded()方法的代碼非常長,我刪除了很多,例如到達邊界以及嵌套滾動這些邏輯,如果一起分析的話實在太亂了。
上面的代碼要知道變量是什麼意思,然後主要看 trackMotionScroll(deltaY, incrementalDeltaY) 方法就好。這個方法裡面就包含了回收 view 以及復用 view,在滑動回收的核心。那麼我們跟進去。

trackMotionScroll()

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;

     // "effective padding" In this case is the amount of padding that affects
     // how much space should not be filled by items. If we don't clip to padding
     // there is no effective padding.
     int effectivePaddingTop = 0;
     int effectivePaddingBottom = 0;
     if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
         effectivePaddingTop = listPadding.top;
         effectivePaddingBottom = listPadding.bottom;
     }

     //第一個 item 超出屏幕的空間
     final int spaceAbove = effectivePaddingTop - firstTop;
     final int end = getHeight() - effectivePaddingBottom;
     //最後一個 item 超出屏幕的空間
     final int spaceBelow = lastBottom - end;

     final int height = getHeight() - mPaddingBottom - mPaddingTop;

     final int firstPosition = mFirstPosition;

     final boolean cannotScrollDown = (firstPosition == 0 &&
             firstTop >= listPadding.top && incrementalDeltaY >= 0);
     final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
             lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

     //如果不能上滑或者下滑,返回 true 代表已到達邊界
     if (cannotScrollDown || cannotScrollUp) {
         return incrementalDeltaY != 0;
     }

     //是否上滑
     final boolean down = incrementalDeltaY < 0;


     int start = 0;
     int count = 0;

     if (down) {
         int top = -incrementalDeltaY;
         if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
             top += listPadding.top;
         }
         for (int i = 0; i < childCount; i++) {
             final View child = getChildAt(i);
             //這裡由於是上滑,所以當 child view 的 bottom 大於 top,證明還沒有 view 滑出頂部,不做處理。
             if (child.getBottom() >= top) {
                 break;
             } else {
                 //count 統計總共回收的 view 個數
                 count++;
                 int position = firstPosition + i;
                 //只回收非 headerView 以及 非footerView
                 if (position >= headerViewsCount && position < footerViewsStart) {
                     // The view will be rebound to new data, clear any
                     // system-managed transient state.
                     child.clearAccessibilityFocus();
                     //添加到廢棄 view
                     mRecycler.addScrapView(child, position);
                 }
             }
         }
     }
     //mMotionViewOriginalTop 是觸摸到的 item 的 top 值,在 down 的時候完成賦值,
     mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

     mBlockLayoutRequests = true;

     //有回收掉的 view,解除關聯關系,且由於是下滑,所以 start 肯定是0
     if (count > 0) {
         detachViewsFromParent(start, count);
         mRecycler.removeSkippedScrap();
     }

     // 滑動 view,跟 scrollTo 作用差不多
     offsetChildrenTopAndBottom(incrementalDeltaY);

     if (down) {
         mFirstPosition += count;
     }

     final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
     //當第一個 view 超出屏幕的部分大於滑動的偏移量或者最後一個 view 超出屏幕的大小小於滑動的偏移量,填充 ListView
     //這個判斷我也看不懂
     if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
         // 填充 ListView
         fillGap(down);
     }

     invokeOnItemScrollListener();

     return false;
 }

這段代碼我同樣進行刪除,只保留上滑部分的代碼。可以看到內部會遍歷子 view,當 view 的 bottom 小於頂部 top 時候,就會回收 view。同時會把回收掉的 view 跟父布局解除關聯關系detachViewsFromParent()。同時 ListView 的滑動效果也是在裡面實現的,offsetChildrenTopAndBottom()。然後最後,填充 ListView,調用fillGap(down),當上滑的時候,fillGap()內部會調用 fillDown()方法。不過他並不會從第一個 view 開始填充,而是從最後一個 view 開始像下填充,直到填滿 ListView 。我們簡單看一眼。

fillGap()

 @Override
 void fillGap(boolean down) {
     final int count = getChildCount();
     //上滑
     if (down) {
         int paddingTop = 0;
         if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
             paddingTop = getListPaddingTop();
         }
         //偏移量從最後一個 view 的底部開始,將作為 fillDown 開始填充的初始位置
         final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                 paddingTop;
         fillDown(mFirstPosition + count, startOffset);
         correctTooHigh(getChildCount());
     } else {
         int paddingBottom = 0;
         if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
             paddingBottom = getListPaddingBottom();
         }
         final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                 getHeight() - paddingBottom;
         fillUp(mFirstPosition - 1, startOffset);
         correctTooLow(getChildCount());
     }
 }

總結

再把 ListView 的流程過一遍。

layout 部分

首先,ListView 的子 View 是在 layoutChildren 方法中進行添加的。在 layoutChildren 的時候,會把當前已有的子 View 填充到 mActiveViews 中,然後把所有的子 View 跟 ListView 解除關聯關系,因為後面填充的時候會再次關聯。
第一次 layoutChildren 的時候,由於沒有子 view,所以這時候會回調 adapter 的 getView 方法,獲得相應數量的 子 view。
第二次 layoutChildren 的時候,此時已經有子 view 了,所以會填充到 mActiveViews,接著填充的時候會使用 mActiveViews 裡面的 view,而不會重新 inflate。
ListView 進行 layoutChildren 的時候,基本是不會涉及到添加到廢棄 view 數組 mCurrenScrap。

滑動部分

當 view 滑出屏幕的時候,不是調用 removeView,而是調用 detachViewsFromParent 解除關聯關系。下次使用的時候,直接與 ListView 進行關聯,然後進行相應位置的偏移即可。

剩下的可以結合這個流程自己再去過源碼。

ListView 是個相對龐大的 view,學習其源碼相對較困難,我自己也是看了好幾天才梳理出來這邏輯,下一篇將自己寫個 ListView 來加深印象,敬請期待。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved