ListView缓存源码分析

        ListView是一个ViewGroup,对于ListView的使用需要分为两个部分,一个是ListView本身,二是adapter,他们各自的作用也是很分明的,ListView负责显示和缓存已经显示过的View;而adapter负责创建View和负责显示界面的内容,如果ListView中已经缓存了,就会从ListView将缓存的View取出传递给adapter,这样adapter就不需要重新创建View了。

        这里先看Adapter的这个接口的功能:

public interface Adapter {
    // 数据是由adapter来维护的,所有当数据有变化时,只有adapter清楚,
    // 这样当数据更改并要刷新界面时,这个功能就得由adapter来完成了,
    // 这里注册DataSetObserver的作用就是用来通知系统需要刷新界面了,
    // 调用notifyDataSetChanged()请求刷新界面,内部调用的就是observer
    void registerDataSetObserver(DataSetObserver observer);

    // 上面注册了,这里自然就是解注册了
    void unregisterDataSetObserver(DataSetObserver observer);

    // 返回数据的条数
    int getCount();

    // 返回该位置的数据实体
    Object getItem(int position);

    // 返回该位置的id,一般就直接返回position了
    long getItemId(int position);

    boolean hasStableIds();

    // adapter的主要作用是创建View,然后对生成的View进行填充数据,之后会返回给ListView,
    // 直到生成的View能填充一屏数据为止,这个阶段参数convertView是为null的,当滑动屏幕
    // 的时候,有些View会滑出屏幕,这时候这个View就需要回收了,也就是缓存起来,同时,
    // 也会有view滑进来,这个滑进来的view会先从缓存中去读取,如果有就会以convertView传
    // 进来,这个阶段convertView就是缓存起来的view,这样就达到了复用的目的
    // convertView需要展示的数据就是通过position来拿到的
    View getView(int position, View convertView, ViewGroup parent);

    static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;

    // 决定了传给getView()中view的类型(如果ListView中有多个不同的item)
    // 返回的值在0(包含)到getViewTypeCount()(不包含)之间
    // 原因:getViewTypeCount()决定了缓存View的类型,View是通过ArrayList
    // 来进行保存的,而ArrayList又是保存在数组中的,而这个数组的大小
    // 又是由getViewTypeCount()决定的,而这里返回的值就决定了是从
    // 数组的那个位置拿缓存的view
    // 这里在举个例子:假如ListView中有四种item,也就是有四种布局,实际也
    // 就是四种View了,这种取名为viewTypeA,viewTypeB,viewTypeC,viewTypeD,
    // 这里有一点需要注意,这几种view都会保存这里返回的值,决定他们缓存时是
    // 缓存在数组的那个位置,这里返回的值一定是0~3,取出时也是由这里决定取哪
    // 个位置缓存的view
    int getItemViewType(int position);

    // 这个方法决定了ListView中可以显示几种View
    int getViewTypeCount();

    static final int NO_SELECTION = Integer.MIN_VALUE;

    // 这里就是判断当前是否有数据,它的作用就是在没有数据时ListView该怎么显示,
    // 在ListView中设置setEmptyView(),那么当没有数据时,自然就会显示这个view了
    boolean isEmpty();

    default @Nullable CharSequence[] getAutofillOptions() {
    return null;
    }
}

上面已经对adapter中各个接口的作用做了说明,主要作用还是决定了View中显示的内容。

        看完adapter后,再来看看ListView中用来缓存View的一个内部类RecycleBin,它位于ListView的父类AbsListView中,它的作用就是用来缓存view以及复用时取出对应类型的view,这里先来看下RecycleBin源码实现:

class RecycleBin {
    private RecyclerListener mRecyclerListener;

    // 显示在屏幕上第一个View所处的位置
    private int mFirstActivePosition;

    // 保存的是需要显示在屏幕上view
    private View[] mActiveViews = new View[0];

    // view移除屏幕后就缓存在这里,
    // 这个数组的大小由adapter的getViewTypeCount()决定
    private ArrayList<View>[] mScrapViews;

    // 缓存View类型的数量
    private int mViewTypeCount;

    private ArrayList<View> mCurrentScrap;

    private ArrayList<View> mSkippedScrap;

    private SparseArray<View> mTransientStateViews;
    private LongSparseArray<View> mTransientStateViewsById;

    // 这个方法在setAdapter()会调用到,viewTypeCount就是adapter的getViewTypeCount()的值
    public void setViewTypeCount(int viewTypeCount) {
        if (viewTypeCount < 1) {
            throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
        }
        //这里创建了一个类型为ArrayList,大小是viewTypeCount的数组,就是用来缓存移出屏幕的View
        ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
        for (int i = 0; i < viewTypeCount; i++) {
            scrapViews[i] = new ArrayList<View>();
        }
        mViewTypeCount = viewTypeCount;
        mCurrentScrap = scrapViews[0];
        mScrapViews = scrapViews;
    }

    // 清楚所有缓存起来的view
    void clear() {
        if (mViewTypeCount == 1) {
            final ArrayList<View> scrap = mCurrentScrap;
            clearScrap(scrap);
        } else {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                final ArrayList<View> scrap = mScrapViews[i];
                clearScrap(scrap);
            }
        }

        clearTransientStateViews();
    }

    // 这里就是保存所有将要显示在屏幕上的View,childCount就是一屏有多少条数数据
    // 这里有个需要注意的地方,layout会执行两遍,第一遍会将adapter中生成的view添
    // 加到ListView中,当执行第二遍的时候,再次执行这个方法时,就会将ListView中
    // 的子View添加到mActiveViews中,由于第二遍执行的时候也会添加View到ListView中,
    // 所以这里就是为了解决这个问题,第二次layout的时候,先将ListView中的子view添
    // 加到mActiveViews,然后再将ListView中的子view移除,等到再次需要添加子view的
    // 时候,就直接从mActiveViews拿了
    void fillActiveViews(int childCount, int firstActivePosition) {
        if (mActiveViews.length < childCount) {
            mActiveViews = new View[childCount];
        }
        mFirstActivePosition = firstActivePosition;

        //noinspection MismatchedReadAndWriteOfArray
        final View[] activeViews = mActiveViews;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
            // Don't put header or footer views into the scrap heap
            if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
                //        However, we will NOT place them into scrap views.
                activeViews[i] = child;
                // Remember the position so that setupChild() doesn't reset state.
                lp.scrappedFromPosition = firstActivePosition + i;
            }
        }
    }

    // 这里就是返回需要显示的view,并移除
    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >=0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }

    // 这里就是从缓存中取对应view
    View getScrapView(int position) {
        // getItemViewType()返回的值就是作为mScrapViews下标的索引
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap < 0) {
            return null;
        }
        if (mViewTypeCount == 1) {
            // 只有一种类型的view就是从这里返回
            return retrieveFromScrap(mCurrentScrap, position);
        } else if (whichScrap < mScrapViews.length) {
            // 有多种类型的view时就是从这里返回
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
        return null;
    }

    // 这里就是将滑出的view缓存起来
    void addScrapView(View scrap, int position) {
        final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
        if (lp == null) {
            return;
        }
        lp.scrappedFromPosition = position;

        // 这里lp.viewType的值就是getItemViewType()的值,正好和前面取缓存view对应上了
        final int viewType = lp.viewType;
        if (!shouldRecycleViewType(viewType)) {
            if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                getSkippedScrap().add(scrap);
            }
            return;
        }
        ......

        final boolean scrapHasTransientState = scrap.hasTransientState();
        if (scrapHasTransientState) {
            ......
        } else {
            clearScrapForRebind(scrap);
            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                //这里就是缓存view,这个viewType就是getItemViewType()的值
                mScrapViews[viewType].add(scrap);
            }

            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
        }
    }

    // 这里就是返回缓存起来的view,也就是复用View
    private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
        final int size = scrapViews.size();
        if (size > 0) {
            // See if we still have a view for this position or ID.
            // Traverse backwards to find the most recently used scrap view
            for (int i = size - 1; i >= 0; i--) {
                final View view = scrapViews.get(i);
                final AbsListView.LayoutParams params =
                        (AbsListView.LayoutParams) view.getLayoutParams();

                if (mAdapterHasStableIds) {
                    final long id = mAdapter.getItemId(position);
                    if (id == params.itemId) {
                        return scrapViews.remove(i);
                    }
                } else if (params.scrappedFromPosition == position) {
                    //一般都是执行这里
                    final View scrap = scrapViews.remove(i);
                    clearScrapForRebind(scrap);
                    return scrap;
                }
            }
            final View scrap = scrapViews.remove(size - 1);
            clearScrapForRebind(scrap);
            return scrap;
        } else {
            return null;
        }
    }

}

看完上面的代码,这里先来做一些总结:

1、Adapter主要负责数据的处理;

2、Adapter当没有复用View的时候,负责创建View;

3、Adapter在拿到View之后,对View中需要的数据进行填充;

4、RecycleBin主要是对View的缓存和复用的逻辑处理;

上面分析完后,接下来就是该考虑创建出来的VIew是如何显示的,分析的是ListView,那自然是在ListView中去寻找了,这里我们看下ListView的layoutChildren()这个方法,这里需要注意一点,绘制一次,这个方法会执行两次:

    protected void layoutChildren() {
        ......

        try {
            ......
            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
            // 这里初次进来的时候,返回的是0,不是初次进来的时候返回的就是将显示在界面上View的数量
            final int childCount = getChildCount();
            ......

            // 记录第一个显示View的位置,
            final int firstPosition = mFirstPosition;
            // 这里拿到RecycleBin对象,方便对view的缓存
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
                // 数据发生变化时才会执行到这里,这里实际就是将所有的子View缓存起来
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition + i);
                }
            } else {
                // 数据没有变化时会执行到这里,第一次布局的时候childCount = 0,
                // 第二次执行的时候会将所有的View添加到RecycleBin的mActiveViews中
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // 这里在第二次布局的时候会将第一次添加的view去不清除,这样就不会产生一份重复的数据,
            // 前面已经将ListView中的view添加到了mActiveViews中,虽说这里进行了解绑,但再次添加
            // 的时候是直接将mActiveViews中的添加进去就可以了,所以对性能没什么影响
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
                case LAYOUT_SET_SELECTION:
                    ......
                    break;
                case LAYOUT_SYNC:
                    sel = fillSpecific(mSyncPosition, mSpecificTop);
                    break;
                case LAYOUT_FORCE_BOTTOM:
                    sel = fillUp(mItemCount - 1, childrenBottom);
                    adjustViewsUpOrDown();
                    break;
                case LAYOUT_FORCE_TOP:
                    ......
                    break;
                case LAYOUT_SPECIFIC:
                    ......
                    break;
                case LAYOUT_MOVE_SELECTION:
                    sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                    break;
                default:
                    // 默认情况下都是普通模式LAYOUT_NORMAL,所有会执行到这里,第一次childCount = 0,
                    // 第二次执行到这里的时候ListView中有子View,childCount就不等于0
                    if (childCount == 0) {
                        if (!mStackFromBottom) {
                            final int position = lookForSelectablePosition(0, true);
                            setSelectedPositionInt(position);
                            // 默认布局是从上往下进行布局的,会执行到这里
                            sel = fillFromTop(childrenTop);
                        } else {
                            final int position = lookForSelectablePosition(mItemCount - 1, false);
                            setSelectedPositionInt(position);
                            sel = fillUp(mItemCount - 1, childrenBottom);
                        }
                    } else {
                        if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                            // 有选中的view时会执行这里
                            sel = fillSpecific(mSelectedPosition,
                                    oldSel == null ? childrenTop : oldSel.getTop());
                        } else if (mFirstPosition < mItemCount) {
                            // 首个显示的View的位置小于adapter中数据的条数
                            sel = fillSpecific(mFirstPosition,
                                    oldFirst == null ? childrenTop : oldFirst.getTop());
                        } else {
                            sel = fillSpecific(0, childrenTop);
                        }
                    }
                    break;
            }

            ......
        }
    }

从上面看下来,第一次布局的时候会执行fillFromTop(childrenTop),这里就是对ListView中的子view进行布局,跟着进去瞧一瞧,看看他是如何实现的:

扫描二维码关注公众号,回复: 3387569 查看本文章
    /**
     * 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);
    }

这个方法中没有什么逻辑,就是先判断第一个显示view的位置的合法性,从注释中可以知道,这里的功能是自顶部到底部开始填充,看来具体的实现逻辑要去看看fillDown()这个方法了:

    private View fillDown(int pos, int nextTop) {
        View selectedView = null;

        // 底部距离减去顶部距离就是可以用来填充view的像素值
        int end = (mBottom - mTop);
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            // 如果设置了padding,这里还需要减去padding的距离
            end -= mListPadding.bottom;
        }

        // nextTop是第一个view在屏幕显示的位置,传进来的pos是显示在屏幕上的第一
        // 个view在adapter中的位置,没循环一次,这个值会加1
        while (nextTop < end && pos < mItemCount) {
            // is this the selected item?
            boolean selected = pos == mSelectedPosition;
            // 这里就是创建或获取view
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
            // 计算下一个view在屏幕所处的位置,以确定是否填满了屏幕,填满了屏幕就会跳出这个循环
            nextTop = child.getBottom() + mDividerHeight;
            if (selected) {
                selectedView = child;
            }
            pos++;
        }

        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
        return selectedView;
    }

 在这个循环中,主要的还是获取view,其他的逻辑也是基于这个view的,那这里就去看看makeAndAddView()方法了:

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                                boolean selected) {
        if (!mDataChanged) {
            // 这里是看是否能拿到一个可以复用的view,前面有提到在第二次布局的时候会将第一次的view 
            // 清除掉,这里就是再次去拿清除掉的view
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                // 当有复用的view返回时执行到这这里
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }

        // 当没有复用的view时,就是通过这个方法去创建view
        final View child = obtainView(position, mIsScrap);

        // 在拿到view之后,还没有将view添加到ListView中,那这个方法就是测量view并添加到ListView中去
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

这里获取view有两种方式,一种是拿之前缓存的view,如果之前没有缓存,那么就会重新创建一个view,接下来就去看看obtainView()是如何去创建view的:

    View obtainView(int position, boolean[] outMetadata) {
        ......
        // 下面这两个方法可以说是ListView缓存的主要逻辑了,mRecycler.getScrapView(position)是获取缓存的view,
        // 而mAdapter.getView(position, scrapView, this)这个方法是不是很熟悉,就是我们重新Adapter中getView()方法,
        // 一开始没有缓存时,scrapView返回的是null,所以getView()中传进去的view参数就为null,这是就需要我们创建view了,
        // 而当scrapView返回不为null时,这时传进去的view我们就可以直接复用了,这也就是为什么我们一般在getView()中要
        // 对传进去的view进行判断,如果为null就创建,不为null就直接使用了,
        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // 如果传进去的View没被复用,而是重新创建了view,那么会将传进去的view再次添加进复用池中
                mRecycler.addScrapView(scrapView, position);
            } else if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        // 这个方法中就是给生成的view设置一些参数,这下参数中就包括对view的分类
        setItemViewLayoutParams(child, position);
        ......
        return child;
    }

这个方法中的主要逻辑就是生成view还是复用view,当然setItemViewLayoutParams()这个方法也是挺有意思的,它主要是对view的一些参数进行设置,这里去看下它对view的那些参数进行了设置:

    private void setItemViewLayoutParams(View child, int position) {
        final ViewGroup.LayoutParams vlp = child.getLayoutParams();
        LayoutParams lp;
        if (vlp == null) {
            lp = (LayoutParams) generateDefaultLayoutParams();
        } else if (!checkLayoutParams(vlp)) {
            lp = (LayoutParams) generateLayoutParams(vlp);
        } else {
            lp = (LayoutParams) vlp;
        }

        if (mAdapterHasStableIds) {
            lp.itemId = mAdapter.getItemId(position);
        }
        // 看这里,还记得getItemViewType()这个Adapter中方法么,这个方法返回的就是view属于哪一种类型,
        // 当我们在RecycleBin这个类中对view进行缓存的时候用到的就是view的这个viewType
        lp.viewType = mAdapter.getItemViewType(position);
        lp.isEnabled = mAdapter.isEnabled(position);
        if (lp != vlp) {
            child.setLayoutParams(lp);
        }
    }

这个方法中我们主要看对viewType的赋值,这个值对于view的缓存很重要,它区分生成的view时属于哪一类的(item中有多种布局),这也说明了Adapter中getItemViewType()这个方法的作用了。获取view就分析到这了,接下来让我们返回到makeAndAddView()方法,接下来我们再看下它的setupChild()方法做了些什么东西:

    /**
     * Adds a view as a child and make sure it is measured (if necessary) and
     * positioned properly.
     * 添加一个view作为子view确保它被测量和放置到合适的位置
     */
    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
                            boolean selected, boolean isAttachedToWindow) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

        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 = !isAttachedToWindow || updateChildSelected
                || child.isLayoutRequested();

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

        // Set up view state before attaching the view, since we may need to
        // rely on the jumpDrawablesToCurrentState() call that occurs as part
        // of view attachment.
        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 ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            // 第二次布局时会执行到这里,前面有提到会将第一次添加的view detach掉,
            // 这时只需要将detach的view再次attachViewToParent()就可以了
            attachViewToParent(child, flowDown ? -1 : 0, p);

            // If the view was previously attached for a different position,
            // then manually jump the drawables.
            if (isAttachedToWindow
                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
                    != position) {
                child.jumpDrawablesToCurrentState();
            }
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            // 将view添加进父布局中,这里其实就是添加到ListView中,第一次布局会执行到这里
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
            // add view in layout will reset the RTL properties. We have to re-resolve them
            child.resolveRtlPropertiesIfNeeded();
        }

        if (needToMeasure) {
            final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                    mListPadding.left + mListPadding.right, p.width);
            final int lpHeight = p.height;
            final int childHeightSpec;
            if (lpHeight > 0) {
                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
            } else {
                childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
                        MeasureSpec.UNSPECIFIED);
            }
            // 对子view进行测量
            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;
            // 对子view进行布局
            child.layout(childrenLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childrenLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }

        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
            child.setDrawingCacheEnabled(true);
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

这里可以分为三个步骤,一是将已经生成的view添加进ListView;二是对添加进去的view进行测量;三是对添加进去的view进行布局,这样一次完整的流程就完成了。

猜你喜欢

转载自blog.csdn.net/tangedegushi/article/details/81034237