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