热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ListView乱谈之ListView的布局

本来预备写一篇博客的,写着写着发现要想细细写起来还是要很大篇幅,所以就预计写三篇博客。本篇主要是写ListView的布局,相对来说是本篇篇幅不是很大,其实对于android高手来说ListView

  本来预备写一篇博客的,写着写着发现要想细细写起来还是要很大篇幅,所以就预计写三篇博客。本篇主要是写ListView的布局,相对来说是本篇篇幅不是很大,其实对于android高手来说ListView的布局他们应该很容易就能知道其原理,不过还是准备把我的心得写出来,有不足和错误之处欢迎批评吐槽,批评吐槽过后再给指点一二。

ListView的布局就像在我之前实现的简单的横向ListView那样(详情点击此处),核心方法就是layout(left,top,right,bottom)方法的调用,该方法参数可以用如下图来说明:


其实通过这个图不难想象出让Adapter对象里getView方法所返回的View一个个竖直排列的思想很简单:在ListView高度允许的范围内,循环遍历Adapter中的ItemView,对该View进行测量并通过layout方法布局到ListView中去;然后取Adapter中的下一个position的View(在此称之为nextView),通过相应的位置计算,让nextView布局在上一个View的下面,到此完成布局的过程。上面所说的相应的位置计算,主要是改变每个ItemView的layout方法中第二个参数(top)的值。这个值每次递增的量(或者说下一个Itemview的top值)为:preItemView.getBottom() + mDividerHeight(该变量为ListView中ItemView之间的间隔).

简单的图例:



ListView的布局类型(layoutMode)有好几个,这里就从自上而下的布局开始讲起,布局涉及到的方法调用简单脉络可以表示为layoutChildren()--->fillFromTop(nextTop )-->fillDown(position,nextTop)-->makeAndaddView(position, nextTop,....)-->setupChild(child, position, nextTop, .., ......)-->view.layout(left,top,right,bottom);通过这个方法脉络可以看出nextTop一直在随着这些方法传递着(貌似是废话)。其中方法position代表着Adapter中第position位置的那个ItemView,该参数最终在makeAndAddView方法中使用,其使用也很简单: child = obtainView(position, mIsScrap);只要简单的阅读源码 就知道obtainView方法中调用了adapter.getView(position,convertView,parent)方法。

 private View fillDown(int pos, int nextTop) {
View selectedView = null;
//获取listView的高度
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}

/***
循环遍历对当前child布局,并计算下一个child的layout的top值
*/
 while (nextTop             // is this the selected item?
boolean selected = pos == mSelectedPosition;
//将此child添加并布局到ListView中
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

//计算下一个child的layout方法的top值
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
//该参数用来表示Adapter中下一个child的位置,也就是getView方法中第一个参数
pos++;
}

最终的布局是在setupChild方法中进行的:该方法大整体上分成两个部分,一是对先itemView进行测量(
详细点击此处),二是对测量过后的View通过layout方法布局到ListView中(
点击这里是关于layout的一个简单的应用);大体代码如下:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
//此处省略若干代码

// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);

if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
//讲child 添加到ViewGroup的数组View[] mChildren中去
 addViewInLayout(child, flowDown ? -1 : 0, p, true);
}


//进行测量
if (needToMeasure) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
//开始进行测量
 child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}

//获取测量后的
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
//此处的y就是上文所说的nextTop
 final int childTop = flowDown ? y : y - h;

if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
//此处正是child进行布局的真正地方
 child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}

//此处省略若干代码

 }

到此为止,ListView的实现布局的原理就简单的写完了。不过本文到此并未结束,还需要讲一些其他的东西,比如重复利用的View以及关于ListVIew的一些小细节。通过上面的代码可发现,fillDown里面有一个方法的while循环,如下:

      while (nextTop             boolean selected = pos == mSelectedPosition;
//将此child添加并布局到ListView中
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

//计算下一个child的layout方法的top值
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
//该参数用来表示Adapter中下一个child的位置,也就是getView方法中第一个参数
pos++;
}
while循环的一个条件就是nextTop

getChildCount返回的是ListView中在屏幕中可看到的itemView的个数

getCount()返回的是mItemCount,该变量是在调用setAdapter等方法的时候通过 mItemCount = mAdapter.getCount();很简单就是Adapter里面有多少个Item,mItemCount就等于多少。

同时,我们知道getFirstVisiablePosition():获取在页面中第一个可见的View(item),也就是adapter的getView方法参数中参数position对应的值,在ListView的父类AdapterView中用mFirstPosition变量来表示,哪怕只有部分的View显示出来,也被当做第一个可见的view;getLastVisiablePosition():获取在页面中最有一个可见的View(item),哪怕只有部分的View显示出来也被当做最有一个可见的View,所以

getLastVisiablePosition() == getFirstVisiablePosition()+getChildCount()-1


在android中addView的时候,最终通过addViewInLayout调用了ViewGroup里面addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout)方法(该方法又调用了addInArray方法),ViewGrouup里面提供一个View的数组mChildren,在addView方法中调用了addVew(view view,index),index默认传的是-1,表明添加到View数组mChildren最后面,当index为正数的时候就把该View插入到数组中的index的位置,相应的View数组mChildren里面的元素后移;也就说说本质上ViewGroup里面的addView系列重载方法其实就是对mChilderen这个View类型的数据进行的数组插入操作。


在setupChild方法中就调用了 addViewInLayout(child, flowDown ? -1 : 0, p, true)把Adapter中的View添加到了ViewGroup的数组中去因为在代码中有if(index<0){index=mChildren}这个处理。按照我们上面的探讨顺序,flowDown是true:也就是addInArray参数的index为-1;

 

 private void addInArray(View child, int index) {
View[] children = mChildren;
final int count = mChildrenCount;//获取ViewGroup
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 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);
}
}

因为getChildAt(int index)就是从数组mChildren获取返回对应索引的View:return mChildren[index],所以需要注意的是:在ListView中,使用getChildAt(index)的取特定位置的View的时候,index的取值范围是 index>=getFristVisiblePosition()&&index<==getlastVisiablePosition();超出此范围的话getChildAt会返回null;

另外AdapterView里面mSelectedPosition这个变量,代表着当前ListView中选中的ItemView所在的位置,对应的是该ItemView在getView中position的值);而AbsListView中的方法getSelectedView返回也是mChildren数组里面对应位置的View,所以这样的话getSelectedView返回的View=mChildren[mSelectionPosition-mFirstPosition]就不难理解了。

  public View getSelectedView() {
if (mItemCount > 0 && mSelectedPosition >= 0) {
return getChildAt(mSelectedPosition - mFirstPosition);
} else {
return null;
}
}
public View getChildAt(int index) {
        if (index <0 || index >= mChildrenCount) {
            return null;
        }
        return mChildren[index];
    }


到此位置,ListView的布局就算是告一段落,通过读取里面的代码加深理解和学到不少的知识,写了这么多貌似有点啰嗦,

简单总结一下:

1)ListView的layout只是循环遍历Adapter中的View,通过累加layout方法的top参数的值来把一个个itemView 布局我们所见到的的那些效果

2)每次布局的时候,只是向ListView的mChildren数组中(该数组在ListView的父类ViewGroup中定义)添加Adapter中的部分ItemView,而不是全部。getChildAt方法和getSelectedView方法都是从mChildren数组中获取到对应的View返回之。

最后丢一个简单的疑问:

1)既然每次addView的时候不把全部的ItemView添加完,那么其余的itemView是什么时候,怎么添加进来的呢?

2)在添加新的itemView之前或者之后对mChildren数组都做了怎样的操作,这种操作的时机和目的是什么?

这些问题将在下一篇博客:《ListIView乱谈之ListView的滚动》详细解答




推荐阅读
author-avatar
赢在青春创业团队
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有