目录
RelativeLayout绘制流程解析
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