Android之ListView学习笔记--layout以及view的复用

ListView的一个很需要处理的,且很重要的点,就是如果处理数据的显示操作。一般在一个listView中会有很多数据,如果每个数据对应的view都预先缓存,那估计内存会爆了...所以ListView中采用的是对view进行复用的操作。因为每次展示的只有几个数据,也就是说只会用到几个view,所以ListView的做法就是将view进行复用,每当有新的数据进入屏幕也会伴随着旧的数据移出屏幕,所以只要拿这些“废弃”的view来复用,更新其中要展示的数据就可以了。对这些“废弃”了的view进行存储和复用的就是AbsListView中的RecycleBin类了。

RecycleBin

RecycleBin是ListView中用于对子view进行复用的,大致的流程就是,当我们的界面滚动时,会有一部分view移出屏幕,而同时会有另外的view进入到屏幕,所以我们可以通过对移出屏幕的view进行复用,这样就可以节省重新生成对象的时间,也节省了内存。

RecycleBin中的实例变量主要有这些:

//屏幕中第一个展示的数据的位置
private int mFirstActivePosition;

//在ListView开始进行layout的时候,会用这里面的view,在layout结束之后这些会移到mScrapViews中
//数组大小是屏幕最多能展示的view的数量,从mFirstActivePosition开始的data对应的view存到里面
private View[] mActiveViews = new View[0];
//相当于废弃的view,可以被Adapter进行复用
private ArrayList<View>[] mScrapViews;
//view的类型(ListView中的view可以有多种类型)
private int mViewTypeCount;
//当前使用的ScrapView的list,一般是mScrapView[0]
private ArrayList<View> mCurrentScrap;
//用于存储一些无法recycle的view(可能有特殊处理)
private ArrayList<View> mSkippedScrap;
/**
 * 当废弃的view中的transient state不为空的时候,会将其放入以下两个map中,方便查找和直接复用view
 * 貌似这两个现在还没用到?
 */
//相当于一个map,用position对应的index作为key,view作为value,在getTransientStateView()方法中会利用其寻找对应的view
private SparseArray<View> mTransientStateViews;
//基本同上,只是用position对应的id作为key
private LongSparseArray<View> mTransientStateViewsById;

RecycleBin中的一些方法:

fillAcitveViews

首先一个是fillAcitveViews,这个主要是用于对mActiveView数组进行初始化或者更新,代码如下:

 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 
         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) {
             activeViews[i] = child;
             //记录当前对应的子view的位置
             lp.scrappedFromPosition = firstActivePosition + i;
         }
     }
 }

在ListView中实现了ViewGroup.LayoutParams的子类LayoutParams,在其中添加了子view的type,id,position等信息,这也更加方便我们对ListView中的子view进行记录。

getActiveView

当listView要进行layout的时候,会去用到mActiveView中的view,调用的方法是getActiveView:

View getActiveView(int position) {
    int index = position - mFirstActivePosition;
    final View[] activeViews = mActiveViews;
    if (index >=0 && index < activeViews.length) {
        final View match = activeViews[index];
        //一旦mActiveView中的view被listView使用了,mActiveView就不再存储,防止内存泄露
        activeViews[index] = null;
        return match;
    }
    return null;
}

addScrapView

当listView中的数据发生改变,或者有某些view滑出了屏幕,就需要将这些view移到mScrapView中,具体实现的方法是addScrapView:

void addScrapView(View scrap, int position) {
    final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    //没有Layout数据,不回收
    if (lp == null) {
        return;
    }

    //保存view所在的位置
    lp.scrappedFromPosition = position;

    final int viewType = lp.viewType;
    //如果所在的viewType是不能回收的就放在mSkippedScrap
    if (!shouldRecycleViewType(viewType)) {
        if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
           getSkippedScrap().add(scrap);
        }
        return;
    }

    scrap.dispatchStartTemporaryDetach();
    
    notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
  
    final boolean scrapHasTransientState = scrap.hasTransientState();
    //如果view具有transient State,不直接放入mScrapView中  
    if (scrapHasTransientState) {
        //如果有itemId就存在mTransientStateViewsById中
        if (mAdapter != null && mAdapterHasStableIds) {
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<>();
            }
            mTransientStateViewsById.put(lp.itemId, scrap);
        //如果数据未改变,放入mTransientStateViews中
        } else if (!mDataChanged) {
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<>();
            }
            mTransientStateViews.put(position, scrap);
        } else {
            getSkippedScrap().add(scrap);
        }
    }
    //放入mScrapView中 
    else {
        if (mViewTypeCount == 1) {
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }

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

getScrapView

当Adapter要复用到mScrapViews中的view的时候,就会调用getScrapView方法,代码如下:

View getScrapView(int position) {
    //找到对应位置的view的type
    final int whichScrap = mAdapter.getItemViewType(position);
    if (whichScrap < 0) {
        return null;
    }
    //总共就一种type
    if (mViewTypeCount == 1) {
        return retrieveFromScrap(mCurrentScrap, position);
    }
    //找到数组中对应的type的list中的view 
    else if (whichScrap < mScrapViews.length) {
        return retrieveFromScrap(mScrapViews[whichScrap], position);
    }
    return null;
}

这里主要是调用了retrieveFromScrap方法来返回最终获得的view,该方法主要是从scrapView中找到一个view用于adapter的复用。看一下代码:

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // 判断我们是否有与该position或者id相同的view,有的话就直接复用
        for (int i = 0; i < size; i++) {
            final View view = scrapViews.get(i);
            final AbsListView.LayoutParams params =(AbsListView.LayoutParams) view.getLayoutParams();
            //判断id是否相同
            if (mAdapterHasStableIds) {
                final long id = mAdapter.getItemId(position);
                if (id == params.itemId) {
                    return scrapViews.remove(i);
                }
            }
            //判断position是否相同
            else if (params.scrappedFromPosition == position) {
                final View scrap = scrapViews.remove(i);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            }
        }
        //如果没有就随意弹出一个返回
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}

除了以上的方法之外,还有一些方法的功能就罗列在下面了

  • markChildrenDirty():让所有List里面存储的view在下一次使用之前必须重新layout
  • clear():清除掉所有的list中的数据
  • getTransientStateView():获取mTransientStateViews和mTransientStateViewsById中的view(如果有)
  • scrapActiveViews():将mActiveViews里面没有被layout用到的view放到mScrapViews中,如果有TransientState就放到mTransientStateViews或mTransientStateViewsById中

Layout

上面大概讲了RecycleBin中的一些方法,接下来看一下ListView中是怎么使用到RecycleBin的。ListView中最主要的使用是在onLayout中,代码在AbsListView中:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        mInLayout = true;

        final int childCount = getChildCount();
        //如果数据改变,则所有的view在使用前都需要重新layout
        if (changed) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }

        //在子类中需要重写的方法
        layoutChildren();
        mInLayout = false;

        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

        // TODO: Move somewhere sane. This doesn't belong in onLayout().
        if (mFastScroll != null) {
            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
        }
    }

可以看到主要的实现是在layoutChildren()方法来实现的,这也是在子类中根据需要来实现,ListView中也实现了相关代码,不过代码量有点大,就只拿跟RecycleBin相关的来看吧。

    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (blockLayoutRequests) {
            return;
        }

        mBlockLayoutRequests = true;

        try {
            super.layoutChildren();

            invalidate();

            if (mAdapter == null) {
                resetList();
                invokeOnItemScrollListener();
                return;
            }
            
            //后面要用到的一些参数
            final int childrenTop = mListPadding.top;
            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
            final int childCount = getChildCount();

            int index = 0;
            int delta = 0;

            View sel;
            View oldSel = null;
            //之前显示的第一个view
            View oldFirst = null;
            View newSel = null;

            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                index = mNextSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    newSel = getChildAt(index);
                }
                break;
            case LAYOUT_FORCE_TOP:
            case LAYOUT_FORCE_BOTTOM:
            case LAYOUT_SPECIFIC:
            case LAYOUT_SYNC:
                break;
            case LAYOUT_MOVE_SELECTION:
            //对相应数据进行更新
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    oldSel = getChildAt(index);
                }

                // Remember the previous first child
                oldFirst = getChildAt(0);

                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition;
                }

                // Caution: newSel might be null
                newSel = getChildAt(index + delta);
            }


            boolean dataChanged = mDataChanged;
            //如果ListView的数据改变需要进行的操作,主要是对一些参数进行更新
            if (dataChanged) {
                handleDataChanged();
            }
            
            //省略部分代码
            ...

            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            //如果数据改变,原来的view不能直接使用,放到mScrapView中等待被复用
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            }
            //将ListView需要的view放到mActiveView中
            else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            //将原来的view都detach
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            //根据不同的mode对ListView进行相应的view填充
            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                final int selectedPosition = reconcileSelectedPosition();
                sel = fillSpecific(selectedPosition, mSpecificTop);
                /**
                 * When ListView is resized, FocusSelector requests an async selection for the
                 * previously focused item to make sure it is still visible. If the item is not
                 * selectable, it won't regain focus so instead we call FocusSelector
                 * to directly request focus on the view after it is visible.
                 */
                if (sel == null && mFocusSelector != null) {
                    final Runnable focusRunnable = mFocusSelector
                            .setupFocusIfValid(selectedPosition);
                    if (focusRunnable != null) {
                        post(focusRunnable);
                    }
                }
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                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) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }

            // 将mActiveView中没有被使用的view放回到mScrapView中
            recycleBin.scrapActiveViews();

            // remove any header/footer that has been temp detached and not re-attached
            removeUnusedFixedViews(mHeaderViewInfos);
            removeUnusedFixedViews(mFooterViewInfos);

            //省略部分代码
            ...

        } finally {
            if (mFocusSelector != null) {
                mFocusSelector.onLayoutComplete();
            }
            if (!blockLayoutRequests) {
                mBlockLayoutRequests = false;
            }
        }
    }

在layoutChildren中,先是判断数据是否改变,如果数据改变,则所有的view在重新被使用之前,都需要重新layout;反之,原来ListView中的view可以直接复用,将其放在mActiveView中等待使用。接下来对ListView进行填充操作,最后将mActiveView中没有用到的view放到mScrapView中。代码中根据不同的mode会选择相应的fillXXX方法,看了一下这些代码,最终往ListView中添加view调用的都是makeAndAddView方法:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
    if (!mDataChanged) {
        // 直接使用一个已有的view
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

    // 生成一个新的view,或者找到一个可以复用的view
    final View child = obtainView(position, mIsScrap);

    // 需要重新measure
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

在该方法中,先看一下能不能直接使用原来的view,如果不行的话就复用mScrapViews中的或者重新生成一个view,后面一步的操作是在obtainView中,代码是在AbsListView中实现:

View obtainView(int position, boolean[] outMetadata) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
    //view之前是否已经用过,主要用于后续是否需要重新measure之类的
    outMetadata[0] = false;
    
    //判断是否能根据transient state来获取view
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        //类型不变
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            //rebind数据失败,将updatedView放到mScrapView中(因为不会用到)
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        outMetadata[0] = true;

        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    //从mScrapView中选择一个view,如果选择不到就返回null
    final View scrapView = mRecycler.getScrapView(position);
    //判断能够rebind数据
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        //rebind数据失败,将其重新放回mScrapView中
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;
            child.dispatchFinishTemporaryDetach();
        }
    }

    //对view的各种设置
    if (mCacheColorHint != 0) {
        child.setDrawingCacheBackgroundColor(mCacheColorHint);
    }

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

    setItemViewLayoutParams(child, position);
    if (AccessibilityManager.getInstance(mContext).isEnabled()) {
        if (mAccessibilityDelegate == null) {
            mAccessibilityDelegate = new ListItemAccessibilityDelegate();
        }
        if (child.getAccessibilityDelegate() == null) {
            child.setAccessibilityDelegate(mAccessibilityDelegate);
        }
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    return child;
}

在代码中判断RecycleBin中的存储的view能否被复用,或者需要重新生成view,同时调用了Adapter中的getView方法来判断能够重新绑定数据。最终会返回一个reuse的或者是新的view。一般Adapter需要我们自己重写,getView方法也是我们自己实现的,一般在实现的过程中需要判断view是否为null,如果不为null,我们应该对其进行复用。这样可以节省空间。

在makeAndAddView方法中,一旦我们获取到一个view之后,接下来要进行的操作就是setChild,看一下该代码:

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();
    //判断是否要measure,用的就是obtainView中的outMetadata[0]
    //如果view是重新inflate的,就需要重新measure
    //或者view之前的selected状态和当前需要的selected状态不一样也需要重新measured
    final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
                || child.isLayoutRequested();

    //尽量复用原来的layoutParams
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    }
    p.viewType = mAdapter.getItemViewType(position);
    p.isEnabled = mAdapter.isEnabled(position);

    //各种状态设置
    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重新attached
        attachViewToParent(child, flowDown ? -1 : 0, p);

        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;
        }
        // 在layout过程中将这个view添加进来
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
        // add view in layout will reset the RTL properties. We have to re-resolve them
        child.resolveRtlPropertiesIfNeeded();
    }

    //对child view进行measure操作
    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);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }

    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;

    //如果是新inflate的view,需要进行layout操作,确定位置
    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    }
    //如果是reuse的view,可以重新设置一下其对应的布局位置就可以
    else {
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }
    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
        child.setDrawingCacheEnabled(true)
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

到这里,我们整个listView的layout过程就基本OK了,其中对RecycleBin的使用也大致讲了一下,总体的流程大致是这样的:

  1. onLayout:判断数据是否改变,如果改变,则listView中的view和RecycleBin中的view在使用之前都要重新layout,调用layoutChildren方法
  2. layoutChildren:先将原来listView中的数据放入mActiveView或者mScrapView中,并将其detach,根据我们定义的layout的mode对ListView进行填充操作,调用相应的fillXXX方法。
  3. fillXXX:用循环来判断是否需要往ListView里面添加view(是否可见),如果可以添加,就调用makeAndAddView方法进行添加。
  4. makeAndAddView:从RecycleBin的mActiveViews中或mScrapViews获取view,如果获取不到就inflate一个view,对得到的view调用setupChild方法添加到ListView中。
  5. setupChild:如果view是复用的,则重新attach并调整相应的layout参数,如果是新生成的,则将其添加到ListView中,并进行measure,layout等操作。

 

有些地方还没有看的比较深入,先大概这么写一下流程吧,以后有新的理解的话再来更新。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值