RecyclerView解析之LinearLayoutManager

废话不多说,关于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和滚动我们就看完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352