本文共 20935 字,大约阅读时间需要 69 分钟。
ListView是一个ViewGroup,对于ListView的使用需要分为两个部分,一个是ListView本身,二是adapter,他们各自的作用也是很分明的,ListView负责显示和缓存已经显示过的View;而adapter负责创建View和负责显示界面的内容,如果ListView中已经缓存了,就会从ListView将缓存的View取出传递给adapter,这样adapter就不需要重新创建View了。
这里先看Adapter的这个接口的功能:
public interface Adapter { // 数据是由adapter来维护的,所有当数据有变化时,只有adapter清楚, // 这样当数据更改并要刷新界面时,这个功能就得由adapter来完成了, // 这里注册DataSetObserver的作用就是用来通知系统需要刷新界面了, // 调用notifyDataSetChanged()请求刷新界面,内部调用的就是observer void registerDataSetObserver(DataSetObserver observer); // 上面注册了,这里自然就是解注册了 void unregisterDataSetObserver(DataSetObserver observer); // 返回数据的条数 int getCount(); // 返回该位置的数据实体 Object getItem(int position); // 返回该位置的id,一般就直接返回position了 long getItemId(int position); boolean hasStableIds(); // adapter的主要作用是创建View,然后对生成的View进行填充数据,之后会返回给ListView, // 直到生成的View能填充一屏数据为止,这个阶段参数convertView是为null的,当滑动屏幕 // 的时候,有些View会滑出屏幕,这时候这个View就需要回收了,也就是缓存起来,同时, // 也会有view滑进来,这个滑进来的view会先从缓存中去读取,如果有就会以convertView传 // 进来,这个阶段convertView就是缓存起来的view,这样就达到了复用的目的 // convertView需要展示的数据就是通过position来拿到的 View getView(int position, View convertView, ViewGroup parent); static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; // 决定了传给getView()中view的类型(如果ListView中有多个不同的item) // 返回的值在0(包含)到getViewTypeCount()(不包含)之间 // 原因:getViewTypeCount()决定了缓存View的类型,View是通过ArrayList // 来进行保存的,而ArrayList又是保存在数组中的,而这个数组的大小 // 又是由getViewTypeCount()决定的,而这里返回的值就决定了是从 // 数组的那个位置拿缓存的view // 这里在举个例子:假如ListView中有四种item,也就是有四种布局,实际也 // 就是四种View了,这种取名为viewTypeA,viewTypeB,viewTypeC,viewTypeD, // 这里有一点需要注意,这几种view都会保存这里返回的值,决定他们缓存时是 // 缓存在数组的那个位置,这里返回的值一定是0~3,取出时也是由这里决定取哪 // 个位置缓存的view int getItemViewType(int position); // 这个方法决定了ListView中可以显示几种View int getViewTypeCount(); static final int NO_SELECTION = Integer.MIN_VALUE; // 这里就是判断当前是否有数据,它的作用就是在没有数据时ListView该怎么显示, // 在ListView中设置setEmptyView(),那么当没有数据时,自然就会显示这个view了 boolean isEmpty(); default @Nullable CharSequence[] getAutofillOptions() { return null; }}
上面已经对adapter中各个接口的作用做了说明,主要作用还是决定了View中显示的内容。
看完adapter后,再来看看ListView中用来缓存View的一个内部类RecycleBin,它位于ListView的父类AbsListView中,它的作用就是用来缓存view以及复用时取出对应类型的view,这里先来看下RecycleBin源码实现:
class RecycleBin { private RecyclerListener mRecyclerListener; // 显示在屏幕上第一个View所处的位置 private int mFirstActivePosition; // 保存的是需要显示在屏幕上view private View[] mActiveViews = new View[0]; // view移除屏幕后就缓存在这里, // 这个数组的大小由adapter的getViewTypeCount()决定 private ArrayList[] mScrapViews; // 缓存View类型的数量 private int mViewTypeCount; private ArrayList mCurrentScrap; private ArrayList mSkippedScrap; private SparseArray mTransientStateViews; private LongSparseArray mTransientStateViewsById; // 这个方法在setAdapter()会调用到,viewTypeCount就是adapter的getViewTypeCount()的值 public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //这里创建了一个类型为ArrayList,大小是viewTypeCount的数组,就是用来缓存移出屏幕的View ArrayList [] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList (); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } // 清楚所有缓存起来的view void clear() { if (mViewTypeCount == 1) { final ArrayList scrap = mCurrentScrap; clearScrap(scrap); } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList scrap = mScrapViews[i]; clearScrap(scrap); } } clearTransientStateViews(); } // 这里就是保存所有将要显示在屏幕上的View,childCount就是一屏有多少条数数据 // 这里有个需要注意的地方,layout会执行两遍,第一遍会将adapter中生成的view添 // 加到ListView中,当执行第二遍的时候,再次执行这个方法时,就会将ListView中 // 的子View添加到mActiveViews中,由于第二遍执行的时候也会添加View到ListView中, // 所以这里就是为了解决这个问题,第二次layout的时候,先将ListView中的子view添 // 加到mActiveViews,然后再将ListView中的子view移除,等到再次需要添加子view的 // 时候,就直接从mActiveViews拿了 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; } } } // 这里就是返回需要显示的view,并移除 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; } // 这里就是从缓存中取对应view View getScrapView(int position) { // getItemViewType()返回的值就是作为mScrapViews下标的索引 final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { // 只有一种类型的view就是从这里返回 return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { // 有多种类型的view时就是从这里返回 return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; } // 这里就是将滑出的view缓存起来 void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } lp.scrappedFromPosition = position; // 这里lp.viewType的值就是getItemViewType()的值,正好和前面取缓存view对应上了 final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } ...... final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { ...... } else { clearScrapForRebind(scrap); if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { //这里就是缓存view,这个viewType就是getItemViewType()的值 mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } } // 这里就是返回缓存起来的view,也就是复用View private View retrieveFromScrap(ArrayList scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. // Traverse backwards to find the most recently used scrap view for (int i = size - 1; i >= 0; i--) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { //一般都是执行这里 final View scrap = scrapViews.remove(i); clearScrapForRebind(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearScrapForRebind(scrap); return scrap; } else { return null; } }}
看完上面的代码,这里先来做一些总结:
1、Adapter主要负责数据的处理;
2、Adapter当没有复用View的时候,负责创建View;
3、Adapter在拿到View之后,对View中需要的数据进行填充;
4、RecycleBin主要是对View的缓存和复用的逻辑处理;
上面分析完后,接下来就是该考虑创建出来的VIew是如何显示的,分析的是ListView,那自然是在ListView中去寻找了,这里我们看下ListView的layoutChildren()这个方法,这里需要注意一点,绘制一次,这个方法会执行两次:
protected void layoutChildren() { ...... try { ...... final int childrenBottom = mBottom - mTop - mListPadding.bottom; // 这里初次进来的时候,返回的是0,不是初次进来的时候返回的就是将显示在界面上View的数量 final int childCount = getChildCount(); ...... // 记录第一个显示View的位置, final int firstPosition = mFirstPosition; // 这里拿到RecycleBin对象,方便对view的缓存 final RecycleBin recycleBin = mRecycler; if (dataChanged) { // 数据发生变化时才会执行到这里,这里实际就是将所有的子View缓存起来 for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition + i); } } else { // 数据没有变化时会执行到这里,第一次布局的时候childCount = 0, // 第二次执行的时候会将所有的View添加到RecycleBin的mActiveViews中 recycleBin.fillActiveViews(childCount, firstPosition); } // 这里在第二次布局的时候会将第一次添加的view去不清除,这样就不会产生一份重复的数据, // 前面已经将ListView中的view添加到了mActiveViews中,虽说这里进行了解绑,但再次添加 // 的时候是直接将mActiveViews中的添加进去就可以了,所以对性能没什么影响 detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: ...... break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: ...... break; case LAYOUT_SPECIFIC: ...... break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; default: // 默认情况下都是普通模式LAYOUT_NORMAL,所有会执行到这里,第一次childCount = 0, // 第二次执行到这里的时候ListView中有子View,childCount就不等于0 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) { // 有选中的view时会执行这里 sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { // 首个显示的View的位置小于adapter中数据的条数 sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0, childrenTop); } } break; } ...... } }
从上面看下来,第一次布局的时候会执行fillFromTop(childrenTop),这里就是对ListView中的子view进行布局,跟着进去瞧一瞧,看看他是如何实现的:
/** * 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); }
这个方法中没有什么逻辑,就是先判断第一个显示view的位置的合法性,从注释中可以知道,这里的功能是自顶部到底部开始填充,看来具体的实现逻辑要去看看fillDown()这个方法了:
private View fillDown(int pos, int nextTop) { View selectedView = null; // 底部距离减去顶部距离就是可以用来填充view的像素值 int end = (mBottom - mTop); if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { // 如果设置了padding,这里还需要减去padding的距离 end -= mListPadding.bottom; } // nextTop是第一个view在屏幕显示的位置,传进来的pos是显示在屏幕上的第一 // 个view在adapter中的位置,没循环一次,这个值会加1 while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; // 这里就是创建或获取view View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); // 计算下一个view在屏幕所处的位置,以确定是否填满了屏幕,填满了屏幕就会跳出这个循环 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }
在这个循环中,主要的还是获取view,其他的逻辑也是基于这个view的,那这里就去看看makeAndAddView()方法了:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (!mDataChanged) { // 这里是看是否能拿到一个可以复用的view,前面有提到在第二次布局的时候会将第一次的view // 清除掉,这里就是再次去拿清除掉的view final View activeView = mRecycler.getActiveView(position); if (activeView != null) { // 当有复用的view返回时执行到这这里 setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // 当没有复用的view时,就是通过这个方法去创建view final View child = obtainView(position, mIsScrap); // 在拿到view之后,还没有将view添加到ListView中,那这个方法就是测量view并添加到ListView中去 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
这里获取view有两种方式,一种是拿之前缓存的view,如果之前没有缓存,那么就会重新创建一个view,接下来就去看看obtainView()是如何去创建view的:
View obtainView(int position, boolean[] outMetadata) { ...... // 下面这两个方法可以说是ListView缓存的主要逻辑了,mRecycler.getScrapView(position)是获取缓存的view, // 而mAdapter.getView(position, scrapView, this)这个方法是不是很熟悉,就是我们重新Adapter中getView()方法, // 一开始没有缓存时,scrapView返回的是null,所以getView()中传进去的view参数就为null,这是就需要我们创建view了, // 而当scrapView返回不为null时,这时传进去的view我们就可以直接复用了,这也就是为什么我们一般在getView()中要 // 对传进去的view进行判断,如果为null就创建,不为null就直接使用了, final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { if (child != scrapView) { // 如果传进去的View没被复用,而是重新创建了view,那么会将传进去的view再次添加进复用池中 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); } // 这个方法中就是给生成的view设置一些参数,这下参数中就包括对view的分类 setItemViewLayoutParams(child, position); ...... return child; }
这个方法中的主要逻辑就是生成view还是复用view,当然setItemViewLayoutParams()这个方法也是挺有意思的,它主要是对view的一些参数进行设置,这里去看下它对view的那些参数进行了设置:
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); } // 看这里,还记得getItemViewType()这个Adapter中方法么,这个方法返回的就是view属于哪一种类型, // 当我们在RecycleBin这个类中对view进行缓存的时候用到的就是view的这个viewType lp.viewType = mAdapter.getItemViewType(position); lp.isEnabled = mAdapter.isEnabled(position); if (lp != vlp) { child.setLayoutParams(lp); } }
这个方法中我们主要看对viewType的赋值,这个值对于view的缓存很重要,它区分生成的view时属于哪一类的(item中有多种布局),这也说明了Adapter中getItemViewType()这个方法的作用了。获取view就分析到这了,接下来让我们返回到makeAndAddView()方法,接下来我们再看下它的setupChild()方法做了些什么东西:
/** * Adds a view as a child and make sure it is measured (if necessary) and * positioned properly. * 添加一个view作为子view确保它被测量和放置到合适的位置 */ 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)) { // 第二次布局时会执行到这里,前面有提到会将第一次添加的view detach掉, // 这时只需要将detach的view再次attachViewToParent()就可以了 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 { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } // 将view添加进父布局中,这里其实就是添加到ListView中,第一次布局会执行到这里 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); } // 对子view进行测量 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; // 对子view进行布局 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); }
这里可以分为三个步骤,一是将已经生成的view添加进ListView;二是对添加进去的view进行测量;三是对添加进去的view进行布局,这样一次完整的流程就完成了。
转载地址:http://zkhai.baihongyu.com/