美文网首页android菜鸟笔记
RecyclerView解析之LinearLayoutManag

RecyclerView解析之LinearLayoutManag

作者: 李发糕 | 来源:发表于2019-11-07 14:28 被阅读0次

    废话不多说,关于layoutmanager的学习,我们选一个最有代表性的:LinearLayoutManager。

    首先看一下一些内部类。

    AnchorInfo

    锚点信息

    我们看一下他都存储了那些信息

    OrientationHelper mOrientationHelper;
    int mPosition;//position
    int mCoordinate;//坐标
    boolean mLayoutFromEnd;//是否从结尾开始布局
    boolean mValid;//是否合法
    

    然后是构造方法及reset方法

    AnchorInfo() {
        reset();
    }
    
    void reset() {
        mPosition = RecyclerView.NO_POSITION;
        mCoordinate = INVALID_OFFSET;
        mLayoutFromEnd = false;
        mValid = false;
    }
    

    根据padding计算并存储开始坐标:

    void assignCoordinateFromPadding() {
        mCoordinate = mLayoutFromEnd
                ? mOrientationHelper.getEndAfterPadding()
                : mOrientationHelper.getStartAfterPadding();
    }
    

    根据child来存储其坐标和对应的position

    public void assignFromView(View child, int position) {
        if (mLayoutFromEnd) {
            mCoordinate = mOrientationHelper.getDecoratedEnd(child)
                    + mOrientationHelper.getTotalSpaceChange();
        } else {
            mCoordinate = mOrientationHelper.getDecoratedStart(child);
        }
    
        mPosition = position;
    }
    

    验证child成为锚点是否合法

    boolean isViewValidAsAnchor(View child, RecyclerView.State state) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0
                && lp.getViewLayoutPosition() < state.getItemCount();
      //必须未被移除且layoutposition合法
    }
    
    public void assignFromViewAndKeepVisibleRect(View child, int position) {
        final int spaceChange = mOrientationHelper.getTotalSpaceChange();
        if (spaceChange >= 0) {//如果可用空间未变化或者变多了,child的可视区域肯定不会变,直接标记就行了
            assignFromView(child, position);//重新计算
            return;
        }
        mPosition = position;
        if (mLayoutFromEnd) {//如果从end开始排列,和下面类似 下面的逻辑比较清晰,用下面的举例
            ···
            }
        } else {
            final int childStart = mOrientationHelper.getDecoratedStart(child);//获取child坐标
            final int startMargin = childStart - mOrientationHelper.getStartAfterPadding();//计算child到顶部的距离
            mCoordinate = childStart;//设置坐标
            if (startMargin > 0) { // we have room to fix end as well
                 final int estimatedEnd = childStart
                     + mOrientationHelper.getDecoratedMeasurement(child);//获取child底部坐标
                 final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding()
                     - spaceChange;//计算如果可用空间不变,此时底部的为止。(以新的空间的顶点坐标为基准)
                 final int previousEndMargin = previousLayoutEnd
                     - mOrientationHelper.getDecoratedEnd(child);//此时旧的底部剩余空间
                 final int endReference = mOrientationHelper.getEndAfterPadding()
                     - Math.min(0, previousEndMargin);//如果未超出,则为当前的底部。否则为当前底部+超出空间的高度
                 final int endMargin = endReference - estimatedEnd;//
                 if (endMargin < 0) {//如果小于0,说明child可视区域变小的
                     mCoordinate -= Math.min(startMargin, -endMargin);//提高锚点坐标以尽量维持child的可视区域
                 }
             }
        }
    }
    

    上面的注释说的不太清楚,简单的说,如同方法名描述的一样,在计算标记child为锚点的同时,当父布局可用区域变小时,尝试保证child的可视区域尽量不变。

    SavedState

    public static class SavedState implements Parcelable {
    
        int mAnchorPosition;//锚点position
    
        int mAnchorOffset;//锚点偏移
    
        boolean mAnchorLayoutFromEnd;//是否从end开始布局
    
        ···
    
        boolean hasValidAnchor() {//判断锚点是否合法
            return mAnchorPosition >= 0;
        }
    
        void invalidateAnchor() {//设置锚点非法
            mAnchorPosition = RecyclerView.NO_POSITION;
        }
    
        ···
    }
    

    比较简单,用于列表状态的存储和恢复,在onRestoreInstanceState和onSaveInstanceState中调用。这里就不贴代码了,比较简单。

    LayoutState

    用于在LayoutManger填充空白时保持临时状态。布局完成后,它不会保持状态,但是我们仍然保留引用以重复使用同一对象。他的属性基本都是用于状态存储,我们先不看了。简单看一下他的几个方法。

    boolean hasMore(RecyclerView.State state) {
        return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
    }
    

    是否还有更多item,直接用当前pos和数据集总数做比对。

    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
    

    获取要添加的下一个view。如果持有rv的scrapList,则直接从scrapList中获取(此时一般是在执行动画,这一篇先不考虑),否则通过recycler的getViewForPosition方法正常获取。

    layoutState的分析就先到这里。

    到这里,LinearLayoutManager的内部类我们就分析完了。下面步入正题~

    首先是onLayoutChildren。在RecyclerView的dispatchLayoutStep2中就是通过此方法对子view进行布局的。下面我们看一下详细代码。

    onLayoutChildren

    关于原注释我一直不太理解,就不上了。我们直接看分段看代码。

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {//如果当前itemcount==0
                removeAndRecycleAllViews(recycler);//移除并回收所有的view
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {//有需要恢复的状态
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;//设置需要滚动到的position
        }
    
        ensureLayoutState();//确保存在LayoutState,如果没有,new一个
        mLayoutState.mRecycle = false;//禁止回收
        // resolve layout direction
        resolveShouldLayoutReverse();//计算是否需要颠倒绘制。
    
        final View focused = getFocusedChild();//获取目前持有焦点child
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {//如果当前锚点信息非法,不可用或者有需要恢复的存储的SaveState
            mAnchorInfo.reset();//重置锚点信息
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);//更新锚点信息
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {//如果当前页面有view持有焦点且其不在rv的显示区域内
            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));//重新以此view为锚点更新信息并尽量保持其可见范围不变
        }
        ···
    }
    

    首先是一些状态判断及准备工作,然后是对锚点信息的判断及选择更新。关于锚点信息的选定,我们暂时不用关注,有兴趣的可以自己看一下。

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ···
        
        ···//省略滚动相关的代码,我们在这里先不看。
        //计算第一次布局的方向
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;
        if (mAnchorInfo.mLayoutFromEnd) {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
        } else {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        }
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);//通知锚点ready,在GridLayoutManager中重写了此方法,先不关注
        detachAndScrapAttachedViews(recycler);//将所有child detach并通过scrap回收 目前只有这里回调用scrap的方式回收
        mLayoutState.mInfinite = resolveIsInfinite();//是否无穷布局?
        mLayoutState.mIsPreLayout = state.isPreLayout();//是否处于prelayout状态
        if (mAnchorInfo.mLayoutFromEnd) {//如果是从end开始布局
            ···
        } else {//从start开始布局,比较好理解所以我们只看这个就行
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);//使用锚点信息更新layoutState,设置布局方向为end
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);//开始填充布局
            endOffset = mLayoutState.mOffset;//结束偏移
            final int lastElement = mLayoutState.mCurrentPosition;//绘制后的最后一个元素的item
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);//再次使用锚点信息更新layoutState,设置布局方向为start
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);//填充布局
            startOffset = mLayoutState.mOffset;
    
            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }
            ···
    }
    

    上面的代码中可以看出来,将填充布局的工作交给了fill方法。那么为什么要fill两次呢?为什么要变更方向呢?那我们先看看fill方法是如何运行的。

    fill
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;//可用区域
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {//滚动等情况下调用fill
                // TODO ugly bug fix. should not happen
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);//判断是否需要回收,需要的话,回收
            }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;//剩余空间=可用区域+扩展空间。
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
          //循环直到没有剩余空间了或者没有剩余数据了
            layoutChunkResult.resetInternal();//重置layoutChunkResult
            ···
            layoutChunk(recycler, state, layoutState, layoutChunkResult);//添加一个child
            ···
            if (layoutChunkResult.mFinished) {//如果布局结束了,退出循环
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根据所添加的child消费的高度更新偏移
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;//消费剩余空间
            }
    
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);//根据布局状态进行回收
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        ···
        return start - layoutState.mAvailable;//返回本次布局所填充的区域
    }
    

    下面我们一次看一下fill中调用的方法

    layoutChunk
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);//之前看过了,获取当前position所展示需要的viewholder的view
        ···
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);//调用addview添加
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);//addDisappearingView
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);//调用measure进行测量child
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);//记录获取测量后的尺寸为消费尺寸
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {//计算ltrb
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            ···//类似上面
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);//调用child.layout方法进行布局
        
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {//如果此对象被remove或者update了,则需要忽略此次消费
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }
    
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);//回收滚动偏移相关的布局
        } else {
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }
    
    private void addViewInt(View child, int index, boolean disappearing) {
        final ViewHolder holder = getChildViewHolderInt(child);
        if (disappearing || holder.isRemoved()) {
            // these views will be hidden at the end of the layout pass.
            mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
        } else {
            // This may look like unnecessary but may happen if layout manager supports
            // predictive layouts and adapter removed then re-added the same item.
            // In this case, added version will be visible in the post layout (because add is
            // deferred) but RV will still bind it to the same View.
            // So if a View re-appears in post layout pass, remove it from disappearing list.
            mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
        }
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (holder.wasReturnedFromScrap() || holder.isScrap()) {//如果在scrap回收池中
            if (holder.isScrap()) {
                holder.unScrap();//取出
            } else {
                holder.clearReturnedFromScrapFlag();//清除标记
            }
            mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);//添加到parent上
            ···
        } else if (child.getParent() == mRecyclerView) { //在页面上
            // ensure in correct position
            int currentIndex = mChildHelper.indexOfChild(child);//获取当前的index
            if (index == -1) {
                index = mChildHelper.getChildCount();
            }
            if (currentIndex == -1) {
                throw new IllegalStateException("Added View has RecyclerView as parent but"
                        + " view is not a real child. Unfiltered index:"
                        + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
            }
            if (currentIndex != index) {
                mRecyclerView.mLayout.moveView(currentIndex, index);//移动view过去
            }
        } else {
            mChildHelper.addView(child, index, false);//直接添加
            lp.mInsetsDirty = true;
            if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
                mSmoothScroller.onChildAttachedToWindow(child);
            }
        }
        if (lp.mPendingInvalidate) {//如果需要刷新
            if (DEBUG) {
                Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
            }
            holder.itemView.invalidate();//刷新
            lp.mPendingInvalidate = false;
        }
    }
    

    下面我们结合滚动一起看一下。

    当滚动发生时,会触发

    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }
    

    进而调用

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;//布局方向根据滚动方向来
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);//直接调用fill进行填充
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;//计算滚动的距离
        mOrientationHelper.offsetChildren(-scrolled);//offset
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;//记录下本次滚动的距离
        return scrolled;
    }
    

    到这里为止,layoutmanager的layoutchildren和滚动我们就看完了。

    相关文章

      网友评论

        本文标题:RecyclerView解析之LinearLayoutManag

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