ListView的兩次測量(原始碼分析)
#前言
ListView是Android開發者最常見的控制元件之一,但是真的很少有人會去思考他是如何實現的,包括筆者也是。
最近有學長正好問到這個問題,筆者當場懵逼。
於是痛定思痛,決定閱讀其原始碼,瞭解一下ListView的測量原理。一方面是提高自己閱讀原始碼的自學能力,另一方面是打算讓自己對View的測量的理解更進一步。
#進入正題
在此,不得不提一個概念:
任何一個View,在展示到介面上之前都會經歷至少兩次onMeasure()和兩次onLayout()的過程。
這在平時其實對我們沒有什麼影響,但是對於ListView這種複雜的控制元件來講就不一樣了。經過ListView原始碼中諸多的if-else語句的過濾,兩次執行的程式碼看上去是完全不一樣的。
所以我們原始碼的分析也分成了第一次測量與第二次測量。
為了方便讀者的理解,我們附上官網ListView的繼承結構:
##尋找入口
ListView是一個非常複雜的控制元件,僅僅我們經常用的功能就包括:複用回收、設定HeadView、設定FootView、設定Adapter、設定分割線、設定當前位置等等。
如果要完全把ListView進行分析,那需要花費大量的時間和文筆。我們首先要清晰自己分析需求,筆者此文也是僅針對ListView的測量進行分析。
我相信大家首先想到分析的方法就是setAdapter(),因為只有當設定adapter之後,ListView才會擁有子View並進行顯示,但是如上所說,ListView是一個非常複雜的控制元件,通過對setAdapter()分析後,很容易可以發現其中只是獲取到了該adapter,具體繪製內容並不在裡面,如下所示:
public void setAdapter(ListAdapter adapter) { if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } resetList(); mRecycler.clear(); if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; // AbsListView#setAdapter will update choice mode states. super.setAdapter(adapter); if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position); setNextSelectedPositionInt(position); if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { mAreAllItemsSelectable = true; checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); }
那麼我們只能尋找最常規的方法了。View的測量是依靠onMeasure()以及onLayout()方法。
我們使用ListView一般就是佔用整個螢幕,onMeasure()沒有特別需要分析的必要。
我們主要就講一下onLayout()。
#ListView第一次測量
首先我們可以發現在ListView中並不存在onLayout()這個方法。
那麼這個方法就一定是寫在ListView的父類AbsListView中了。
我們可以找到如下:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}
我們可以看到,onLayout()方法中並沒有做什麼複雜的邏輯操作,主要就是一個判斷,如果ListView的大小或者位置發生了變化,ListView所有的子佈局都強制進行重繪。layoutChildren()這個方法,從方法名上我們就可以猜出這個方法是用來進行子元素佈局的。
但是我們點開後如下所示:
/**
* Subclasses must override this method to layout their children.
*/
protected void layoutChildren() {
}
我們發現這是一個空方法。其實很容易理解,ListView和GridView都繼承自AbsListView,子部局的排版方式當然是繼承後寫到子類中了。
然後我們跳轉到ListView的layoutChildren方法:
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
// Remember stuff we will need down below
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
index = mNextSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
newSel = getChildAt(index);
}
break;
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
case LAYOUT_MOVE_SELECTION:
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
setSelectedPositionInt(mNextSelectedPosition);
AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
View accessibilityFocusLayoutRestoreView = null;
int accessibilityFocusPosition = INVALID_POSITION;
// Remember which child, if any, had accessibility focus. This must
// occur before recycling any views, since that will clear
// accessibility focus.
final ViewRootImpl viewRootImpl = getViewRootImpl();
if (viewRootImpl != null) {
final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
if (focusHost != null) {
final View focusChild = getAccessibilityFocusedChild(focusHost);
if (focusChild != null) {
if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)
|| focusChild.hasTransientState() || mAdapterHasStableIds) {
// The views won't be changing, so try to maintain
// focus on the current host and virtual view.
accessibilityFocusLayoutRestoreView = focusHost;
accessibilityFocusLayoutRestoreNode = viewRootImpl
.getAccessibilityFocusedVirtualView();
}
// If all else fails, maintain focus at the same
// position.
accessibilityFocusPosition = getPositionForView(focusChild);
}
}
}
View focusLayoutRestoreDirectChild = null;
View focusLayoutRestoreView = null;
// Take focus back to us temporarily to avoid the eventual call to
// clear focus when removing the focused child below from messing
// things up when ViewAncestor assigns focus back to someone else.
final View focusedChild = getFocusedChild();
if (focusedChild != null) {
// TODO: in some cases focusedChild.getParent() == null
// We can remember the focused view to restore after re-layout
// if the data hasn't changed, or if the focused position is a
// header or footer.
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)
|| focusedChild.hasTransientState() || mAdapterHasStableIds) {
focusLayoutRestoreDirectChild = focusedChild;
// Remember the specific view that had focus.
focusLayoutRestoreView = findFocus();
if (focusLayoutRestoreView != null) {
// Tell it we are going to mess with it.
focusLayoutRestoreView.onStartTemporaryDetach();
}
}
requestFocus();
}
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
if (sel != null) {
// The current selected item should get focus if items are
// focusable.
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
focusLayoutRestoreView != null &&
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
if (!focusWasTaken) {
// Selected item didn't take focus, but we still want to
// make sure something else outside of the selected view
// has focus.
final View focused = getFocusedChild();
if (focused != null) {
focused.clearFocus();
}
positionSelector(INVALID_POSITION, sel);
} else {
sel.setSelected(false);
mSelectorRect.setEmpty();
}
} else {
positionSelector(INVALID_POSITION, sel);
}
mSelectedTop = sel.getTop();
} else {
final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
|| mTouchMode == TOUCH_MODE_DONE_WAITING;
if (inTouchMode) {
// If the user's finger is down, select the motion position.
final View child = getChildAt(mMotionPosition - mFirstPosition);
if (child != null) {
positionSelector(mMotionPosition, child);
}
} else if (mSelectorPosition != INVALID_POSITION) {
// If we had previously positioned the selector somewhere,
// put it back there. It might not match up with the data,
// but it's transitioning out so it's not a big deal.
final View child = getChildAt(mSelectorPosition - mFirstPosition);
if (child != null) {
positionSelector(mSelectorPosition, child);
}
} else {
// Otherwise, clear selection.
mSelectedTop = 0;
mSelectorRect.setEmpty();
}
// Even if there is not selected position, we may need to
// restore focus (i.e. something focusable in touch mode).
if (hasFocus() && focusLayoutRestoreView != null) {
focusLayoutRestoreView.requestFocus();
}
}
// Attempt to restore accessibility focus, if necessary.
if (viewRootImpl != null) {
final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
if (newAccessibilityFocusedView == null) {
if (accessibilityFocusLayoutRestoreView != null
&& accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
final AccessibilityNodeProvider provider =
accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
accessibilityFocusLayoutRestoreNode.getSourceNodeId());
provider.performAction(virtualViewId,
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
} else {
accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
}
} else if (accessibilityFocusPosition != INVALID_POSITION) {
// Bound the position within the visible children.
final int position = MathUtils.constrain(
accessibilityFocusPosition - mFirstPosition, 0,
getChildCount() - 1);
final View restoreView = getChildAt(position);
if (restoreView != null) {
restoreView.requestAccessibilityFocus();
}
}
}
}
// Tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.onFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
程式碼有三百多行,說實話筆者剛開始看的時候是非常懵逼的。
但是我們可以根據函式名進行邏輯的篩選:
1、mAdapter為空等為特殊情況,我們不需要考慮,我們想得到的是正常情況下ListView的測量方法。
2、我們可以看到有很多引數都與“focus”相關,我們僅僅想知道ListView的測量方法,至於ListView其他的功能是怎麼實現的,我們暫時就放一邊了。
經過篩選後,我們可以得到以下程式碼:
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
首先我們可以看switch (mLayoutMode){}之前的邏輯,根據註釋可得,這些是ListView的複用邏輯,可以排除。
// Pull all children into the RecycleBin.
// These views will be reused if possible
//Clear out old views
那麼我們就需要看switch (mLayoutMode){}中的邏輯了,首先我們要知道會進入mLayoutMode的什麼模式。
我們可以發現ListView是不帶有mLayoutMode這個引數的。那麼,我們直接進入AbsListView進行檢視。
/**
* Controls how the next layout will happen
*/
int mLayoutMode = LAYOUT_NORMAL;
我們可以發現mLayoutMode 預設情況下是LAYOUT_NORMAL模式,在switch中不存在,那麼便會進入default中。
接下來我們檢視default的程式碼:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
首先我們先思考一下我們一般使用LIstView的情況,我們一般會下xml中如下定義:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</RelativeLayout>
我們可以看到,ListView剛開始是沒有子部局的。也是說childCount =0;
(只有在使用SetAdapter()之後,ListView才會有子部局,當然在xml中也不會體現出來)
那麼,我們接下來就很方便了。childCount =0,並且我們預設的佈局都是從上往下的,因此我們就會跳入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);
}
從這個方法的註釋中可以看出,它所負責的主要任務就是從mFirstPosition開始,自頂至底去填充ListView。但是這個方法本身並沒有什麼邏輯,因此我們可以確定邏輯在fillDown()這個函式中:
/**
* 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.
*/
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;
}
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;
}
這時候我們看到了一個獲取到child這個View的方法makeAndAddView(),於是再進方法內檢視:
/**
* 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;
if (!mDataChanged) {
// Try to use an existing 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;
}
這時候我們看註釋:
獲取到View並且把它加入到list的子群,View可以被重新整理。
那麼我們可以推得,這個把View加入到list中的函式一定是ListView的測量與佈局的函數了,即setupChild():
/**
* 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) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
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 = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
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));
}
}
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 = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
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();
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
到這裡,對於自定義過ViewGroup的onLayout()方法的讀者來講就會非常熟悉了。
由於此篇文章僅僅對於ListView測量以及佈局的研究,此處對於needToMeasure這個boolean條件的判斷就不討論了,我們直接看needToMeasure為true時會進行的測量,首先是child的measure:
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
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 = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
首先獲取到子View寬度的MeasureSpec。padding為ListView的左右padding之和,寬度即為child的寬度。
然後獲取到高度的MeasureSpec,高度即為child的高度。
接著是最關鍵的子View的layout,函式如下:
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());
}
我們可以看到childRight 就是等於childrenLeft 加上子View的寬度,而childBottom就是等於childTop 加上子View的高度。
可知其實layout的關鍵是在childrenLeft 和childTop 上。
childrenLeft 是由setupChild()的引數之一。
而childTop 得到的方式如下:
final int childTop = flowDown ? y : y - h;
這時候我們再看setupChild()這個函式:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled)
我們可以發現childrenLeft 和childTop 都是取決於傳遞過來的引數,然後我們回到makeAndAddView()這個方法中,makeAndAddView這個方法如下:
makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected)
setupChild()在makeAndAddView()中的呼叫如下:
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;
}
我們發現這些引數還是被傳遞過來的,所以我們還需要回去找:
這時候就是回到fillDown中了:
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++;
}
終於我們找到了引數傳遞的源頭!
於是我們再看layout函式:
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());
}
childrenLeft為ListView的PaddingLeft。
而決定childTop 的為flowDown 這個boolean和y這個int。我們可以看到,flowDown 在這裡的值為true,而y則是由nextTop這個值傳回來的,我們需要再回去找,接著就到了fillDown和fillFromTop這兩個函式,最終回到了ListView中的layoutChild:
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;
}
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;
}
View fillFromTop(int nextTop)
protected void layoutChildren() {
…………………………………………
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
…………………………………………
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
}
…………………………………………
}
然後我們可以在layoutChildren這個函式中發現,最初的nextTop這個值為ListView的PaddingTop。
而接下來是在fillDown這個函式中累加,如下:
nextTop = child.getBottom() + mDividerHeight;
nextTop 等於上一個子View的底部的位置加上分隔線的高度。
終於layout的所有引數我們都看懂了!
#第一次測量結果
我們再看layout的函式
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);
}
第一次佈局時,四個引數的值分別如下:
**childrenLeft:**為ListView的PaddingLeft
**childTop:**最初是為ListView的PaddingTop,之後為之前的一個View的底部的位置加上分隔線的高度。
**childRight:**為ListView的PaddingLeft加上View的寬度
**childBottom:**為ListView的PaddingTop加上View的高度
#第二次測量
第二次測量與第一次測量最大的不同就是第二次測量的時候,ListView已經擁有了子View,在各邏輯的判斷上回有所不同。
第二次測量與第一次其實查詢的步驟差不多,首先我們還是要進入關鍵函式layoutChild()檢視,由於之前已經分析過濾過,所以此處就 貼上關鍵程式碼:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
之前我們分析這邊的時候是預設子View的個數為0,所以進入的前面的語句,但是第二次測量的時候子View已經放入ListView中,因此childCount 不再等於0,於是我們會進入後面的語句。
我們還是預設是從上至下的預設排序,於是我們會進入fillSpecific()這個函式:
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @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;
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;
if (!mStackFromBottom) {
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);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
通過註釋和邏輯我們可以知道,fillSpecific()這個函式與fillDown()和fillUp()這兩個函式其實差不多。只是fillSpecific()會先載入指定的View,然後再往上往下載入其他的View。
接著我們就檢視一下傳過來的引數:
sel = fillSpecific(mSelectedPosition,oldSel == null ? childrenTop : oldSel.getTop());
fillSpecific(int position, int top)
當我們第一次載入的時候,由於我們還沒有選擇任何一項,所以mSelectedPosition的值為0。同樣的我們也沒有滑動過ListView,因此oldSel=null。於是我們可得,position=0,top=0。
而我們在fillSpecific()函式中進行的操作如下:
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
於是我們可以發現,其實我們還只是執行了一遍fillDown()函式而已,只是第一次測量是通過fillFromTop()這個函式進入的,而第二次是通過fillSpecific()這個函式進入的。
之後的邏輯就是與第一次測量類似了。
#總結(兩次測量比較)
##第一次測量
第一次測量的時候ListView中沒有子View。
查詢到layout關鍵程式碼步驟如下:
onLayout(AbsListView類)
layoutChildren(ListView類)
fillFromTop(ListView類)
fillDown(ListView類)
makeAndAddView(ListView類)
setupChild(ListView類)
##第二次測量
第二次測量的時候ListView中已經擁有了子View。
查詢到layout關鍵程式碼步驟如下:
onLayout(AbsListView類)
layoutChildren(ListView類)
fillSpecific(ListView類)
fillDown(ListView類)
makeAndAddView(ListView類)
setupChild(ListView類)
#拓展
當然我相信有的讀者的思維已經難以維持在這一點點的對ListView的理解了,本篇文章中只是闡述了ListView的兩次測量的問題,而還有殘留了較多的其他問題,比如:
- ListView內部是如何進行子View的複用的呢?(使用RecycleBin)
- 既然ListView兩次測量最終都會進入setupChild()這個函式,那麼這兩次測量到底有什麼不同呢?(第一次將資料放入RecycleBin,第二次直接從裡面獲取)
由於筆者闡述能力有限,這些問題就留給有興趣的讀者自己去進行原始碼的探索吧。
路漫漫其修遠兮,吾將上下而求索。
相關推薦
ListView的兩次測量(原始碼分析)
#前言 ListView是Android開發者最常見的控制元件之一,但是真的很少有人會去思考他是如何實現的,包括筆者也是。 最近有學長正好問到這個問題,筆者當場懵逼。 於是痛定思痛,決定閱讀其原始碼,瞭解一下ListView的測量原理。一方面是提高自己閱讀原始碼
專案裡出現兩個配置類繼承WebMvcConfigurationSupport時,為什麼只有一個會生效(原始碼分析)
為什麼我們的專案裡出現兩個配置類繼承WebMvcConfigurationSupport時,只有一個會生效。我在網上找了半天都是說結果的,沒有人分析原始碼到底是為啥,博主準備講解一下,希望可以幫到大家! 大家基本遇到過一種情況,就是我配置類中已經配置了,為什麼就是沒有生效呢?其中一種原因就是,自己寫的
深入分析集合併發修改異常(原始碼分析)java.util.ConcurrentModificationException
在我們迭代操作集合時,有時可能需要改變集合的元素,若操作不當,經常都會出現不正確的結果或者併發操作異常,如下: Exception in thread "main" java.util.ConcurrentModificatio
Activity的啟動過程(原始碼分析)
startActivity正常啟動分析 通過startActivity(intent)來啟動活動,跟進原始碼看一下 首先Activity類裡面過載了多個startActivity()方法,引數不同而已 @Override public void startAc
openCV中的findHomography函式分析以及RANSAC演算法的詳解(原始碼分析)
本文將openCV中的RANSAC程式碼全部挑選出來,進行分析和講解,以便大家更好的理解RANSAC演算法。程式碼我都試過,可以直接執行。 在計算機視覺和影象處理等很多領域,都需要用到RANSAC演算法。openCV中也有封裝好的RANSAC演算法,以便於人們使用。關於RA
SpringMVC工作原理(原始碼分析)
Spring原始碼分析 我以下呢 就模擬一個請求 從程式碼上 簡單說說 SpringMVC一個請求得流程 先來個圖 當一個請求(request)過來,進入DispatcherServlet中,裡面有個方法叫 doDispatch方法 裡面包含了核心流程 原始碼如下: 然
Android RecyclerView瀑布流中Item寬度異常的問題(原始碼分析)
問題描述 通過RecyclerView配合StaggeredGridLayoutManager可以很方便的實現瀑布流效果,一般情況下會把作為Item的子View寬度設定為MATCH_PARENT,那麼子View將根據列數(假定是垂直排列)平均分配Recycle
可迴圈使用的屏障CyclicBarrier(原始碼分析)
前文有分析了發令槍的原理,這裡先總結一下CountDownLatch和CyclicBarrier的區別。 1.發令槍是一次性的,無法重置,迴圈屏障可重複使用(reset) 2.發令槍是在所有任務都執行結束統一退出的時候使用,迴圈屏障是還沒開始任務前統一步調
vue中nexttick原理(原始碼分析)
nexttick函式的作用是來延遲一個函式的執行。 結合vue nexttick.js原始碼進行分析: /* @flow */ /* globals MessageChannel */ import { noop } from 'shared/util' import
slf4j繫結log4j2日誌系統的過程(原始碼分析)
一、環境及介紹 slf4j和log4j2的介紹在文章http://blog.csdn.net/honghailiang888/article/details/52681777進行過介紹,原始碼分析版本log4j-api-2.2.jar、log4j-core-2.2.jar、
Mybatis總結(一)SqlSessionFactory初始化過程(原始碼分析)
SqlSessionFactoryBuilder根據傳入的資料流生成Configuration物件,然後根據Configuration物件建立預設的SqlSessionFactory例項。Mybatis初始化要經過簡單的以下幾步:1. 呼叫SqlSessionFactoryB
Flink 中LatencyMarks延遲監控(原始碼分析)
流式計算中處理延遲是一個非常重要的監控metric flink中通過開啟配置 metrics.latency.interval 來開啟latency後就可以在metric中看到askManagerJobMetricGroup/operator_id/operator_sub
一篇文章帶您讀懂List集合(原始碼分析)
今天要分享的Java集合是List,主要是針對它的常見實現類ArrayList進行講解 內容目錄 什麼是List核心方法原始碼剖析1.文件註釋2.構造方法3.add()3.remove()如何提升ArrayList的效能ArrayList可以代替陣列嗎? 什麼是List Li
一篇文章帶您讀懂Map集合(原始碼分析)
今天要分享的Java集合是Map,主要是針對它的常見實現類HashMap進行講解(jdk1.8) 什麼是Map核心方法原始碼剖析1.文件註釋2.成員變數3.構造方法4.put()5.get() 什麼是Map Map是非線性資料結構的主要實現,用來存放一組鍵-值型資料,我們稱之
面試高頻SpringMVC執行流程最優解(原始碼分析)
文章已託管到GitHub,大家可以去GitHub檢視閱讀,歡迎老闆們前來Star! 搜尋關注微信公眾號 碼出Offer 領取各種學習資料! SpringMVC執行流程 SpringMVC概述 Spring MVC屬於SpringFrameWork的後續產品,已經融合在Spring Web Flow裡面。
讀Muduo原始碼筆記---3(ttcp分析)
1、ttcp作用:檢測TCP吞吐量 2、ttcp協議: 3、一次寫一定長度的資料 static int write_n(int sockfd, const void* buf, int length) { int written = 0; while (written &l
深入淺出Mybatis---SQL執行流程分析(原始碼篇)
最近太忙了,一直沒時間繼續更新部落格,今天忙裡偷閒繼續我的Mybatis學習之旅。在前九篇中,介紹了mybatis的配置以及使用, 那麼本篇將走進mybatis的原始碼,分析mybatis 的執行流程, 好啦,鄙人不喜歡口水話,還是直接上幹活吧: 1. SqlSessionFactory 與 S
jquery原始碼——merge 淺度克隆兩個陣列(類陣列);
原始碼及應用: function merge(first , second) { var i = first.length; var len = i + second.length; var j = 0; f
自定義view流程(結合原始碼分析)
一、View的繪製流程 主要是:測量(measure)、佈局(layout)、繪製(draw)三大流程。 對於一個普通View(不是容器) 主要是關心測量和繪製兩個過程,測量可以確定自身的寬、高、大小,繪製可以顯示出view的具體內容(呈現在螢幕上的)。 對於
一次ajax呼叫,傳送了兩次請求(一次為請求方法為option,一次為正常請求)
在專案了開發時遇見一個奇怪的現象,就是我在js裡面掉了一次ajax請求,在瀏覽器network那邊查詢到的卻是傳送了兩次請求,第一次的Request Method引數為OPTIONS,第二次的Request Method為我正常設定的POST。 在參考了:https://b