ListView乱谈之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值
         */
       <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的滚动》详细解答



已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值