ListView 绘制流程解析详解手机开发

目录

LinearLayout绘制流程解析

RelativeLayout绘制流程解析

ListView绘制流程解析

RecyclerView绘制流程解析

絮絮叨叨

从年初到现在,一直积攒了好多知识点没有学习和总结,待办越积越多,此时此刻立下flag,要在这个可爱的八月把所有的待办解决掉。

—- 鲁迅:不,我没说过

背景

在我刚开始学习Android的时候,ListView是列表实现的首选项,现在因为效率等问题慢慢地被RecyclerView替代了,当然,很多简单的业务还是用listview实现更方便。抛开具体现实,光谈理论也是耍流氓的。

为了后面更好的学习RecyclerView,很有必要好好学习了解ListView的绘制机制。

分析

在开始LIstView开始之前,先看下整体重要的数据结构。

AbsListView 是 ListView的父类,也是GridView的父类,当然ListView和GridView都是列表,有很多地方是相似的,因此提取了很多同样的流程到AbsListView中。

AbsListView有一个重要的内部类: RecycleBin

RecycleBin 是 ListView绘制机制的基石,顾名思义,是回收桶的意思。负责专门回收和提供、复用ItemView(ItemView就是ListView中每个滑动的子View)。

RecycleBin

看RecycleBin的注释有这么一段说明它的作用

/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
* storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
* start of a layout. By construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
* could potentially be used by the adapter to avoid allocating views unnecessarily.
**/

简单来说,RecycleBin负责回收复用view,其中有两个复用存储空间:

一个是ActiveView负责回收当前界面的活跃的View,他们保留着当前显示的信息。另外一个ScrapViews,ActiveView当被滑出屏幕会被回收到到ScrapViews,ScrapViews里面的View可以被复用,重新绑定,这样可以避免listView的Adpater重新生成新的View。

翻译一下,就是ActiveViews是负责保存当前屏幕活跃的View,而ScrapViews是保存滑出屏幕非活跃的View。

二者的区别有一个在于,ActiveViews取出来的View可以直接拿来绘制显示,不需要重新绑定,就是调用Adapter的onCreateView(其中,onCreateView具备了创建View和绑定View的逻辑,而RecyclerView将二者分离开了),而ScrapViews取出来的需要重新绑定。

RecyleBin的数据结构

 private View[] mActiveViews = new View[0]; 
// 缓存当前界面显示的子View 
  
 private ArrayList<View>[] mScrapViews; 
 private ArrayList<View> mCurrentScrap; 
 // 存储已经废弃的View,mCurrentScrap在viewTypeCount为1下使用,mScrapViews数组对应viewTypeCount种view 
  
 private int mViewTypeCount; 
// View种类数量,对应Adapter的getItemType() 
 
 private ArrayList<View> mSkippedScrap; 
  
 private SparseArray<View> mTransientStateViews;  
 private LongSparseArray<View> mTransientStateViewsById; 
// mTransientStateViews/mTransientStateViewsById 缓存Transient瞬态的view,若hasStableId=true,则key为itemId 
  

上面其中的mTransientStateViews缓存的是transient状态的view,transient表示瞬间临时状态,通过View的hasTransientState函数来判断,当View处理动画时候,会处于transient状态。

void fillActiveViews(int childCount, int firstActivePosition); 
// 将ListView指定元素存储到mActiveViews 
 
View getActiveView(int position) 
// 从ActiveViews中取出,但只能取一次 
 
void addScrapView(View scrap, int position) 
// 添加ScrapView 
 
View getScrapView(int position) 
// 从ScrapViews中取出 
 
void setViewTypeCount(int viewTypeCount) 
// 设置ViewTypeCount,子View种类 
 
void scrapActiveViews() 
// 将ActiveViews移动到ScrapViews种 
 

在布局之前,如果数据集没有变化,会将显示的子View存到mActivieViews,若数据集发生了变化,将显示的子View添加到mScrapViews,布局完成之后,将mActiveViews剩下的View放到mScrapViews中。

获取子View过程中,如果数据集没有变化,则从mActiveViews直接取出布局子View,若数据集发生了变化,则从mScrapViews中获取,如果匹配到了会重新绑定,如果没匹配到,则创建一个子View绑定。

RecyleBin的大部分的方法都是从上述中获取出View或者存储View的逻辑,在后面源码的过程中再好好具体学习分析。

Measure

之前有分析了ViewGroup/View的绘制流程,其中三个最重要的流程:

measure 测量、layout 布局、draw 绘制

按照这个流程继续来学习ListView,Measure的流程相对比较简单。

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)) {
 
// 存在子View,无法确定高度时候 
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; 
} 

measure的过程相对简单,有一种特殊的情况是在ListView没有确定高度,设定为warp_content时候,会去测量首个子View当做ListView的布局。

Layout

// 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; 
} 
// AbsListView 
protected void layoutChildren() {
 
// 空实现 
} 

layoutChildren在于AbsListView是空实现,由具体的ListView实现布局。

这里是ListView的最重要的一段。

protected void layoutChildren() {
 
.... 
final int firstPosition = mFirstPosition; 
final RecycleBin recycleBin = mRecycler; 
if (dataChanged) {
 // 数据集发生了变化 
// 加入mScrapViews 
for (int i = 0; i < childCount; i++) {
 
recycleBin.addScrapView(getChildAt(i), firstPosition+i); 
} 
} else {
 
// 加入mActiveViews 
recycleBin.fillActiveViews(childCount, firstPosition); 
} 
// 清除掉当前显示view 
detachAllViewsFromParent(); 
recycleBin.removeSkippedScrap(); 
switch (mLayoutMode) {
 
.... 
default: 
// 首次调用layoutChildren时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 {
 
// 非首次调用layoutChildren 
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; 
} 
// 布局完成之后,将mActiveViews剩下的View放到mScrapViews 
recycleBin.scrapActiveViews(); 
.... 
} 

画个图,在这里分成两个逻辑:

首次加载和非首次加载

在这里插入图片描述

继续看下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); 
} 
// 向下填充布局 
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; 
// 获取一个childView,调用了makeAndAddView 
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; 
} 

我们看一下makeAndAddView如何获取一个子View

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 
boolean selected) {
 
// 如果数据集未发生变化,则从mRecycler.getActiveView()从ActiveViews获取 
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; 
} 
} 
// 数据集发生变化,调用obtainView获取View 
final View child = obtainView(position, mIsScrap); 
// This needs to be positioned and measured. 
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); 
return child; 
} 

无论从哪里获取到了子View,都会调用setupChild(),内部对子View进行measure、layout调整子View的位置。

我们继续看在mScrapView中获取子View,也就是obtainView

View obtainView(int position, boolean[] outMetadata) {
 
outMetadata[0] = false; 
// 检查是不是瞬态transient状态,如果是,则从mTransientStateViews获取,这里我们不讨论这种情况 
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; 
} 
// 从mScrapView中获取一个可用的子View,可能为null 
final View scrapView = mRecycler.getScrapView(position); 
final View child = mAdapter.getView(position, scrapView, this); 
if (scrapView != null) {
 
if (child != scrapView) {
 
//说明重新绑定失败了,将加入mScrapViews 
mRecycler.addScrapView(scrapView, position); 
} else if (child.isTemporarilyDetached()) {
 
// 绑定成功 
outMetadata[0] = true; 
// Finish the temporary detach started in addScrapView(). 
child.dispatchFinishTemporaryDetach(); 
} 
} 
// 设置View的布局参数 
setItemViewLayoutParams(child, position); 
return child; 
} 

obtainView()内部调用了 mAdapter.getView(position, scrapView, this);

fillSpecific()填充指定的位置的子View,内部基本逻辑是一样的

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; 
} 
} 

Draw

@Override 
protected void dispatchDraw(Canvas canvas) {
  
// Draw the dividers 
... 
super.dispatchDraw(canvas); 
} 

Draw流程比较简单,绘制divider、overscrollFooter、overscrollHeader等

最后调用交由super.dispatchDraw()父类处理,最后是到ViewGroup,跟之前文章的draw分析是一样的

总结

在布局之前,如果数据集没有变化,会将显示的子View存到mActivieViews,若数据集发生了变化,将显示的子View添加到mScrapViews,布局完成之后,将mActiveViews剩下的View放到mScrapViews中。

获取子View过程中,如果数据集没有变化,则从mActiveViews直接取出布局子View,若数据集发生了变化,则从mScrapViews中获取,如果匹配到了会重新绑定,如果没匹配到,则创建一个子View绑定。

参考

非常感谢郭霖老师的ListView的解析,真的帮助理解了很多。

云图网

云图网

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6259.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论