RecyclerView机制解析: Measure

  1. RecyclerView将布局的职责委托给了LayoutManager类,而测量和布局联系很紧密,因此测量的一部分逻辑也被委托给了LayoutManager,RecyclerView大多数情况下会基于LayoutManager布局之后的ChildView分布情况来决定自己的最终尺寸

  2. RecyclerView支持两种measure模式,取决于LayoutManager的mAutoMeasure属性:

    1. AutoMeasure(LinearLayoutManager和GridLayoutManager使用这种模式)
    2. 自定义Measure(StaggerLayoutManager在一定条件下会使用这种模式,不过这次不介绍)
  3. 下面所有的逻辑基于AutoMeasure模式:

  4. Measure的一部分逻辑被委派给了LayoutManager,LayoutManager有自己的onMeasure方法来接收RecyclerView在这次测量中的MeasureSpec,同时LayoutManager还会记录RecycleView的MeasureSpec**供内部布局时使用(主要是为了测量ChildView)**。

  5. 如果RecylcerView本次测量基于的MeasureSpec模式在宽/高上都是EXACTLY, 即ParentView已经给RecyclerView指定了具体的尺寸,这种情况下,RecyclerView就遵照ParentView的指示,将自己的尺寸设置为指定的尺寸。在代码逻辑中的流程是这样的:

    1. RecyclerView在onMeasure中调用LayoutManager的onMeasure方法
    2. LayoutManager onMeasure的默认实现(Support库中的三个子LayoutManager均使用了默认的实现)会调用RecyclerView的defaultOnMeasure函数
    3. defaultOnMeasure会调用LayoutManager.chooseSize基于当前测量约束MeasureSpec,Padding以及MinimumWidth/Height得到一个合适的height/width, chooseSize**在SpecMode是EXACTLY情况下会使用MeasureSpec指定的尺寸。调用**setMeasuredDimension()将得到width/height设置为RecyclerView的尺寸
    4. 流程走完,RecyclerView的最终尺寸就是EXACTLY MeasureSpec所指定的具体尺寸
  6. 如果上面条件没有被满足,那么RecyclerView的最终尺寸必须在布局完成以后才能够确定,因此需要先进行布局: dispatchLayoutStep1/2会被进行(1只会进行一遍,但是2则每次onMeasure都会被进行), 原本应该在onLayout回调中进行的操作,现在因为测量的需求,被提前到了RecyclerView的measure阶段(RecyclerView会在维护State中维护一个mLayoutStep变量标记Layout已经被进行到哪个阶段,避免在onLayout中重复进行),这种用法其实比较常见,尤其对于ParentView的测量依赖于ChildView布局的场景:

    1. LayoutManager先通过自己的setMeasureSpecs()将RecyclerView本次测量的MeasureSpec记录下来供布局时测量ChildView使用

    2. 随后的dispatchLayoutStep2调起LayoutManager的onLayoutChildren开始真正的布局,在布局过程中,一些ChildView需要被测量,其测量约束基于RecyclerView的MeasureSpec

      1. 测量ChildView一般使用LayoutManager的measureChildWithMargins函数,measureChildWithMargins**综合考虑ItemDecorInset, RecyclerView的MeasureSpec,ChildView的Margin,ChildView在LayoutParam中指定的尺寸要求,该维度是否是可以滑动的等因素,使用**getChildMeasureSpec函数获得一个适合ChildView的测量约束MeasureSpec。
      2. getChildMeasureSpec的具体逻辑:
        1. 如果ChildView在LayoutParam中指定了具体的尺寸,那么按照ChildView的意愿来,SpecMode设置为EXACTLY
        2. 否则如果该尺寸所属维度是可以滑动的:
          1. ChildView要求MATCH_PARENT, ChildView要求和RecyclerView一样大。
            1. 如果RecyclerView的MeasureSpec是AT_MOST/EXACTLY, ChildView的测量约束可以和RecyclerView的保持一致,更多的约束也做不了
            2. 如果RecyclerView的MeasureSpec是UNSPECIFIED, RecyclerView没有任何的测量约束,同样也无法对ChildView进行什么有效的约束。SpecMpde设置为UNSPECIFIED
          2. ChildView要求WRAP_CONTENT, ChildView要求能包含自己的内容。
            1. 既然在这个维度(方向)是可以滑动的,那么即使ChildView在这个维度上超过了RecyclerView也没关系,因为可以滑动显示,SpecMode设置为UNSPECIFIED让ChildView自由伸展
        3. 否则这个尺寸所在维度是不能滑动的:
          1. ChildView要求MATCH_PARENT, ChildView要求和RecyclerView一样大。
            1. 只能给ChildView和RecyclerView一样的约束,更多的也做不了。
          2. ChildView要求WRAP_CONTENT, ChildView要求能包含自己的内容。
            1. 因为在这个方向上不能滑动,因此要满足ChildView**最好不要超出RecyclerView(是最好而不是必须,因为我们无法做到这种约束程度)**.
            2. 如果RecyclerView的SpecMode是AT_MOST, 那么ChildView的SpecMode也只能设置为AT_MOST来尽量使ChildView小于RecyclerView
            3. 如果RecyclerView的SpecMode是EXACTLY, 那么ChildView的SpecMode应该设置为AT_MOST来保证ChildView小于RecyclerView
            4. 如果RecyclerView的SpecMode是UNSPECIFIED, RecyclerView在这种情况下提供不了什么约束,那么也不能约束ChildView了,SpecMode设置为UNSPECIFIED
      3. 然后基于生成的MeasureSpec使用shouldMeasureChild来检测有没有必要对ChildView进行测量(因为有ChildView复用的场景,这种情况下,可能不需要进行测量,直接是上一次的就行)
        1. shouldMeasureChild的检测逻辑包含四个条件(满足一个即可):
          1. ChildView的isLayoutRequested()是否返回true, 如果返回true,代表ChildView的尺寸和布局信息是无效的,需要重新测量布局。对于被缓存后复用的ChildView,这个检查是必须的。
          2. mMeasurementCacheEnabled没有开启, 表示不缓存测量结果,每次都必须重新测量,即使上一次的结果现在仍是有效的
          3. !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width):
          4. !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height): 3和4将ChildView现在的尺寸和新尺寸进行对比,如果不等,那么需要重新测量(粗略可以这么说,细节逻辑不赘述)。
      4. 如果ChildView确实需要再次进行Measure,那么调用ChildView的**measure函数开始测量**ChildView。
    3. ChildView被测量和布局后,RecylcerView终于可以根据ChildView的信息来决定自己的最终尺寸了, setMeasuredDimensionFromChildren函数被调用来完成这个任务。

      1. setMeasuredDimensionFromChildren的实现逻辑:
        1. 遍历所有的ChildView,对每个ChildView进行这样的操作:
          1. 获得ChildView经过ItemDecortion装饰过后的位置坐标
          2. 根据上面的坐标来不断的更新一个长方形,这个长方形满足这样的条件: 可以包含所有被装饰过的ChildView, 比如对于一个列表来说,这个长方形最终的形态就是这个列表所占据的空间
        2. 遍历完毕得到了可以正好容纳当前所有可视ChildView的长方形空间坐标(childrenBounds)后,将其传递给LayoutManager的setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)函数
      2. setMeasuredDimension会综合考虑childrenBounds, 测量约束,MinmumWidth/Height得到RecyclerView的最终尺寸。
      3. 得到了最终尺寸后,调用RecyclerView的setMeasuredDimension使尺寸生效。
      4. 上面这种考虑了ChildrenBound的测量方式比ListView之类的要先进,ListView在可拉伸维度被设置为WRAP_CONTENT后,会出现实际只能显示一个ChildView的问题,究其原因是因为测量时只考虑了单个最大的ChildView。而RecylcerView通过ChildrenBound实现了ChildView面积的累加,RecyclerView在可拉伸维度被设置为WRAP_CONTENT后是可以正常工作的
    4. 在上面测量完成后,如果LayoutManager的shouldMeasureTwice返回true,那么会使用上面测量得到的RecyclerView最终尺寸制作一个EXACTLY的MeasureSpec作为测量约束重新进行一次测量(2+3)

你可能感兴趣的:(Android,Android,Layout,Android,FrameWork)