zoukankan      html  css  js  c++  java
  • Android ListView的理解(一)

    一、概述

            这部分主要是讲解ListView在初始时,即setAdapter之后,如何将Adapter中取得的view添加到ListView中,主要涉及到几个方法:

            layoutChildren,fillFromTop,fillDown / fillUp,makeAndAddView,obtainView,setupChild;

    二、讲解

            layoutChildren是个最重要的方法,也很复杂,本篇略过,会在以后中提到,不过,为了讲解其它方法,还是提一下:

        @Override
        protected void layoutChildren() {
                .........
    			
                boolean dataChanged = mDataChanged;
                if (dataChanged) {
                    handleDataChanged();
                }
    			
                .........
    			
                // Clear out old views
                detachAllViewsFromParent();
    
                switch (mLayoutMode) {
                .........
    			
                case LAYOUT_FORCE_BOTTOM:
                    sel = fillUp(mItemCount - 1, childrenBottom);
                    adjustViewsUpOrDown();
                    break;
                case LAYOUT_FORCE_TOP:
                    mFirstPosition = 0;
                    sel = fillFromTop(childrenTop);
                    adjustViewsUpOrDown();
                    break;
    				
                .........
                }
    			
    	   .........
        }

            setAdapter之后,dataChanged是true,然后走到handleDataChanged方法,在这个方法中会设置mLayoutMode,通常是LAYOUT_FORCE_TOP,即从顶部开始一个一个的往下添加childview:

        @Override
        protected void handleDataChanged() {
            .........
    		
            // Nothing is selected. Give up and reset everything.
            mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;
            mSelectorPosition = INVALID_POSITION;
            checkSelectionChanged();
        }

            这个方法的最后,就会去设置mLayoutMode,并设置其它position为INVALID属性;
            回到layoutChildren中,设置mFirstPosition=0后,将childrenTop(=0或padding top 后的值),进入fillFromTop方法:

        /**
         * Fills the list from top to bottom, starting with mFirstPosition
         *
         * @param nextTop The location where the top of the first item should be
         *        drawn
         *
         * @return The view that is currently selected
         */
        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);
        }

             该方法,第一个child的position起始点,然后调用fillDown;

        /**
         * Fills the list from pos down to the end of the list view.
         *
         * @param pos The first position to put in the list
         *
         * @param nextTop The location where the top of the item associated with pos
         *        should be drawn
         *
         * @return The view that is currently selected, if it happens to be in the
         *         range that we draw.
         */
        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;
                View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
    
                nextTop = child.getBottom() + mDividerHeight;
                if (selected) {
                    selectedView = child;
                }
                pos++;
            }
    
            return selectedView;
        }

            首先,获得可视区域的高度(mBottom - mTop);

            其次,while循环中,判断:累计添加到listview中child的高度,不超过可视区域(添加最后一个child时,有可能只显示部分),且添加的child的下标不超过总的个数(否则系统会报 OutOfBounds 的异常);

            在循环中,会去调用makeAndAddView,这个方法不会真正的去添加child,但会调用之后的setupChild来真正添加到listview中:

        /**
         * Obtain the view and add it to our list of children. The view can be made
         * fresh, converted from an unused view, or used as is if it was in the
         * recycle bin.
         *
         * @param position Logical position in the list
         * @param y Top or bottom edge of the view to add
         * @param flow If flow is true, align top edge to y. If false, align bottom
         *        edge to y.
         * @param childrenLeft Left edge where children should be positioned
         * @param selected Is this position selected?
         * @return View that was added
         */
        private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                boolean selected) {
            View child;
    
    
            if (!mDataChanged) {
                // Try to use an existing view for this position
                child = mRecycler.getActiveView(position);
                if (child != null) {
                    if (ViewDebug.TRACE_RECYCLER) {
                        ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
                                position, getChildCount());
                    }
    
                    // Found it -- we're using an existing child
                    // This just needs to be positioned
                    setupChild(child, position, y, flow, childrenLeft, selected, true);
    
                    return child;
                }
            }
    
            // Make a new view for this position, or convert an unused view if possible
            child = obtainView(position, mIsScrap);
    
            // This needs to be positioned and measured
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    
            return child;
        }

            该方法分两种情况:
            1. adapter中的数据发生了变化,初始setAdapter,或之后我们将adapter中的数据做了新增/删除后,调用Adapter.notifyDataSetChanged;变化了,就会从RecycleBin中的mScrapView中,取之前移出的view,即convertView来复用;

            2. 若没有变化,则从RecycleBin的mActiveView中取当前显示的view(为啥会有这种情况?当listview稳定后,我们不滚动它,但有可能点击或长按),这时就会走到这里。

            obtainView就是从RecycleBin中,取移出去的View,传给Adapter.getView方法(convertView),我们先来看看这个方法:

        /**
         * Get a view and have it show the data associated with the specified
         * position. This is called when we have already discovered that the view is
         * not available for reuse in the recycle bin. The only choices left are
         * converting an old view or making a new one.
         *
         * @param position The position to display
         * @param isScrap Array of at least 1 boolean, the first entry will become true if
         *                the returned view was taken from the scrap heap, false if otherwise.
         *
         * @return A view displaying the data associated with the specified position
         */
        View obtainView(int position, boolean[] isScrap) {
            isScrap[0] = false;
            View scrapView;
    
            scrapView = mRecycler.getScrapView(position);
    
            View child;
            if (scrapView != null) {
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP,
                            position, -1);
                }
    
                child = mAdapter.getView(position, scrapView, this);
    
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW,
                            position, getChildCount());
                }
    
                if (child != scrapView) {
                    mRecycler.addScrapView(scrapView, position);
                    if (mCacheColorHint != 0) {
                        child.setDrawingCacheBackgroundColor(mCacheColorHint);
                    }
                    if (ViewDebug.TRACE_RECYCLER) {
                        ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
                                position, -1);
                    }
                } else {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                }
            } else {
                child = mAdapter.getView(position, null, this);
                if (mCacheColorHint != 0) {
                    child.setDrawingCacheBackgroundColor(mCacheColorHint);
                }
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW,
                            position, getChildCount());
                }
            }
    
            return child;
        }

            这个方法,实际上是在AbsListView类中的,我们知道,ListView, GridView是继承于AbsListView的,而AbsListView是继承于AdapterView<ListAdapter>,而这个是继承于ViewGroup,所以,ListView或AbsListView中,有些变量或方法,都是继承过来的,我们看源码时,需要结合这些类一起看(比较头痛,跳来跳去),但熟悉之后,对我们写出高质量的程序是很有帮助的。

            小提示:RecycleBin类,它的作用就是一个View的缓存,将移出屏幕外的view回收,并给新移入到屏幕内的view来复用,这样就能节省大量内存(之后讲解该类)。

            回归正题obtainView方法:

            1. 先从RecycleBin中获取一个ScrapView,如果缓存中没有,则convertView为null,在Adapter中,需要自己去LayoutInflater一个view;

            2. 若有,将这个convertView传给Adapter.getView方法;

            3. 判断从getView方法中返回的view是否与scrapview一致,如果一致,表明是复用的,反之,则程序又去创建了一个新的view(浪费了一块内存),且将得到的scrapview重新加入到RecycleBin.mScrapView中;

            回到makeAndAddView方法,将child传给setupChild,开始真正的加入到listView中去显示:

        /**
         * Add a view as a child and make sure it is measured (if necessary) and
         * positioned properly.
         *
         * @param child The view to add
         * @param position The position of this child
         * @param y The y position relative to which this view will be positioned
         * @param flowDown If true, align top edge to y. If false, align bottom
         *        edge to y.
         * @param childrenLeft Left edge where children should be positioned
         * @param selected Is this position selected?
         * @param recycled Has this view been pulled from the recycle bin? If so it
         *        does not need to be remeasured.
         */
        private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
                boolean selected, boolean recycled) {
            final boolean isSelected = selected && shouldShowSelector();
            final boolean updateChildSelected = isSelected != child.isSelected();
            final int mode = mTouchMode;
            final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
                    mMotionPosition == position;
            final boolean updateChildPressed = isPressed != child.isPressed();
            final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
    
            // 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 = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT, 0);
            }
            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;
                }
                addViewInLayout(child, flowDown ? -1 : 0, p, true);
            }
    
            if (updateChildSelected) {
                child.setSelected(isSelected);
            }
    
            if (updateChildPressed) {
                child.setPressed(isPressed);
            }
    
            if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
                if (child instanceof Checkable) {
                    ((Checkable) child).setChecked(mCheckStates.get(position));
                } else if (getContext().getApplicationInfo().targetSdkVersion
                        >= android.os.Build.VERSION_CODES.HONEYCOMB) {
                    child.setActivated(mCheckStates.get(position));
                }
            }
    
            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();
            final int childTop = flowDown ? y : y - h;
    
            if (needToMeasure) {
                final int childRight = childrenLeft + w;
                final int childBottom = childTop + h;
                child.layout(childrenLeft, childTop, childRight, childBottom);
            } else {
                child.offsetLeftAndRight(childrenLeft - child.getLeft());
                child.offsetTopAndBottom(childTop - child.getTop());
            }
    
            if (mCachingStarted && !child.isDrawingCacheEnabled()) {
                child.setDrawingCacheEnabled(true);
            }
    
            if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
                    != position) {
                child.jumpDrawablesToCurrentState();
            }
        }

            attachViewToParent 和 addViewInLayout两者大致差不多,都是将view添加到parent view的array中,区别在于,attachView是不用去requestLayout的,而addViewInLayout的最后一个参数指明(true不用requestLayout,false则需要requestLayout),这两个方法都在ViewGroup中。

            flowDown ? -1 : 0 , 不知道大家注意没有, -1 和 0 的区别?我们看下attachViewToParent代码吧:

        /**
         * Attaches a view to this view group. Attaching a view assigns this group as the parent,
         * sets the layout parameters and puts the view in the list of children so it can be retrieved
         * by calling {@link #getChildAt(int)}.
         *
         * This method should be called only for view which were detached from their parent.
         *
         * @param child the child to attach
         * @param index the index at which the child should be attached
         * @param params the layout parameters of the child
         *
         * @see #removeDetachedView(View, boolean)
         * @see #detachAllViewsFromParent()
         * @see #detachViewFromParent(View)
         * @see #detachViewFromParent(int)
         */
        protected void attachViewToParent(View child, int index, LayoutParams params) {
            child.mLayoutParams = params;
    
            if (index < 0) {
                index = mChildrenCount;
            }
    
            addInArray(child, index);
    
            child.mParent = this;
            child.mPrivateFlags = (child.mPrivateFlags & ~DIRTY_MASK & ~DRAWING_CACHE_VALID) |
                    DRAWN | INVALIDATED;
            this.mPrivateFlags |= INVALIDATED;
    
            if (child.hasFocus()) {
                requestChildFocus(child, child.findFocus());
            }
        }

            如果是-1,则将index = mChildrenCount,mChildrenCount就是当前parent view已经有多少个child view,这里将index设置为当前child view个数,意在表明如果是 -1 则将该view加入到parent view中的child view最后,如果是0, 则加入到child view 数组中的第一个;即:该view是插入到顶部,还是添加到底部。
            默认情况下,添加到listview中的item,即child都会measure一次高度和宽度,然后,调用child.layout,通知新添加的child,layout一下它里面的children。

            然后,然后就没有了然后,整个流程走完,回到ListView.layoutChildren中,adjustViewsUpOrDown将所有child调整对齐,刷新一下RecycleBin的Active和Scrap缓存,调用updateScrollIndicators更新一下滚动条的值,若有注意OnScrollListener,也通知一下invokeOnItemScrollListener。

    三、总结

            虽然,只讲了一个fillDown,其实fillUp以及ListView中的其它fillXXX方法,是差不多的,本篇的目的,也只是希望大家了解ListView / GridView继承于AbsListView时,如何将child添加进去。

           之后,会涉及ListView / GridView, AbsListView 以及其内部类 RecycleBin的一些讲解,也欢迎大家留言,一起讨论。

  • 相关阅读:
    perl linux 编译成可执行程序
    MQSeries::Message 面向对象接口的MQSeries messages
    数据化管理第一步,你真的会做报表吗?
    数据化管理第一步,你真的会做报表吗?
    aix ipcs 官方解释
    使用jquery.qrcode生成二维码及常见问题解决方案
    案例分享|某医药集团的BI建设案例
    aix ipcs OxFFFFFFFF
    案例分享|某医药集团的BI建设案例
    地图应用之 :获取当前位置的经纬度
  • 原文地址:https://www.cnblogs.com/james1207/p/3400469.html
Copyright © 2011-2022 走看看