ListView乱谈之ListView的布局

来源:互联网 发布:网络女主播真实收入 编辑:程序博客网 时间:2024/05/18 02:16

  本来预备写一篇博客的,写着写着发现要想细细写起来还是要很大篇幅,所以就预计写三篇博客。本篇主要是写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值         */       <pre name="code" class="java"> while (nextTop < end && pos < mItemCount) {            // 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 < end && pos < mItemCount) {            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<end,这个end值代表着什么呢?源码中是end = (mBottom - mTop);,也就是ListView的高度,严格的来说是ListView可以用来布局的高度(有padding值),而nextTop的意思就不用多说了。通过nextTop<end这个限制可以知道,在ListView中并不是把Adapter中全部的ItemView一次性全部布局到ListVIew中,而是屏幕根据ItemView的高度能显示多少个ItemView就先add多少个ItemView到ListView里面去。所以ListView中getCount()和getChildCount()是两个完全不同的概念:

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 < count) {            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的滚动》详细解答



1 0