美文网首页安卓工具相关
RecyclerView的Measure和Layout

RecyclerView的Measure和Layout

作者: freelifes | 来源:发表于2022-03-19 14:48 被阅读0次

    onMeasure

    recyclerView建议使用自动测量机制。也就是mAutoMeasure = true, 本文就按照自动测量模式研究测量,布局过程。

    protected void onMeasure(int widthSpec, int heightSpec) {
            if (mLayout.isAutoMeasureEnabled()) {
                final int widthMode = View.MeasureSpec.getMode(widthSpec);
                final int heightMode = View.MeasureSpec.getMode(heightSpec);
    
                //1 : 第一次测量 , 测量大小wrap-> min(size,parentSize) match -> parentsize
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    
                //2 :match_parent后续就不执行了。
                final boolean measureSpecModeIsExactly =
                        widthMode == View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY;
                if (measureSpecModeIsExactly || mAdapter == null) {
                    return;
                }
    
                if (mState.mLayoutStep == RecyclerView.State.STEP_START) {
                    dispatchLayoutStep1();
                }
                // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
                // consistency
                //3: 记录old dimension给 pre Layout
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                //4.布局完成,依据child大小,再次计算recycler的大小。最大为parentSize, 小于parentSize则为childSize;
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    
                // if RecyclerView has non-exact width and height and if there is at least one child
                // which also has non-exact width & height, we have to re-measure.
                if (mLayout.shouldMeasureTwice()) {
                    mLayout.setMeasureSpecs(
                            View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(), View.MeasureSpec.EXACTLY),
                            View.MeasureSpec.makeMeasureSpec(getMeasuredHeight(), View.MeasureSpec.EXACTLY));
                    mState.mIsMeasuring = true;
                    dispatchLayoutStep2();
                    // now we can get the width and height from the children.
                    mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
                }
            }
        }
    

    onMeasure过程

    如果widthSpec,heightSpec都为match_parent时候, dispatchLayoutStep1/2都不会执行了,就会再onLayout中执行,此时recyclerView大小为parentSize 。 如果宽高有一个wrap_content,则会执行上述,然后通过child的所需大小,设置recyclerView的大小。
    recycler大小 = Math.min(childNeedSize,parentSize)

    dispatchLayoutStep1做了什么了?

    private void dispatchLayoutStep1() {
            processAdapterUpdatesAndSetAnimationFlags();
            mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
            mState.mInPreLayout = mState.mRunPredictiveAnimations;
    
            if (mState.mRunSimpleAnimations) {
                // Step 0: Find out where all non-removed items are, pre-layout
                int count = mChildHelper.getChildCount();
                for (int i = 0; i < count; ++i) {
                    final RecyclerView.ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                    if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                        continue;
                    }
                    final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator
                            .recordPreLayoutInformation(mState, holder,
                                    RecyclerView.ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                    holder.getUnmodifiedPayloads());
                    mViewInfoStore.addToPreLayout(holder, animationInfo);
                    if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                            && !holder.shouldIgnore() && !holder.isInvalid()) {
                        long key = getChangedHolderKey(holder);
                        mViewInfoStore.addToOldChangeHolders(key, holder);
                    }
                }
            }
            if (mState.mRunPredictiveAnimations) {
                // Step 1: run prelayout: This will use the old positions of items. The layout manager
                // is expected to layout everything, even removed items (though not to add removed
                // items back to the container). This gives the pre-layout position of APPEARING views
                // which come into existence as part of the real layout.
    
                // Save old positions so that LayoutManager can run its mapping logic.
                saveOldPositions();
                final boolean didStructureChange = mState.mStructureChanged;
                mState.mStructureChanged = false;
                // temporarily disable flag because we are asking for previous layout
                mLayout.onLayoutChildren(mRecycler, mState);
                mState.mStructureChanged = didStructureChange;
    
                for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                    final View child = mChildHelper.getChildAt(i);
                    final RecyclerView.ViewHolder viewHolder = getChildViewHolderInt(child);
                    if (viewHolder.shouldIgnore()) {
                        continue;
                    }
                    if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                        int flags = RecyclerView.ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                        boolean wasHidden = viewHolder
                                .hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                        if (!wasHidden) {
                            flags |= RecyclerView.ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                        }
                        final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                                mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                        if (wasHidden) {
                            recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                        } else {
                            mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                        }
                    }
                }
                clearOldPositions();
            } else {
                clearOldPositions();
            }
            mState.mLayoutStep = RecyclerView.State.STEP_LAYOUT;
        }
    
    1、processAdapterUpdatesAndSetAnimationFlags()

    一、执行Adapter的Add,Remove,Move,Update操作,偏移holder的position.和 设置recycler缓存的holder的position。
    如果支持PredictiveAnimations : 会将update和remove这些操作推迟到第二次layout阶段执行。比如remove操作,还没有layout完成,没有展示出来,holder已经被remove了,视觉体验不好。

            mState.mRunSimpleAnimations = mFirstLayoutComplete
                    && mItemAnimator != null
                    && (mDataSetHasChangedAfterLayout
                    || animationTypeSupported
                    || mLayout.mRequestedSimpleAnimations)
                    && (!mDataSetHasChangedAfterLayout
                    || mAdapter.hasStableIds());
            mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                    && animationTypeSupported
                    && !mDataSetHasChangedAfterLayout
                    && predictiveItemAnimationsEnabled();
    

    二、设置动画的Flag
    SimpleAnimations : item改变,faded in/out淡入淡出。
    PredictiveItemAnimations : item的离开和出现,比如add,removed, moved动画
    三、可以看到mRunSimpleAnimations 判断是mFirstLayoutComplete为true,意味着第一次layout已经完成。onLayout调用完成。
    四 、PredictiveAnimations依赖于simpleAnimation的返回值,因此也是OnLayout完成后的动画。mDataSetHasChangedAfterLayout 只有在调用notifyDatasetChanged时候赋值为true.

    2、记录动画信息
    mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    mViewInfoStore.addToPreLayout(holder, animationInfo);
    mViewInfoStore.addToOldChangeHolders(key, holder);
    mLayout.onLayoutChildren(mRecycler, mState);
    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
    

    一、如果itemchange并且simpleAnimale为true,则要记录old viewHolder。
    二、simpleAnimation = true,此时的viewholder的position都是之前数据和old position。记录holder的animationInfo,对象保存着left,top,right,bottom。flag为FLAG_PRE
    三、predictiveAnimatio = true,onLayoutChildren(),如果有新的holder, 记录holder信息,添加flag为 FLAG_APPEAR标识。

    dispatchLayoutStep2做了什么了?

        private void dispatchLayoutStep2() {
            mAdapterHelper.consumeUpdatesInOnePass();
            mState.mInPreLayout = false;
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        }
    

    一、执行adapter的add,remove,update,move操作,计算真正的holder的position位置和缓存position. 通知layoutmanager有add,remove 等操作。
    二、设置prelayout为false,第二次,也是确定最终位置,layoutchildren(),这个方法后续会再详细介绍。主要就是寻找holder的itemview,然后Measure和layout。

    onLayout

        void dispatchLayout() {
            mState.mIsMeasuring = false;
            if (mState.mLayoutStep == RecyclerView.State.STEP_START) {
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                    || mLayout.getHeight() != getHeight()) {
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else {
                mLayout.setExactMeasureSpecsFrom(this);
            }
            dispatchLayoutStep3();
        }
    

    在onLayout阶段,如果之前宽高全是match_parent则dispatchLayoutStep1/2在此阶段执行,并且设置recyclerview的宽高为match_parent。此阶段isMeasureing为false,因为recycler的大小已经知道了。

    dispatchLayoutStep3做了什么?

    执行动画,在dispatchLayoutStep1,布局之前(preLayout)阶段记录holder的ItemInfo。dispatchLayoutStep2真正布局完成。dispatchLayoutStep3 再次记录布局完成holder的itemInfo,执行动画。

     private void dispatchLayoutStep3() {
            mState.mLayoutStep = RecyclerView.State.STEP_START;
            if (mState.mRunSimpleAnimations) {
                // Step 3: Find out where things are now, and process change animations.
                // traverse list in reverse because we may call animateChange in the loop which may
                // remove the target view holder.
                for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                    RecyclerView.ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                    if (holder.shouldIgnore()) {
                        continue;
                    }
                    long key = getChangedHolderKey(holder);
                    final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator
                            .recordPostLayoutInformation(mState, holder);
                    RecyclerView.ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                    if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                        // run a change animation
    
                        // If an Item is CHANGED but the updated version is disappearing, it creates
                        // a conflicting case.
                        // Since a view that is marked as disappearing is likely to be going out of
                        // bounds, we run a change animation. Both views will be cleaned automatically
                        // once their animations finish.
                        // On the other hand, if it is the same view holder instance, we run a
                        // disappearing animation instead because we are not going to rebind the updated
                        // VH unless it is enforced by the layout manager.
                        final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                                oldChangeViewHolder);
                        final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                        if (oldDisappearing && oldChangeViewHolder == holder) {
                            // run disappear animation instead of change
                            mViewInfoStore.addToPostLayout(holder, animationInfo);
                        } else {
                            final RecyclerView.ItemAnimator.ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                                    oldChangeViewHolder);
                            // we add and remove so that any post info is merged.
                            mViewInfoStore.addToPostLayout(holder, animationInfo);
                            RecyclerView.ItemAnimator.ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                            if (preInfo == null) {
                                handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                            } else {
                                animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                        oldDisappearing, newDisappearing);
                            }
                        }
                    } else {
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    }
                }
                mViewInfoStore.process(mViewInfoProcessCallback);
            }
    
            mLayout.removeAndRecycleScrapInt(mRecycler);
            mState.mPreviousLayoutItemCount = mState.mItemCount;
            mDataSetHasChangedAfterLayout = false;
            mDispatchItemsChangedEvent = false;
            mState.mRunSimpleAnimations = false;
    
            mState.mRunPredictiveAnimations = false;
            mLayout.mRequestedSimpleAnimations = false;
            if (mRecycler.mChangedScrap != null) {
                mRecycler.mChangedScrap.clear();
            }
        }
    

    一、如果支持simpleAnimations 则遍历所有holder, 如果这个holder在dispatchLayoutStep1中保存过(必须是flag是update,notremove的),则执行 animateChange动画。否则直接addToPostLayout,记录在在Map<holder,itemInfo>中。
    二、执行动画。依据record.flags
    1、如果holder 在布局之前存在,布局之后也存在,则
    Flag 为 FLAG_PRE_AND_POST 动画为

      callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
    

    2、如果holder在布局之前不存在,布局之后存在或者可见。简单理解新加的holder。 但是之前不存在holder不代表为null。官方解释说,已经被layoutManger加进去了,只是不是recycler的child。

     callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
    

    3、如果holder布局之前存在,布局之后不存在。动画为

    callback.processDisappeared(viewHolder, record.preInfo, null);
    

    4、如果holder布局前后都不存在,但是有出现和消失场景,此时回调如下,没有对应的动画,remove了child,回收child.

    callback.unused(viewHolder);
    

    5、FLAG_APPEAR_PRE_AND_POST holder在布局之前存在,布局之后存在,且有出现动画。这种情况可以理解为holder被加入了recyclerview但是不可见,布局之后可见了,且有出现动画。调用与 2 相同。recyclerview目前没有逻辑可以走到这个case. 因为FLAG_APPEAR加入时,必须不在preLayout中。

     if(!mViewInfoStore.isInPreLayout(viewHolder)){
            if (wasHidden) {
                recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
            } else {  // 添加FLAG_APPEAR
                mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
            }
      }
    

    如果要造这种场景,可以考虑自定义缓存的时候,添加这种Flag。

    notifydata和动画关系

    1、notifyitemChange(index)。
    index 动画---- animateChange() oldviewholder离开动画,添加viewholder动画。本来应该是dispappear + appear + pre + post 动画,但是recycler直接animateChange()动画,没有使用Flag标识。
    非index动画 --- processPersistent().
    2、notifyDataSetChanged() ,view从pool中取,如果数据的viewtype未改变,布局前holder存在,布局后holder也存在,因此调用 processPersistent()。viewtype改变了,则创建新的view, 就执行processAppeared()。
    3、notifyItemRemoved(index)
    index 动画 :processDisappeared()
    新增item:processAppeared()
    其他动画:processPersistent()
    4、notifyItemRangeInserted(index)
    index 动画: processAppeared()
    非index动画:processPersistent ()
    离开屏幕的item: processDisappeared()

    关于notifyItemRemove的IndexOutOfBounds问题

    if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                }
            }
    

    这里是缓存策略里面寻找viewholder, 如果能找到viewholder,则不会执行到这里。
    第一种解决办法:从changeScrap里面获取。

            adapter.mStringList.removeAt(position)
            adapter.notifyItemRemoved(position)
            adapter.notifyItemRangeChanged(position,itemcount);
    

    itemcount的值可以是findlastVisiablecount - position。 也可以mAdapter.getItemcount, rv.getChildCount,这个数字大于findlastVisiablecount - position就可以了,更新只会更新已创建的item。notifyItemRangeChanged不能替换成notifyItemChanged(index), 虽然index位置绕过了crash , 但是其他child不能够匹配,会重新绑定holder,体验不好。还有notifydatasetChange都是性能不好。
    第二种解决办法:从AttachScrap中获取。

        boolean validateViewHolderForOffsetPosition(RecyclerView.ViewHolder holder) {
            if (mAdapter.hasStableIds()) {
                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
            }
            return true;
        }
    

    从AttachScrap获取的holder, 导致旧的holder的itemid 和 remove之后position对应的itemId不一致。因此 adapter中去除getItemId(int position)方法

     setHasStableIds(false);
    

    第三种解决办法:去除动画。

    adapter.mStringList.removeAt(findFirstVisibleItemPosition)
    recyclerView.itemAnimator = null
    adapter.notifyItemRemoved(findFirstVisibleItemPosition)
    

    是因为动画导致了第一次的onlayoutchildren,布局前后的remove导致position不一致。去除动画避免第一次layout。

    总结

    recyclerview 的measure和layout围绕着add,remove,move,update操作,如果存在预定义的动画,onLayoutchildren会执行2次,因此measure和layout分别也会执行2次,第一次预布局,第二次是真正的布局。每次先去除屏幕上的view,再通过缓存策略寻找加入view,然后 measure和layout。
    dispatchLayoutStep1: 布局之前记录动画信息,预layout。
    dispatchLayoutStep2: 真正layout,缓存add和remove。
    dispatchLayoutStep3: 布局之后记录动画信息,执行动画。缓存清理。

    相关文章

      网友评论

        本文标题:RecyclerView的Measure和Layout

        本文链接:https://www.haomeiwen.com/subject/idlarrtx.html