LayoutManager子类实现的核心有两个地方:
- onLayoutChildren,该函数的实现决定了ChildView将会怎样被布局(layout),ChildView的测量也会在其中完成,按照之前分析的RecyclerView Measure流程(http://blog.csdn.net/fyfcauc/article/details/54291174), RecyclerView**自身的Measure也会被onLayoutChildren所影响**。
- 对滑动的处理: scrollToPosition/scrollHorizontallyBy/scrollVerticallyBy, 决定了布局如何响应滑动事件。
LinearLayoutManager的实现逻辑基本和ListView的布局实现一个思路,RecyclerView的onLayout会触发LayoutManager的onLayoutChildren:
- 下面的流程分析先忽略PreLayout等附加机制,从一个相对单纯的角度来总结流程。
- 仿照RecyclerView的State方案(http://blog.csdn.net/fyfcauc/article/details/54343635), LayoutManager在布局过程中需要的信息也被集成到LayoutState对象中进行统一的管理和存取, 另外,因为LineatLayoutManager涵盖了竖向和纵向两个方向, 这两种方向的处理逻辑其实基本一致,只有些许的不同,这些细节被封装在了OrientationHelper中,将Top/Bottom(纵向), Left/Right(横向)统一转换为Start/End相关的接口对外提供服务。
- 先将当前所有的ChildView进行detach/recycle全部回收(detachAndScrapAttachedViews),准备从头开始按照当前的状态进行重新布局
- 上一步说的当前状态包含了很多信息,主要有这么几个: 锚点信息(AnchorInfo), 布局方向(LayoutDirection), 这些信息在布局前都要确定:resolveShouldLayoutReverse()/updateAnchorInfoForLayout()以及mPendingScrollPosition/mPendingSavedState都是关于这个过程的
- 锚点决定了ChildView布局的起点,整个布局过程是以锚点为起点向Start和End方向进行填充ChildView的(先填充Start还是end综合mLayoutFromEnd/mShouldReverseLayout得到
- 在锚点就绪后,会回调onAnchorReady为扩展者提供一次修改AnchorInfo的机会
- 正式开始布局,先将当前的ChildView进行回收(detachAndScrapAttachedViews)
- 然后按照先填充Start/End的顺序以Anchor为起点向Start/End**进行ChildView的填充(fill())**
- 在上面的填充完毕后,可能会在界面呈现出gap,需要进行修复: fixLayoutStart/EndGap
- 随后进行layoutForPredictiveAnimations,这一步和PredictiveAnimation有关,先不介绍。
- 如果这次Layout不是PreLayout,那么可以视为Layout完成: 回调mOrientationHelper.onLayoutComplete()来在OrientationHelper中记录当前的一些信息留待下次Layout做参考
- mLastStackFromEnd会记录本次的mStackFromEnd值,为下一次Layout做参考。
- LayoutState: 记录了在布局过程中需要使用的状态信息,并提供了一些功能函数,LayoutState中的某些状态甚至是为某一次View布局而记录的,可能每布局一个View,都会被更新, 挑一些重要的说:
- mOffset: 填充过程中下一个View的layout开始位置
- mAvailable: 在当前layout方向上,应该填充的距离
- mCurrentPosition: 下一个要填充的位置(注意,这个位置不是ViewGroup中的位置,而是Data中的位置)。
- mItemDirection: Data遍历方向(ITEM_DIRECTION_HEAD/ITEM_DIRECTION_TAIL)
- mLayoutDirection: 当前layout方向: LAYOUT_START/LAYOUT_END
- mScrollingOffset: 当LayoutState在滑动状态下被构造时,会被用到,代表在不创建新的ChildView的前提下,最多可以滑动的距离。
- mExtra: 用于PreLayout, 先不介绍。
- mIsPreLayout: 本次Layout是否属于PreLayout, 先不介绍。
- mLastScrollDelta: 上一次滑动的距离。
- hasMore(): 基于当前的mCurrentPosition得出该Position对应的item是否存在于Adapter的Data中。
- next(): 获取下一个要填充位置(也就是mCurrentPosition)对应的View(关键的View提供者), 同时还会基于mItemDirection**更新mCurrentPosition. 其最终会调到Recycler的getViewForPosition根据Position和ViewType从Recycler中获取一个View(可能是缓存复用的,也可能是新建的,onCreateViewHolder/onBindViewHolder都会在这一步被调用)**。
- mPendingSavedState:
- SavedState的出现是为了使用Android的save/restoreInstance机制来保存LinearLayoutManager的一些暂态信息(在这里主要是Anchor的)
- mAnchorLayoutFromEnd: 锚点View应该从下到上开始取还是从上到下开始取。
- mAnchorPosition: 锚点View的Adapter位置(对于Adapter中Data的位置)
- mAnchorOffset: 锚点View此时位置的偏差值(离Start/End有多少像素的距离)
- 作为一个列表,用户在操作过程中可以将列表滑动到任何的位置,这个位置(也就是锚点)是一个暂态信息,如果此时列表所在的Activity被切到后台导致回收,在重建列表时,因为丢失了之前的滑动位置,会影响用户的使用体验。
- 因此才需要借助save/restoreInstance机制来保存这些暂态信息,在重建时可以恢复用户之前的使用情况,提升体验。
- onSaveInstanceState:
- 如果mPendingSavedState已经指向了一个SavedState实例,那么直接基于mPendingSavedState构造一个SavedState返回进行保存。
- 否则,说明当前并没有明确的Anchor,要new一个空白SavedState并根据当前的ChildView分布情况得出合适的mAnchorLayoutFromEnd/mAnchorOffset/mAnchorPosition(*getChildClosestToEnd()/getChildClosestToStart())。*
- onRestoreInstanceState:
- 取出被保存的SavedState并存在mPendingSavedState中
- 发起一次requestLayout来使得SavedState中暂存的信息得以恢复(恢复到销毁前的列表滑动状态)
- 本次Layout结束后(onLayoutCompleted), mPendingSavedState会被设置为null,因为Layout已经完成,mPendingSavedState承载的要求已经被满足,不需要进行暂存了。
- SavedState的出现是为了使用Android的save/restoreInstance机制来保存LinearLayoutManager的一些暂态信息(在这里主要是Anchor的)
- mPendingScrollPosition/mPendingScrollPositionOffset:
- mPendingScrollPosition代表了本次Layout要的目的,即通过发起Layout滑动到mPendingScrollPosition指定的位置,mPendingScrollPosition**Offset**则是在滑动到mPendingScrollPosition位置的基础上增加一定的滑动偏差。
- scrollToPosition/scrollToPositionWithOffset均会改变上面两个值。
- 本次Layout结束后(onLayoutCompleted),mPendingScrollPosition/scrollToPositionWithOffset均会被重置,因为他们已经完成历史使命了,等待下次ScrollTo时被使用。
- Anchor: 锚点是一个AdapterPosition(Item在Adapter中Data的Position),而非RecyclerView中ChildView的ViewGroup Position。
- 在Layout开始前,如果AnchorInfo无效的话/存在有效的mPendingScrollPosition/存在有效的mPendingSavedState,就会重新计算锚点并刷新mAnchorInfo: updateAnchorInfoForLayout
- updateAnchorInfoForLayout有3种计算锚点的策略,优先级从高到低为:
- updateAnchorFromPendingData:从Pending信息(mPendingScrollPosition和mPendingSavedState)中得到Anchor:
- updateAnchorFromChildren: 基于布局方向等信息从当前的ChildView中选择一个ChildView, 得到其对应的AdapterPosition作为锚点。
- 根据mStackFromEnd属性选择第一个或者最后一个Position作为锚点
- 取得了Anchor后,还会求一个特殊的距离 extra: getExtraLayoutSpace
- extra的目的是提升SmoothScroll时用户体验,SmoothScroll(通过state.hasTargetScrollPosition()判断)的过程中布局时,LinearLayoutManager会额外的向滑动方向(这个方向可以由mLastScrollDelta得出)多layout一屏(就是extra标识的距离)的ChildView来提升滑动时的流畅度(有点像预加载)
- fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable): 基于LayoutState/State等信息填充一定距离的ChildView 。
- fill()会试图去填充remainingSpace = layoutState.mAvailable + layoutState.mExtra长度的空间,并在最后返回一个实际被填充的距离(不一定保证能填充完),
- 一开始会调用recycleByLayoutState(recycler, layoutState)回收已经不可见的ChildView(处理Scroll的逻辑也会调到fill函数,因此需要这个流程来回收因为滑动而不可见的ChildView)
- 然后开始循环调用layoutChunk()(每次填充一个View)开始填充,直到已经填充了足够的长度(remainingSpace <= 0)或者已经到了列表的最后,没有Item可以填充了(layoutState.hasMore() == false。
- 每一次layoutChunk()的结果不通过返回值来得到,而是在调用layoutChunk()时填入一个layoutChunkResult,layoutChunk()内部会修改layoutChunkResult,结束后检查layoutChunkResult即可(layoutChunkResult实际来自mLayoutChunkResult,其本质是一个缓存, 避免在频繁的操作中频繁的创建layoutChunkResult对象)。
- 每次调用完layoutChunk(),检查layoutChunkResult.mFinished,如果结束,break循环
- layoutState.mOffset会根据布局的方向来加/减被填充的距离(layoutChunkResult.mConsumed)。
- remainingSpace减去此次被填充的距离: layoutChunkResult.mConsumed,layoutState.mAvailable也会同步减去。
- 下面还有关于mScrollingOffset和stopOnFocusable的逻辑,先不介绍。
- layoutChunk: 每次从LayoutState的next()中取出一个View进行测量布局以填充空间
- layoutState.next获取一个View。如果获取失败(得到的是null), 设置result.mFinished=true然后return。
- 获取View的LayoutParams,调用LayoutManager的addView (注意,不是ViewGroup的addView,LayoutManager的addView会根据View的情况来执行不同的操作)函数将View加入到RecyclerView中
- measureChildWithMargins(view, 0, 0)对View进行测量(注意,如果View之前的Measurement还有效的话,这个函数不会触发View的measure)
- 测量完了就是布局填充,需要根据现在的情况确定View的布局位置:
- 区分VERITICAL和HORIZONTAL布局情况,不过大同小异。
- 区分填充的方向是START还是END,基于layoutState.mOffset和result.mConsumed算出在变化维度的布局位置,对于不变维度的布局,则只需考虑Padding和View本身在该维度的尺寸。
- ItemDecoration会被考虑, 通过getDecoratedMeasurement/getDecoratedMeasurementInOther
- 获得布局位置后,调用layoutDecoratedWithMargins进行布局,至此该View填充完毕。
- 最后会根据params.isItemRemoved() || params.isItemChanged()设置result.mIgnoreConsumed = true, 这个机制主要是为了PredictiveAnimation服务的,先不讨论。
- ChunkResult的mFocusable设置为view.isFocusable()。
- 在填充完View后,此时LayoutState的mOffset(endOffset)保存就是最后一个被填充的View的最外侧位置(在变化维度上的)
- mOrientationHelper.getEndAfterPadding() - endOffset得到的是当前填充的内容离EndPadding位置还有多远,这段空间,理论上讲也应该被填充View,因此这里把这段距离成为gap.
- fixLayoutStartGap/fixLayoutEndGap会被调用来尝试修补gap
- 如果存在gap的话,会调用scrollBy尝试进行修复。
- 如果scroll之后还存在gap, 那么调用mOrientationHelper.offsetChildren(gap)再次尝试进行修复。
- 最终返回还剩余的gap的长度。
- 尽最大可能的修复gap,在修完Start/End后,会继续修复End/Start的,先被修复的方向优先级高.
RecyclerView机制解析: Linear Layout
最新推荐文章于 2024-03-26 17:26:27 发布