-
setAdapter
ListView是根据数据来生成UI的,而数据是交给adapter来管理的,我们从setAdapter方法入手。
@Override public void setAdapter(ListAdapter adapter) { if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } resetList(); mRecycler.clear(); //封装HeaderView和FooterView到Adapter中 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { mAdapter = wrapHeaderListAdapterInternal(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(); }
首先,resetList方法逻辑走完会把所有的已添加的View(包括header和footer)都移除:
@Override void resetList() { // The parent's resetList() will remove all views from the layout so we need to // cleanup the state of our footers and headers clearRecycledState(mHeaderViewInfos); clearRecycledState(mFooterViewInfos); super.resetList(); mLayoutMode = LAYOUT_NORMAL; }
super.resetList()也就是AbsListView的resetList方法:
/** * The list is empty. Clear everything out. */ void resetList() { removeAllViewsInLayout(); mFirstPosition = 0; mDataChanged = false; mPositionScrollAfterLayout = null; mNeedSync = false; mPendingSync = null; mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); mSelectedTop = 0; mSelectorPosition = INVALID_POSITION; mSelectorRect.setEmpty(); invalidate(); }
这里会调用一次invalidate方法同步清空UI。
最后会调用requestLayout方法,按照requestLayout方法的逻辑,我们先来看onMeasure方法。
-
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; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); 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); } if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; } if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; }
在这里测量ListView的宽高,可以看出这么几个信息,宽是通过第一个View的宽来定的(然后加上竖直滚动条的宽度),这就说明ListView一定是等宽的,而且可以看出ListView都是按照竖直方向的List设计的;高度是通过measureHeightOfChildren方法测量的,注意如果是MeasureSpec.UNSPECIFIED模式则以第一个View的高度加上上下内边距和上下渐变边界来算作高度,measureHeightOfChildren是在“合法的”MeasureSpec.AT_MOST模式时调用,也就是wrap_content时,可以理解,除此之外,EXACTLY模式(精确尺寸或match_parent)的话直接使用heightSize即
可。
-
measureHeightOfChildren
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, int maxHeight, int disallowPartialChildPosition) { final ListAdapter adapter = mAdapter; if (adapter == null) { return mListPadding.top + mListPadding.bottom; } // Include the padding of the list int returnedHeight = mListPadding.top + mListPadding.bottom; final int dividerHeight = mDividerHeight; // The previous height value that was less than maxHeight and contained // no partial children int prevHeightWithoutPartialChild = 0; int i; View child; // mItemCount - 1 since endPosition parameter is inclusive endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; final AbsListView.RecycleBin recycleBin = mRecycler; final boolean recyle = recycleOnMeasure(); final boolean[] isScrap = mIsScrap; for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec, maxHeight); if (i > 0) { // Count the divider for all but one child returnedHeight += dividerHeight; } // Recycle the view before we possibly return from the method if (recyle && recycleBin.shouldRecycleViewType( ((LayoutParams) child.getLayoutParams()).viewType)) { recycleBin.addScrapView(child, -1); } returnedHeight += child.getMeasuredHeight(); if (returnedHeight >= maxHeight) { // We went over, figure out which height to return. If returnedHeight > maxHeight, // then the i'th position did not fit completely. return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) && (i > disallowPartialChildPosition) // We've past the min pos && (prevHeightWithoutPartialChild > 0) // We have a prev height && (returnedHeight != maxHeight) // i'th child did not fit completely ? prevHeightWithoutPartialChild : maxHeight; } if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { prevHeightWithoutPartialChild = returnedHeight; } } // At this point, we went through the range of children, and they each // completely fit, so return the returnedHeight return returnedHeight; }
上面传进来的endPosition是NO_POSITION,所以这里取adapter.getCount()-1作为endPosition,然后通过obtainView方法循环获取View,每一项通过returnedHeight += child.getMeasuredHeight()把所有的View的高度加起来(这也说明item的高度是可以不同的),此外除了第一项之外,每一项还要加上divider的高度,最后的处理就是判断是否超出ListView设置的最大高度,如果超出了则可以根据disallowPartialChildPosition判断如果在此项之前有“可以未全部显示的View”的话符合条件的话则返回此项之前已得的总高度,否则返回maxHeight,这里其实disallowPartialChildPosition在onMeasure中是硬编码的“-1”,所以若超出最大高度只会返回最大高度,应该是设计者预留的功能。
-
obtainView
obtainView方法是AbsListView中的方法:
View obtainView(int position, boolean[] outMetadata) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); outMetadata[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } 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 if (child.isTemporarilyDetached()) { outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } //L1 setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }
前面的步骤是关于View复用的,我们先看普通情况,child是通过mAdapter.getView方法获取的,这就是我们自定义Adapter的时候需要重写这个方法来构造Item View的原因,L1处的setItemViewLayoutParams方法为:
private void setItemViewLayoutParams(View child, int position) { final ViewGroup.LayoutParams vlp = child.getLayoutParams(); LayoutParams lp; if (vlp == null) { lp = (LayoutParams) generateDefaultLayoutParams(); } else if (!checkLayoutParams(vlp)) { lp = (LayoutParams) generateLayoutParams(vlp); } else { lp = (LayoutParams) vlp; } if (mAdapterHasStableIds) { lp.itemId = mAdapter.getItemId(position); } lp.viewType = mAdapter.getItemViewType(position); lp.isEnabled = mAdapter.isEnabled(position); if (lp != vlp) { child.setLayoutParams(lp); } }
可以看到mAdapter.getItemId、mAdapter.getItemViewType的获取,generateLayoutParams方法会包装成AbsListView.LayoutParams对象,这个类继承自ViewGroup.LayoutParams,不过多了几个像viewType这样的属性,那么现在我们知道child的LayoutParams持有了mAdapter的相关方法返回的值。
mRecycler是RecycleBin(回收站),是AbsListView的一个内部类,前面会先调用mRecycler.getTransientStateView(position)来尝试取可复用的View,如果和mAdapter当前位置的viewtype相同则表示可以复用,但是依然会通过mAdapter.getView拿到View判断是否和之前的是同一个View,如果不是则调用mRecycler.addScrapView保存。因为现在是初始化时期,所以复用这里有些不容易理解,因为此时还不知道mRecycler里面的数据是怎么来的,我们只知道列表的第一个元素是在onMeasure中保存的:
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType( ((LayoutParams) child.getLayoutParams()).viewType)) { mRecycler.addScrapView(child, 0); }
mRecycler.shouldRecycleViewType方法判断只要viewType的值不小于0即可,默认为0,所以这里可以知道,如果要设置多布局ListView,则Adapter中返回的ItemViewType不能小于0,否则影响复用。addScrapView方法如下:
void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { // Can't recycle, but we don't know anything about the view. // Ignore it completely. return; } lp.scrappedFromPosition = position; // Remove but don't scrap header or footer views, or views that // should otherwise not be recycled. final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { // Can't recycle. If it's not a header or footer, which have // special handling and should be ignored, then skip the scrap // heap and we'll fully detach the view later. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); // The the accessibility state of the view may change while temporary // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null && mAdapterHasStableIds) { // If the adapter has stable IDs, we can reuse the view for // the same data. if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<>(); } mTransientStateViews.put(position, scrap); } else { // Otherwise, we'll have to remove the view and start over. clearScrapForRebind(scrap); getSkippedScrap().add(scrap); } } else { clearScrapForRebind(scrap); if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } }
首先,viewtype小于0的且不是头或脚view的放到mSkippedScrap中,复用的时候会跳过这些View。scrap.hasTransientState()查询是否有临时状态(这个需要View调用setTransientState方法设置,默认不会有),如果有的话根据需要放到mTransientStateViewsById或mTransientStateViews中(mAdapterHasStableIds = mAdapter.hasStableIds();),否则根据viewType的数量放到不同的集合里,如果只有一种type则放到mCurrentScrap(它是一个ArrayList),不止一种就放到mScrapViews[viewType]中(它是一个ArrayList的数组),以viewType为key,即相当于每一种viewType都有一个mCurrentScrap集合来存放。
-
onLayout
onLayout方法呢,在ListView中没有重写,直接找AbsListView的onLayout方法:
@Override 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(); mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; // TODO: Move somewhere sane. This doesn't belong in onLayout(). if (mFastScroll != null) { mFastScroll.onItemCountChanged(getChildCount(), mItemCount); } mInLayout = false; }
可以看到,getChildAt(i).forceLayout()会修改当前所有子View的重新布局标志位,mRecycler.markChildrenDirty()会调用待复用的子view的forceLayout修改需要重新布局的标识位:
public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).forceLayout(); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).forceLayout(); } } } if (mTransientStateViews != null) { final int count = mTransientStateViews.size(); for (int i = 0; i < count; i++) { mTransientStateViews.valueAt(i).forceLayout(); } } if (mTransientStateViewsById != null) { final int count = mTransientStateViewsById.size(); for (int i = 0; i < count; i++) { mTransientStateViewsById.valueAt(i).forceLayout(); } } }
看一下layoutChildren方法,这个方法是又子类重写的,去ListView找,因为这个方法里处理了包括初始化、数据更新、数据移除、数据添加等很多情况的layout逻辑,所以代码很长,我们取和setAdapter逻辑路线相关的第二个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); } } break;
childCount是通过getChildCount方法获取的,如果是第一次加载的时候是为0的,比如这里的setAdapter逻辑之前是把子view全都移除了,所以这里会走childCount=0的逻辑分支。
这里的fillFromTop、fillUp、fillSpecific都是用来添加子View的,mStackFromBottom标志着是否按照从下往上的顺序添加View,所以这三个方法的作用分别是按照从上往下的顺序添加、按照从下往上的顺序添加和更新某一个确定位置的View,这里我们来看默认的初始化时的按照从上往下的顺序来添加的fillFromTop方法:
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); }
可见就是找出最顶部的位置,正常是0,nextTop是前面传过来的,就是ListView的上方内边距的top,这里可以知道,ListView设置的上内边距其实最终是作为第一个View的顶部的,这也可以理解,滚动的时候是整个ListView的内部整体滚动而不是上方留了个不动的边距,这个方法调用了fillDown方法:
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 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; }
可以看到这里有一个while来循环调用makeAndAddView方法,注意while循环里有两个条件,第一个条件是表示只添加屏幕内能显示出来的(部分也算)Item,第二个条件是Adapter中getCount方法返回的数量之内,selected标志着当前View是否是选中的,最终会返回选中的View,之后在layoutChildren中执行后面和选中相关的逻辑,这里看一下makeAndAddView方法:
@UnsupportedAppUsage private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (!mDataChanged) { // Try to use an existing view for this position. final View activeView = mRecycler.getActiveView(position); if (activeView != null) { // Found it. We're reusing an existing child, so it just needs // to be positioned like a scrap view. setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // Make a new view for this position, or convert an unused view if // possible. final View child = obtainView(position, mIsScrap); // This needs to be positioned and measured. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
这里会先调用mRecycler.getActiveView方法从mActiveViews中尝试找是否已经生成过:
View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; }
这个方法在滚动的时候重新layout的时候才会生效,因为初始化的时候是没有任何子View的。如果没找到就会调用obtainView方法创建新的View,获取或者生成的View通过setupChild方法添加到当前ListView中:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean isAttachedToWindow) { 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 = !isAttachedToWindow || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); p.isEnabled = mAdapter.isEnabled(position); // Set up view state before attaching the view, since we may need to // rely on the jumpDrawablesToCurrentState() call that occurs as part // of view attachment. 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 ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); // If the view was previously attached for a different position, // then manually jump the drawables. if (isAttachedToWindow && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition) != position) { child.jumpDrawablesToCurrentState(); } } else { //Code1 p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); // add view in layout will reset the RTL properties. We have to re-resolve them child.resolveRtlPropertiesIfNeeded(); } 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); } Trace.traceEnd(Trace.TRACE_TAG_VIEW); }
可以看到,selected最终用在了child.setSelected(isSelected),然后不是header或footer的话会走到Code1处,会调用addViewInLayout方法:
protected boolean addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout) { if (child == null) { throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); } child.mParent = null; addViewInner(child, index, params, preventRequestLayout); child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; return true; }
这里又会调用addViewInner方法,在addViewInner方法里又会调用addInArray(child, index)方法:
private void addInArray(View child, int index) { View[] children = mChildren; final int count = mChildrenCount; final int size = children.length; if (index == count) { if (size == count) { mChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; System.arraycopy(children, 0, mChildren, 0, size); children = mChildren; } children[mChildrenCount++] = child; } else if (index < count) { if (size == count) { mChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; System.arraycopy(children, 0, mChildren, 0, index); System.arraycopy(children, index, mChildren, index + 1, count - index); children = mChildren; } else { System.arraycopy(children, index, children, index + 1, count - index); } children[index] = child; mChildrenCount++; if (mLastTouchDownIndex >= index) { mLastTouchDownIndex++; } } else { throw new IndexOutOfBoundsException("index=" + index + " count=" + count); } }
可以看到,在这里把创建的View添加到mChildren中,mChildrenCount也是在这里通过自增的方式变化。
至此,我们知道在onLayout流程中创建的所有的Item。
addViewInner走完之后,回到addViewInLayout方法之后会执行:
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
然后下面会执行到offsetLeftAndRight和offsetTopAndBottom方法,里面会执行invalidateXxx相关方法从而触发View的onDraw方法进行绘制。
前面的getActiveView方法获取的数组是在layoutChildren中通过fillActiveViews方法填充的,他发生在fillXxx系列方法之前,正如前面说的,这个方法在滚动时候重新onLayout的时候才有用,初始化时候是没有任何子View的:
void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; //noinspection MismatchedReadAndWriteOfArray final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; // Remember the position so that setupChild() doesn't reset state. lp.scrappedFromPosition = firstActivePosition + i; } } }
-
滚动
ListView中没有重写onTouchEvent方法,我们看AbsListView中的,在这个方法中,我们只看和子View相关的部分,关于滑动手势、速度等先不考虑。
首先,深拷贝一个当前的MotionEvent对象:
final MotionEvent vtev = MotionEvent.obtain(ev);
然后使用这个新的MotionEvent记录位移数据:
final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { onTouchDown(ev); break; } case MotionEvent.ACTION_MOVE: { onTouchMove(ev, vtev); break; } ... ... }
mNestedYOffset记录的是距第一次按下时的位置的y轴位移距离。
再看onTouchMove方法:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) { if (mHasPerformedLongPress) { // Consume all move events following a successful long press. return; } int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { pointerIndex = 0; mActivePointerId = ev.getPointerId(pointerIndex); } if (mDataChanged) { // Re-sync everything if data has been changed // since the scroll operation can query the adapter. layoutChildren(); } final int y = (int) ev.getY(pointerIndex); switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap. If so, we'll enter scrolling mode. if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) { break; } // Otherwise, check containment within list bounds. If we're // outside bounds, cancel any active presses. final View motionView = getChildAt(mMotionPosition - mFirstPosition); final float x = ev.getX(pointerIndex); if (!pointInView(x, y, mTouchSlop)) { setPressed(false); if (motionView != null) { motionView.setPressed(false); } removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); mTouchMode = TOUCH_MODE_DONE_WAITING; updateSelectorState(); } else if (motionView != null) { // Still within bounds, update the hotspot. final float[] point = mTmpPoint; point[0] = x; point[1] = y; transformPointToViewLocal(point, motionView); motionView.drawableHotspotChanged(point[0], point[1]); } break; case TOUCH_MODE_SCROLL: case TOUCH_MODE_OVERSCROLL: scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); break; } }
因为第一次进入这个方法是在down之后,所以此时的mTouchMode是TOUCH_MODE_DOWN(在onTouchDown中修改的),然后进入startScrollIfNeeded方法:
private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) { // Check if we have moved far enough that it looks more like a // scroll than a tap final int deltaY = y - mMotionY; final int distance = Math.abs(deltaY); final boolean overscroll = mScrollY != 0; if ((overscroll || distance > mTouchSlop) && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { createScrollingCache(); if (overscroll) { mTouchMode = TOUCH_MODE_OVERSCROLL; mMotionCorrection = 0; } else { mTouchMode = TOUCH_MODE_SCROLL; mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop; } removeCallbacks(mPendingCheckForLongPress); setPressed(false); final View motionView = getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); // Time to start stealing events! Once we've stolen them, don't let anyone // steal from us final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } scrollIfNeeded(x, y, vtev); return true; } return false; }
这里会判断滑动距离是否符合滚动的最小距离(根据是否超出手指按下的范围),之前在onTouchEvent中已经通过startNestedScroll(SCROLL_AXIS_VERTICAL)设置了mNestedScrollAxes,overscroll是判断是否是嵌套滑动引发的滚动,这里会把mTouchMode修改为TOUCH_MODE_OVERSCROLL或TOUCH_MODE_SCROLL,最后调用scrollIfNeeded方法,这个方法里:
int rawDeltaY = y - mMotionY; final int deltaY = rawDeltaY; int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
mMotionY是DOWN事件时的y坐标,mLastY等于Integer.MIN_VALUE的时候表示的是手指按下的时候(非fling时按下),所以incrementalDeltaY表示的是距离上一次滑动记录点的y方向的位移距离:
if (incrementalDeltaY != 0) { atEdge = trackMotionScroll(deltaY, incrementalDeltaY); }
如果竖直方向产生了有效位移,则调用trackMotionScroll方法,里面又调用了offsetChildrenTopAndBottom(incrementalDeltaY)方法,这个方法在ViewGroup中定义:
@UnsupportedAppUsage public void offsetChildrenTopAndBottom(int offset) { final int count = mChildrenCount; final View[] children = mChildren; boolean invalidate = false; for (int i = 0; i < count; i++) { final View v = children[i]; v.mTop += offset; v.mBottom += offset; if (v.mRenderNode != null) { invalidate = true; v.mRenderNode.offsetTopAndBottom(offset); } } if (invalidate) { invalidateViewProperty(false, false); } notifySubtreeAccessibilityStateChangedIfNeeded(); }
可以看到,所有的子View的纵轴坐标都同时加上offset,mRenderNode持有了View在界面上显示的属性,所以在这里判断它是否为空可以得出界面上此时是否有正在显示的View,如果有则执行invalidateViewProperty方法:
@UnsupportedAppUsage void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) { if (!isHardwareAccelerated() || !mRenderNode.hasDisplayList() || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) { if (invalidateParent) { invalidateParentCaches(); } if (forceRedraw) { mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation } invalidate(false); } else { damageInParent(); } }
可以看到,调用invalidate引起当前节点以下的view树重绘,从而达到滚动内容的效果。
注意这样一段代码:
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; } // FIXME account for grid vertical spacing too? final int spaceAbove = effectivePaddingTop - firstTop; final int end = getHeight() - effectivePaddingBottom; final int spaceBelow = lastBottom - end; ... ... ... ... final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); }
这段代码作用是什么呢?我们已经知道前面初始化添加的View只是界面上能显示的下的View,那么这里的getChildAt方法取得的就是屏幕上的显示的View(屏幕外的其实也不存在),仔细分析一下不难发现,这段代码其实就是判断滚动的距离是否超出已添加View的总高度(即是否需要显示新View),若是需要显示新的Item则调用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(); } 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()); } }
可以看到,这里又会根据方向调用fillDown或fillUp方法,从而回到obtainView、Adapter.getView的逻辑上去。
在trackMotionScroll方法中还有一个点就是view回收复用的逻辑,拿向上滑动来说吧,代码如下:
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); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } } }
child.getBottom>=top这个算法你可以理解一下,其实就是往上滑动出了屏幕就保存,之所以用incrementalDeltaY而不是0可能是为了拇指的有效滑动做的限制,实际上就是还剩一点滑出屏幕的时候就要保存了。
-
Header和Footer
那么HeaderViews和FooterViews是何时添加的呢?
还记得在onLayout过程中构造View的时候会调用mAdapter.getView方法吧,在setAdapter的时候有这样的代码:
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; }
所以在setAdapter方法调用之前如果通过addHeaderView方法添加过HeaderView的话这里的mAdapter就是HeaderViewListAdapter(在KITKAT版本开始就不需要注意设置的顺序了),所以getView方法就是HeaderViewListAdapter的getView方法:
public View getView(int position, View convertView, ViewGroup parent) { // Header (negative positions will throw an IndexOutOfBoundsException) int numHeaders = getHeadersCount(); if (position < numHeaders) { return mHeaderViewInfos.get(position).view; } // Adapter final int adjPosition = position - numHeaders; int adapterCount = 0; if (mAdapter != null) { adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getView(adjPosition, convertView, parent); } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) return mFooterViewInfos.get(adjPosition - adapterCount).view; }
可以看到,这里会判断position是否小于mHeaderViewInfos的容量,来决定此处是头部View还是常规item,如果不在头部View范围内了,adjPosition表示常规item中的position,如果这个值小于自定义Adapter.getCount()(我们重写的这个方法通常是返回数据集合的长度)则说明是在常规item范围,就去自定义Adapter中的getView方法去取View,如果超出了则表示进入了FooterViews的范围,adjPosition - adapterCount就是要取的FooterViews中的View的下标。
同样,getItemViewType方法也是这样判断的:
public int getItemViewType(int position) { int numHeaders = getHeadersCount(); if (mAdapter != null && position >= numHeaders) { int adjPosition = position - numHeaders; int adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getItemViewType(adjPosition); } } return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; }
-
总结
- ListView会测量View的高度,根据position只创建ListView在屏幕中可见范围内能显示出来的View,关键方法是obtainView。
- 滚动的时候是先把当前屏幕内可见的View整体调整top和bottom从而达到上移或下移的效果,然后计算位移之后是否需要创建新的View,如果需要则重走obtainView方法,关键方法是trackMotionScroll。
- 不管是ScrapView集合还是TransientStateView集合保存了View,每次滑动或者初始化都会走Adapter的getView方法,当前position的View未初始化过的时候是为了创建新View,否则是为了和旧的比较,getView方法中传入的convertView就是之前保存的ScrapView或TransientStateView,这就是ListView为什么需要我们在getView方法中自定义ViewHolder的逻辑,如果没有这个逻辑,那么不管调用getView的目的是不是只是用于比较都需要调用LayoutInflater.inflate方法去重复创建View,从而影响性能,这就是convertView的来源和作用。
- header和footer都可以添加多个,在每次getView的时候都会先跳过这些集合的长度去找到普通的ItemView。
ListView原理解析
©著作权归作者所有,转载或内容合作请联系作者
- 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
- 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
- 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
推荐阅读更多精彩内容
- 目录介绍 1.RecycleView的结构 2.Adapter2.1 RecyclerView.Adapter扮演...
- 1.RecyclerView缓存原理 2.ListView和RecyclerView区别 3.为什么Recycle...