RecyclerView机制解析: Linear Layout

来源:互联网 发布:淘宝账户被冻结了 编辑:程序博客网 时间:2024/04/20 08:09
  1. LayoutManager子类实现的核心有两个地方:

    1. onLayoutChildren,该函数的实现决定了ChildView将会怎样被布局(layout),ChildView的测量也会在其中完成,按照之前分析的RecyclerView Measure流程(http://blog.csdn.net/fyfcauc/article/details/54291174), RecyclerView**自身的Measure也会被onLayoutChildren所影响**。
    2. 对滑动的处理: scrollToPosition/scrollHorizontallyBy/scrollVerticallyBy, 决定了布局如何响应滑动事件
  2. LinearLayoutManager的实现逻辑基本和ListView的布局实现一个思路,RecyclerView的onLayout会触发LayoutManager的onLayoutChildren:

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