上一章,主要分析了ListView绘制的三大方法,这三大方法之中又侧重于分析ListView的测量和布局两个方法。其中在布局方法之中,最核心的一个步骤就是根据不同的布局情景来采取不同的填充方式来对ListView的子视图进行填充布局。由上一节【进阶android】ListView源码分析——布局三大方法可知ListView一共有7种布局场景,而这7种布局场景一共使用了7种填充方式来进行子视图的填充与布局。
根据上一节布局方法layoutChildren中的源码,可以知道这7种填充方式对应的方法如下:
1、fillFromSelection,根据被选择的item来上下填充子视图;
2、fillFromMiddle,将被选择的item固定到屏幕的中间来向上向下填充子视图,该方式强制被选择的item处于屏幕的中间位置;
3、fillSpecific,根据一个指定的item来向上向下填充子视图;
4、fillUp,根据一个指定的item向上填充子视图;
5、fillDown,根据一个指定的item向下填充子视图;
6、fillFromTop,从mFirstPosition处开始,从列表的顶部到底部依次填充;
7、fillAboveAndBelow,从上、下填充子视图
不同的布局场景对应着不同的布局时机,例如第一次布局、同步布局等;下面我们将主要分析这7种填充方式的实现方式并简单分析每一种填充方式对应的布局场景。在分析这7个填充方法之前,我们需要明确几点:
1、调用此7种方法的时候,ListView的mSelectedPostion与mNextSeletedPostion相等(两者已经同步更新)。
2、ListView的显示屏幕(或者说View的显示屏幕)分为三个层次,上边缘垂直逐渐消失区域、中间正常显示区域,下边缘垂直逐渐消失区域;入下图所示:
一、fillFromSelection方法:
将新的被选择的item定位到一个制定的位置,以此为基础来上下填充视图;该方法的原型如下:
private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom);
入参一共有三个,其含义如下:
1、selectedTop:新的被选择的item所指定的绘制的地方,top;
2、childrenTop:开始绘制子视图的位置;
3、childrenBottom:子视图被绘制的最后一个像素所在的位置;
该方法将返回当前新的被选择的item对应的视图。
下面我根据该方法的源码来分析该方法:
private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
int fadingEdgeLength = getVerticalFadingEdgeLength();//返回垂直逐渐消失边缘的高度
final int selectedPosition = mSelectedPosition;
View sel;
final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
selectedPosition);//如果被选择的item不是第一个子视图,则开始绘制子视图的位置会在原有的基础上加上fadingEdgeLength
final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
selectedPosition);//如果被选择的item不是最后一个子视图,则结束绘制子视图的位置会在原有的基础上减去fadingEdgeLength
//利用重用的方式获取子视图,并将它添加到ListView指定的位置之中
sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true);
// Some of the newly selected item extends below the bottom of the list
//如果被选择的item对应的视图的底部超出类正常视图的范围(即该视图的部分进入类下方垂直逐渐消失区域之中)
if (sel.getBottom() > bottomSelectionPixel) {
// Find space available above the selection into which we can scroll
// upwards
final int spaceAbove = sel.getTop() - topSelectionPixel;
// Find space required to bring the bottom of the selected item
// fully into view
final int spaceBelow = sel.getBottom() - bottomSelectionPixel;
final int offset = Math.min(spaceAbove, spaceBelow);
// Now offset the selected item to get it into view
sel.offsetTopAndBottom(-offset);//将被选择item对应的子视图向上拉入正常范围之中
} else if (sel.getTop() < topSelectionPixel) {
// Find space required to bring the top of the selected item fully
// into view
final int spaceAbove = topSelectionPixel - sel.getTop();
// Find space available below the selection into which we can scroll
// downwards
final int spaceBelow = bottomSelectionPixel - sel.getBottom();
final int offset = Math.min(spaceAbove, spaceBelow);
// Offset the selected item to get it into view
sel.offsetTopAndBottom(offset);
}
// Fill in views above and below
fillAboveAndBelow(sel, selectedPosition);//向上下填充视图
//太高或者太低的情况进行调整
if (!mStackFromBottom) {
correctTooHigh(getChildCount());
} else {
correctTooLow(getChildCount());
}
return sel;
}
首先该方法确定垂直逐渐消失区域的高度,以此结合第二个入参和第三个入参来确定正常显示区域的范围;
接着调用makeAndAddView方法来生成一个视图,并将它添加到ListView中的指定位置,这个指定位置就是第一个入参制定的top,至于makeAndAddView方法的实现,我们将在下一章进行分析;
将被选择的item对应的子视图填充到ListView中指定的位置,则判断该子视图是否处于正常显示区域的范围之类,如果该视图与上区域垂直逐渐消失区域,或者与下区域垂直逐渐消失区域有交集,则向上、向下调整该子视图的位置(调用View的offsetTopAndBottom的方法),至此确定了被选择的item的子视图的最终位置;
确定了被选择item对应的子视图的最终位置,就可有根据该子视图向上、向下填充子视图,我们将在下文分析fillAboveAndBelow方法的具体实现;
最后,调用correctTooHigh或者correctTooLow来调整整个ListView的最终位置。
那第一次布局的时候会调用此方法吗(选择此分支吗)?答案是否然的!根据上一章layoutChildren方法,我们可知第一次布局时,ListView的布局之前被选择item对应的视图为空(本来就是第一次布局,那么就不存在上一次布局这一说法)。那什么时候会调用此方法呢?我们继续回到layoutChildren方法之中,可知当前布局模式为LAYOUT_SET_SELECTION且布局之后被选择的item对应的视图不为空时会调用此方法。
二、fillFromMiddle方法
该方法根据布局之后被选择的item(此处布局之前的被选择item与布局之后的被选择item已经同步为一致了)来确定基准的item;如果当前被选则的item不存在,则使用用来复活的item(mResurrectToPosition)来作为基准item;如果mResurrectToPosition不符和规范(小于0或者大于mItemCount-1),则以第一个item作为基准item。确定了基准item后,将该基准item对应的子视图放入屏幕的中间位置,最终在以此子视图为标准向上下填充ListView。
该方法的源代码如下:
private View fillFromMiddle(int childrenTop, int childrenBottom) {
int height = childrenBottom - childrenTop;
int position = reconcileSelectedPosition();//获取被选择的item对应的位置
View sel = makeAndAddView(position, childrenTop, true,
mListPadding.left, true);//获取被选择的item对应的视图
mFirstPosition = position;
int selHeight = sel.getMeasuredHeight();
if (selHeight <= height) {
sel.offsetTopAndBottom((height - selHeight) / 2);//将选择视图设定为中间位置
}
fillAboveAndBelow(sel, position);
if (!mStackFromBottom) {
correctTooHigh(getChildCount());
} else {
correctTooLow(getChildCount());
}
return sel;
}
该方法只有两个入参,childrenTop表示子视图能够被绘制的区域的顶部;而childrenBottom表示子视图能够被绘制的区域的底部。
根据源码可知,fillFromMiddle方法首先根据两个入参,计算出子视图能够被绘制的区域的总高度;
接着调用reconcileSelectedPosition方法来确定基准item的位置,即为上文所述的三种情况中的一种(被选择的item,用来复活的item还是第一个item);
调用makeAndAddView方法获取基准item对应的子视图;此时子视图定位在ListView可绘制子视图区域的顶部(makeAndAddView方法的第二个入参为fillFromMiddle方法的第一个入参);
由于上一不基准item对应的子视图定位在可绘制子视图区域的顶部,所以需要进行调整,将该子视图定位在可绘制子视图区域的中间位置;因此此步根据第一步计算出的可绘制子视图区域的总高度和基准item对应的子视图的测量高度,来确定该子视图的最终位置。然后调用View视图的offsetTopAndBottom方法来将顶部的子视图移动到中间的最终位置;
最后,调用correctTooHigh或者correctTooLow来调整整个ListView的最终位置。
与fillFromSelection的使用颇为相似,当布局模式为LAYOUT_SET_SELECTION时,会调用此方法;与fillFromSelection方法不同的时,当前被选择的item为空时,才会调用此方法。
三、fillSpecific方法
该方法将一个特定的item放在屏幕制定的位置之上,然后以此向上向下构建(屏幕)。
方法的原型如下:
private View fillSpecific(int position, int top)
从原型可知该方法有两个入参,两个入参的含义如下:
position:指定item的位置;
top:指定item对应子视图的位置(顶部像素描述);
该方法返回一个视图。
该方法的源代码如下:
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;//指定item是否是被选择的item
//获取指定视图
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
//如果我们在指定视图的上方添加了视图,那么mFirstPosition的值还会改变
//此处就以指定item的位置作为mFirstPosition的值
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 {//向下填充子视图,与向上填充子视图相反,该分支先填充指定子视图下方的子视图,在填充上方的子视图
...
}
if (tempIsSelected) {
return temp;//选择指定的视图
} else if (above != null) {
return above;//返回第一个子视图
} else {
return below;//返回最后一个子视图
}
}
该方法的逻辑较为简单,首先根据两个参数,直接调用makeAndAddView方法获取指定item对应的子视图,并将该视图填充到第二个参数指定的位置;
更新mFirstPosition,使其值等于第一个入参;
如果ListView的填充方式是由上到下,则先调用fillup方法,填充指定item对应的视图上方所有的子视图;接着调用fillDown方法,填充指定item对应的视图下方所有的子视图;如果ListView的填充方式是由下到上,则相反,先调用fillDown,在调用fillUp方法。
最后返回结果视图,如果被选择的item对应的子视图处于可见屏幕中则返回该子视图,否则返回nulll。
四、fillUp方法
该方法从一个指定item开始,向上填充子视图,直到ListView的顶部位置;该方法的原型如下:
private View fillUp(int pos, int nextBottom);
两个入参的含义如下:
pos:指定item的位置;
nextBottom:指定item对应的子视图在屏幕中的位置(底部像素描述);
该方法返回被选择item对应的子视图,如果被选择的子视图没有在该方法之中填充,则返回空。
源代码如下:
private View fillUp(int pos, int nextBottom) {
View selectedView = null;
int end = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end = mListPadding.top;//如果要考虑ListView的padding属性,则结束位置为padding.top
}
//除非填充的子视图的底部小于等于end或者适配器里所有的item填充完毕,否值一直填充
while (nextBottom > end && pos >= 0) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
nextBottom = child.getTop() - mDividerHeight;//更新nextBottom
if (selected) {//是否填充过被选择item
selectedView = child;
}
pos--;//更新pos
}
mFirstPosition = pos + 1;//更新mFirstPosition
...
return selectedView;
}
fillUp的逻辑并不复杂。不过还需要注意的一是,如果是第一次布局,且是从下到上布局,则会调用该方法。
五、fillDown方法
fillDown的逻辑与fillUp的逻辑极为相似,只不过一个是向下填充子视图,一个是向上填充;直接贴上源码:
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) {//向下加载的第一个非处于屏幕中的item及其对应的视图高度如果合法,则执行加载
// is this the selected item?
boolean selected = pos == mSelectedPosition;//当前加载的item是否是当前被选择的item
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);//获取正在加载的item对应的视图
nextTop = child.getBottom() + mDividerHeight;//更新下一个加载item对应视图的高度
if (selected) {
selectedView = child;
}
pos++;//更新下一个加载item所在的位置
}
......
return selectedView;
}
该方法并未在layoutChildren方法之中被调用;它作为一个原子逻辑被其他填充方法调用。六、fillFromTop方法
顾名思义,该方法是指从ListView的顶部开始,向下填充子视图;该方法只是调用了一下fillDown,源码如下:
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);
}
该方法还需要注意的一是,如果是第一次布局,且是从上到下布局,则会调用该方法。
七、fillAboveAndBelow方法
该方法一旦被选择视图被定位,上下填充可视区域;该方法与fillSpecific方法的逻辑颇为相似;源码如下:
private void fillAboveAndBelow(View sel, int position) {
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {//如果是从上到下填充
fillUp(position - 1, sel.getTop() - dividerHeight);//先填充被选择视图上方的视图
adjustViewsUpOrDown();//调整第一个视图与列表视图的顶部对齐
fillDown(position + 1, sel.getBottom() + dividerHeight);//再填充被选择视图下方的视图
} else {//如果时从下到上,则相反先调用fillDown方法,再调用fillUp方法
......
}
}
该方法并未在layoutChildren方法之中被调用;它作为一个原子逻辑被其他填充方法调用。
最后我们通过一个表格,来对比这7种填充方式:
方法名 | 入参信息 | 第一填充item对应的位置 | 使用场景 | 调用模式 |
fillFormSelection | selectedTop:被选择item对应视图的位置; childrenTop:子视图可绘制区域顶部; childrenBottom:子视图可绘制区域底部; | 布局后被选择item | 1、LAYOUT_SET_SELECTION布局模式, 且存在布局后被选择item对应的子视图。 | layoutChildren 方法调用。 |
fillFromMiddle | childrenTop:子视图可绘制区域顶部; childrenBottom:子视图可绘制区域底部; | 三种可能: 1、被选择item; 2、mResurrectToPosition; 3、0. | 1、LAYOUT_SET_SELECTION布局模式, 且不存在布局后被选择item对应的子视图。 | layoutChildren 方法调用。 |
fillSpecific | position:指定的位置; top:指定定位的位置; | 入参position | 1、LAYOUT_SYNC布局模式; 2、LAYOUT_SPECIFIC布局模式; 3、LAYOUT_NORMAL布局模式,且存在子视图。 | layoutChildren 方法调用。 |
fillUp | pos:从哪一个item开始向上填充; nextBottom:当前填充视图的底部位置; | 入参pos | 1、LAYOUT_FORCE_BOTTOM布局模式; 2、LAYOUT_NORMAL布局模式,无子视图,且布局方式是从下到上; |
|
fillDown | pos:从哪一个item开始向下填充; nextTop:当前填充视图的顶部位置; | 入参pos | 无 | 其他布局方式调用。 |
fillFromTop | nextTop:当前填充视图的顶部位置; | mFirstPosition | 1、LAYOUT_FORCE_TOP布局模式; 2、LAYOUT_NORMAL布局模式,无子视图,且布局方式是从上到下; |
|
fillAboveAndBelow | sel:指定从该子视图开始布局; position:从该位置开始向上向下填充布局; | 入参positition | 无 | 其他布局方式调用。 |
至此,7种填充方式便分析完了;7种填充方式的共性是:先找到一个特定的item位置,获取该item的子视图,并把它定位在一个指定的屏幕位置上,然后在以此子视图为基准点,开始向上或者向下填充余下的子视图。
除了这7种填充方式,ListView还有一些其他的填充方式,例如moveSelection、fillGap等方法,用在一些特殊的地方,由于篇幅的缘故,就不再具体分析。