当前位置: 首页 > 工具软件 > vlayout > 使用案例 >

vlayout原理剖析

芮博厚
2023-12-01

vlayout是阿里的一个开源ui框架,是一个实现多样的item的列表的神器,相信大多数开发者对于vlayout内部的实现原理不太了解。本篇文章就和大家一起探讨下valyout的内部原理:

  用过vlayout的同学都知道他的主要几个关键类,首先是各种layoutHelper的实现类,VirtualLayoutManager,DelegateAdapter

 阅读过RecycleView源码的同学都知道,layoutManager是一个核心的内部类,recycleView把添加到其中的view都是通过   layoutManager进行布局,当用户调用setLayoutmanager方法之后,就会触发requestLayout方法


    /**
     * Set the {@link LayoutManager} that this RecyclerView will use.
     *
     * <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
     * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
     * layout arrangements for child views. These arrangements are controlled by the
     * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
     *
     * <p>Several default strategies are provided for common uses such as lists and grids.</p>
     *
     * @param layout LayoutManager to use
     */
    public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();
        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
        // chance that LayoutManagers will re-use views.
        if (mLayout != null) {
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            mLayout.removeAndRecycleAllViews(mRecycler);
            mLayout.removeAndRecycleScrapInt(mRecycler);
            mRecycler.clear();

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        // this is just a defensive measure for faulty item animators.
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout
                        + " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        mRecycler.updateViewCacheSize();
        requestLayout();
    }

requestLayout的调用势必就会触发recycle的onlayout方法,我们看下onLayout的源码

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Trace.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        Trace.endSection();
        mFirstLayoutComplete = true;
    }

我们可以看到在onlayout方法中又调用了dispatchLaout方法,我们接着往下看

    void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

在dispatchLaout方法,我们只关注一个比较重要的方法,dispatchLayoutStep2,我们看下这个方法

 private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }

我们可以看到在dispatchLayoutStep2方法中的 mLayout.onLayoutChildren(mRecycler, mState);

 mlayout就是我们LayoutManager的一个引用,这里相信大家可以明白,recycleView调用setLayoutManager之后,最终是把view的布局交给了LayoutManager去管理。

对RecycleView的原理有初步了解后,我们就来看vLayout的原理,文章开头,介绍了一个VirtualLayoutManager,这个类是LinearManager的子类,我们用过vlayout的同学应该知道,vlayout是一个线性方向的多样式布局;

其中就是通过这个VirtualLayoutManager关键类来实现各种样式item的管理;

public class VirtualLayoutManager extends ExposeLinearLayoutManagerEx implements LayoutManagerHelper {

class ExposeLinearLayoutManagerEx extends LinearLayoutManager {

我们可以看到virtualLayoutManager是linearlayoutMangaer的间接子类

    通过对recycleview的分析,我们也应该来找一下VirtualLayoutManageronLayoutChildren方法

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Trace.beginSection(TRACE_LAYOUT);
        }

        if (mNoScrolling && state.didStructureChange()) {
            mSpaceMeasured = false;
            mSpaceMeasuring = true;
        }


        runPreLayout(recycler, state);

        try {
            //关键的一步
            super.onLayoutChildren(recycler, state);
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            // MaX_VALUE means invalidate scrolling offset - no scroll
            runPostLayout(recycler, state, Integer.MAX_VALUE); // hack to indicate its an initial layout
        }


        if ((mNestedScrolling || mNoScrolling) && mSpaceMeasuring) {
            // measure required, so do measure
            mSpaceMeasured = true;
            // get last child
            int childCount = getChildCount();
            View lastChild = getChildAt(childCount - 1);
            if (lastChild != null) {
                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) lastChild.getLayoutParams();
                // found the end of last child view
                mMeasuredFullSpace = getDecoratedBottom(lastChild) + params.bottomMargin + computeAlignOffset(lastChild, true, false);

                if (mRecyclerView != null && mNestedScrolling) {
                    ViewParent parent = mRecyclerView.getParent();
                    if (parent instanceof View) {
                        // make sure the fullspace be the min value of measured space and parent's height
                        mMeasuredFullSpace = Math.min(mMeasuredFullSpace, ((View) parent).getMeasuredHeight());
                    }
                }
            } else {
                mSpaceMeasuring = false;
            }
            mSpaceMeasuring = false;
            if (mRecyclerView != null && getItemCount() > 0) {
                // relayout
                mRecyclerView.post(new Runnable() {
                    @Override
                    public void run() {
                        // post relayout
                        if (mRecyclerView != null)
                            mRecyclerView.requestLayout();
                    }
                });
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Trace.endSection();
        }
    }

         大家可以看到我加的注释   //关键的一步
        当VirtualLayoutManageronLayoutChildren方法执行后,他会调用    super.onLayoutChildren(recycler, state)

       实际上就是调用了他的父类ExposeLinearLayoutManagerEx的onLayoutChildren方法

  */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mCurrentPendingSavedState != null && mCurrentPendingSavedState.getInt("AnchorPosition") >= 0) {
            mCurrentPendingScrollPosition = mCurrentPendingSavedState.getInt("AnchorPosition");
        }

        ensureLayoutStateExpose();
        mLayoutState.mRecycle = false;
        // resolve layout direction
        myResolveShouldLayoutReverse();

        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayoutExpose ^ getStackFromEnd();
        // calculate anchor position and coordinate
        updateAnchorInfoForLayoutExpose(state, mAnchorInfo);


        if (DEBUG) {
            Log.d(TAG, "Anchor info:" + mAnchorInfo);
        }

        // LLM may decide to layout items for "extra" pixels to account for scrolling target,
        // caching or predictive animations.
        int extraForStart;
        int extraForEnd;
        final int extra = getExtraLayoutSpace(state);
        boolean before = state.getTargetScrollPosition() < mAnchorInfo.mPosition;
        if (before == mShouldReverseLayoutExpose) {
            extraForEnd = extra;
            extraForStart = 0;
        } else {
            extraForStart = extra;
            extraForEnd = 0;
        }
        extraForStart += mOrientationHelper.getStartAfterPadding();
        extraForEnd += mOrientationHelper.getEndPadding();
        if (state.isPreLayout() && mCurrentPendingScrollPosition != RecyclerView.NO_POSITION &&
                mPendingScrollPositionOffset != INVALID_OFFSET) {
            // if the child is visible and we are going to move it around, we should layout
            // extra items in the opposite direction to make sure new items animate nicely
            // instead of just fading in
            final View existing = findViewByPosition(mCurrentPendingScrollPosition);
            if (existing != null) {
                final int current;
                final int upcomingOffset;
                if (mShouldReverseLayoutExpose) {
                    current = mOrientationHelper.getEndAfterPadding() -
                            mOrientationHelper.getDecoratedEnd(existing);
                    upcomingOffset = current - mPendingScrollPositionOffset;
                } else {
                    current = mOrientationHelper.getDecoratedStart(existing)
                            - mOrientationHelper.getStartAfterPadding();
                    upcomingOffset = mPendingScrollPositionOffset - current;
                }
                if (upcomingOffset > 0) {
                    extraForStart += upcomingOffset;
                } else {
                    extraForEnd -= upcomingOffset;
                }
            }
        }
        int startOffset;
        int endOffset;
        onAnchorReady(state, mAnchorInfo);
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mIsPreLayout = state.isPreLayout();
        mLayoutState.mOnRefresLayout = true;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStartExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            //关键步骤
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEndExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;

在这里调用了最关键的一步,也就是mananger开始真正布局的逻辑,

  fill(recycler, mLayoutState, state, false);大家可以去看fill方法的源码,它里面实际上是调用了

 VirtualLayoutManager的layoutChunk(recycler, state, layoutState, layoutChunkResultCache);方法,我们来看下这个方法

 


    @Override
    protected void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, com.alibaba.android.vlayout.layout.LayoutChunkResult result) {
        final int position = layoutState.mCurrentPosition;
        mTempLayoutStateWrapper.mLayoutState = layoutState;
        LayoutHelper layoutHelper = mHelperFinder == null ? null : mHelperFinder.getLayoutHelper(position);
        if (layoutHelper == null)
            layoutHelper = mDefaultLayoutHelper;
//关键步骤

        layoutHelper.doLayout(recycler, state, mTempLayoutStateWrapper, result, this);


        mTempLayoutStateWrapper.mLayoutState = null;


        // no item consumed
        if (layoutState.mCurrentPosition == position) {
            Log.w(TAG, "layoutHelper[" + layoutHelper.getClass().getSimpleName() + "@" + layoutHelper.toString() + "] consumes no item!");
            // break as no item consumed
            result.mFinished = true;

大家看到熟悉的layoutHelper类,doLayout方法中会调用layoutViews(recycler, state, layoutState, result, helper);

这个方法是一个空实现,他会把具体的实现就给vlayout支持的八大布局的子类去实现,比如

LinearLayoutHelper和GridLayoutHelper等等,我们来看下LinearLayoutHelper的layoutViews方法

 public void layoutViews(RecyclerView.Recycler recycler, RecyclerView.State state,
                            VirtualLayoutManager.LayoutStateWrapper layoutState, LayoutChunkResult result,
                            LayoutManagerHelper helper) {
        // reach the end of this layout
        if (isOutOfRange(layoutState.getCurrentPosition())) {
            return;
        }
        int currentPosition = layoutState.getCurrentPosition();

        // find corresponding layout container
        View view = nextView(recycler, layoutState, helper, result);
        if (view == null) {
            return;
        }

        VirtualLayoutManager.LayoutParams params = (VirtualLayoutManager.LayoutParams) view.getLayoutParams();
        final boolean layoutInVertical = helper.getOrientation() == VERTICAL;

        int startSpace = 0, endSpace = 0, gap = 0;
        boolean isLayoutEnd = layoutState.getLayoutDirection() == VirtualLayoutManager.LayoutStateWrapper.LAYOUT_END;
        boolean isStartLine = isLayoutEnd
                ? currentPosition == getRange().getLower().intValue()
                : currentPosition == getRange().getUpper().intValue();
        boolean isEndLine = isLayoutEnd
                ? currentPosition == getRange().getUpper().intValue()
                : currentPosition == getRange().getLower().intValue();

        if (isStartLine) {
            startSpace = layoutInVertical
                    ? (isLayoutEnd ? mMarginTop + mPaddingTop : mMarginBottom + mPaddingBottom)
                    : (isLayoutEnd ? mMarginLeft + mPaddingLeft : mMarginRight + mPaddingRight);
        }

        if (isEndLine) {
            endSpace = layoutInVertical
                    ? (isLayoutEnd ? mMarginBottom + mPaddingBottom : mMarginTop + mPaddingTop)
                    : (isLayoutEnd ? mMarginRight + mPaddingRight : mMarginLeft + mPaddingLeft);
        }

        if (!isStartLine) {
            gap = mDividerHeight;
        }

        final int widthSize = helper.getContentWidth() - helper.getPaddingLeft() - helper
                .getPaddingRight() - getHorizontalMargin() - getHorizontalPadding();
        int widthSpec = helper.getChildMeasureSpec(widthSize, params.width, !layoutInVertical);
        int heightSpec;
        float viewAspectRatio = params.mAspectRatio;
        if (!Float.isNaN(viewAspectRatio) && viewAspectRatio > 0) {
            heightSpec = View.MeasureSpec.makeMeasureSpec((int) (widthSize / viewAspectRatio + 0.5f),
                    View.MeasureSpec.EXACTLY);
        } else if (!Float.isNaN(mAspectRatio) && mAspectRatio > 0) {
            heightSpec = View.MeasureSpec.makeMeasureSpec((int) (widthSize / mAspectRatio + 0.5),
                    View.MeasureSpec.EXACTLY);
        } else {
            heightSpec = helper.getChildMeasureSpec(
                    helper.getContentHeight() - helper.getPaddingTop() - helper.getPaddingBottom()
                            - getVerticalMargin() - getVerticalPadding(), params.height,
                    layoutInVertical);
        }

        helper.measureChild(view, widthSpec, heightSpec);

        OrientationHelper orientationHelper = helper.getMainOrientationHelper();
        result.mConsumed = orientationHelper.getDecoratedMeasurement(view) + startSpace + endSpace + gap;
        int left, top, right, bottom;
        if (helper.getOrientation() == VERTICAL) {
            // not support RTL now
            if (helper.isDoLayoutRTL()) {
                right = helper.getContentWidth() - helper.getPaddingRight() - mMarginRight - mPaddingRight;
                left = right - orientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = helper.getPaddingLeft() + mMarginLeft + mPaddingLeft;
                right = left + orientationHelper.getDecoratedMeasurementInOther(view);
            }

            // whether this layout pass is layout to start or to end
            if (layoutState.getLayoutDirection() == VirtualLayoutManager.LayoutStateWrapper.LAYOUT_START) {
                // fill start, from bottom to top
                bottom = layoutState.getOffset() - startSpace - (isStartLine ? 0 : mDividerHeight);
                top = bottom - orientationHelper.getDecoratedMeasurement(view);
            } else {
                // fill end, from top to bottom
                top = layoutState.getOffset() + startSpace + (isStartLine ? 0 : mDividerHeight);
                bottom = top + orientationHelper.getDecoratedMeasurement(view);
            }
        } else {
            top = helper.getPaddingTop() + mMarginTop + mPaddingTop;
            bottom = top + orientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.getLayoutDirection() == VirtualLayoutManager.LayoutStateWrapper.LAYOUT_START) {
                // fill left, from right to left
                right = layoutState.getOffset() - startSpace - (isStartLine ? 0 : mDividerHeight);
                left = right - orientationHelper.getDecoratedMeasurement(view);
            } else {
                // fill right, from left to right
                left = layoutState.getOffset() + startSpace + (isStartLine ? 0 : mDividerHeight);
                right = left + orientationHelper.getDecoratedMeasurement(view);
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.

//关键步骤
        layoutChild(view, left, top, right, bottom, helper);

        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + helper.getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }

        handleStateOnResult(result, view);
    }

看到这里相信大家应该能明白,layouthelper和 VirtualLayoutManager之间的关系了

RecycleView把view的事件交互都交给了VirtualLayoutManager去实现,把布局最终交给了layouthelper辅助类去实现。

现在最后还剩下一个DelegateAdapter ,这个类就是一个适配器的管理者,他管理所有的和layouthelper一起传递到DelegateAdapter里面的子Adapter,从而实现数据源和item的绑定,实现数据的刷新。DelegateAdapter的源码逻辑不复杂,这里我就不带大家看了,有兴趣的朋友可以研究下。

 类似资料: