ListView的setAdapter實現 查看GrepCode網站ListView源碼發現setAdapter主要有以下幾個重要方法:
layoutChildren,fillFromTop,fillDown /fillUp makeAndAddView,obtainView,setupChild
先簡單看下layoutChildren源碼
@Override PRotected void layoutChildren() { ......... boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } ......... // Clear out old views detachAllViewsFromParent(); switch (mLayoutMode) { ......... case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; ......... } ......... }setAdapter之后dataChanged為true,執行handleDataChanged()方法。
@Override protected void handleDataChanged() { ......... // Nothing is selected. Give up and reset everything. mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; mSelectedPosition = INVALID_POSITION; mSelectedRowId = INVALID_ROW_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedRowId = INVALID_ROW_ID; mNeedSync = false; mSelectorPosition = INVALID_POSITION; checkSelectionChanged(); }注意這個方法注意是設置mLayoutMode,通常是LAYOUT_FORCE_TOP,即從頂部開始一個一個的往下添加childview。 ??布局中ListView默認沒有設置android:stackfrombottom屬性,因此回到layoutChildren()方法中,執行LAYOUT_FORCE_TOP條件語句,設置mFirstPosition=0后,將childrenTop(=0或padding top 后的值),進入fillFromTop方法:
/** * Fills the list from top to bottom, starting with mFirstPosition * * @param nextTop The location where the top of the first item should be * drawn * * @return The view that is currently selected */ private View fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } return fillDown(mFirstPosition, nextTop); }/** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * * @param nextTop The location where the top of the item associated with pos * should be drawn * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ //第一次進來pos = 0,nexttop 是 padding.top private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (mBottom - mTop);//可視區高度 if ((mGroupFlags & CLip_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom;//減去paddingbotton屬性值 } 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++; } return selectedView; }可視區域的高度(mBottom - mTop),如果ListView條目很少,可視區高度不超過屏幕高度,最大不超過屏幕高度。 ??while循環中,判斷累計添加到listview中child的高度,不超過可視區域(添加最后一個child時,有可能只顯示部分),且添加的child的下標不超過總的個數(否則系統會報 OutOfBounds 的異常)。在循環中,會去調用makeAndAddView,這個方法不會真正的去添加child,但會調用之后的setupChild來真正添加到listview中:
/** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; //兩種情況:1.數據源沒有發生改變 2.數據源發生改變 if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP, position, getChildCount()); } // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child;//數據源沒有發生改變,調用mRecycler.getActiveView(position); } } // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap);//數據源發生改變,調用obtainView() // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }該方法分兩種情況:
adapter中的數據發生了變化,初始setAdapter,或之后我們將adapter中的數據做了新增/刪除后,調用Adapter.notifyDataSetChanged;變化了,就會從RecycleBin中的mScrapView中,取之前滑出屏幕的view,即convertView來復用;
若沒有變化,則從RecycleBin的mActiveView中取當前顯示的view( 為啥會有這種情況?當listview穩定后,我們不滾動它,但有可能點擊或長按 ),這時就會走到這里。 ??obtainView就是從RecycleBin中,取移出去的View,傳給Adapter.getView方法(convertView):
/*** Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the scrap heap, false if otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { isScrap[0] = false; View scrapView; scrapView = mRecycler.getScrapView(position);//獲取scrapView View child; if (scrapView != null) {//如果緩存中有,將這個convertView傳給Adapter.getView方法 if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP, position, -1); } child = mAdapter.getView(position, scrapView, this); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, position, getChildCount()); } if (child != scrapView) {//判斷從getView方法中返回的view是否與scrapview一致 //如果一致,表明是復用的,反之,則程序又去創建了一個新的view(浪費了一塊內存),且將得到的scrapview重新加入到RecycleBin.mScrapView中; mRecycler.addScrapView(scrapView, position); if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, position, -1); } } else { isScrap[0] = true; child.dispatchFinishTemporaryDetach(); } } else {//如果緩存中沒有,則convertView為null,在Adapter中,需要自己去LayoutInflater一個view child = mAdapter.getView(position, null, this); if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW, position, getChildCount()); } } return child; }這個方法,實際上是在AbsListView類中的。RecycleBin類,它的作用就是一個View的緩存,將移出屏幕外的view回收,并給新移入到屏幕內的view來復用,這樣就能節省大量內存。
回到makeAndAddView方法,將child傳給setupChild,開始真正的加入到listView中去顯示。
/** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of this child * @param y The y position relative to which this view will be positioned * @param flowDown If true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. */private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled) { final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); 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 = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { if (child instanceof Checkable) { ((Checkable) child).setChecked(mCheckStates.get(position)); } else if (getContext().getapplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { child.setActivated(mCheckStates.get(position)); } } //如果需要測量,先測量子view if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) != position) { child.jumpDrawablesToCurrentState(); } }??attachViewToParent 和 addViewInLayout兩者大致差不多,都是將view添加到parent view的array中,區別在于,attachView是不用去requestLayout的,而addViewInLayout的最后一個參數指明(true不用requestLayout,false則需要requestLayout),這兩個方法都在ViewGroup中。 ??flowDown ? -1 : 0 , -1 和 0 的區別?我們看下attachViewToParent代碼吧:
/** * Attaches a view to this view group. Attaching a view assigns this group as the parent, * sets the layout parameters and puts the view in the list of children so it can be retrieved * by calling {@link #getChildAt(int)}. * * This method should be called only for view which were detached from their parent. * * @param child the child to attach * @param index the index at which the child should be attached * @param params the layout parameters of the child * * @see #removeDetachedView(View, boolean) * @see #detachAllViewsFromParent() * @see #detachViewFromParent(View) * @see #detachViewFromParent(int) */ protected void attachViewToParent(View child, int index, LayoutParams params) { child.mLayoutParams = params; if (index < 0) { index = mChildrenCount; } addInArray(child, index); child.mParent = this; child.mPrivateFlags = (child.mPrivateFlags & ~DIRTY_MASK & ~DRAWING_CACHE_VALID) | DRAWN | INVALIDATED; this.mPrivateFlags |= INVALIDATED; if (child.hasFocus()) { requestChildFocus(child, child.findFocus()); } }如果是-1,則將index = mChildrenCount,即從當前child view數組的尾部開始加入,如果是0, 則從當前child view的頭部開始加入。 ??默認情況下,添加到listview中的item,即child都會measure一次高度和寬度,然后,調用child.layout,通知新添加的child,layout一下它里面的children。 ??然后,然后就沒有了然后,整個流程走完,回到ListView.layoutChildren中,adjustViewsUpOrDown將所有child調整對齊,刷新一下RecycleBin的Active和Scrap緩存,調用updateScrollIndicators更新一下滾動條的值,若有注意OnScrollListener,也通知一下invokeOnItemScrollListener。
新聞熱點
疑難解答