4 View的工作原理

主要内容

自定义View、View的底层工作原理,比如View的测量流程、布局流程、绘制流程。

4.1 初识ViewRoot和DecorView

  1. ViewRoot的实现是ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程(mearsure、layout、draw)均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立连接。

     root = new ViewRootImpl(view.getContext(),display);
     root.setView(view,wparams, panelParentView);
    
  2. View的三大流程:
    1. measure用来测量View的宽高
    2. layout来确定View在父容器中的位置
    3. draw负责将View绘制在屏幕上
  3. View的绘制流程从ViewRoot的performTraversals方法开始:
    1. performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。
    2. 其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。 performTraversals
  4. Measure完成后, getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。
  5. Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTopgetBottongetLeftgetRight拿到View的四个定点坐标。
  6. DecorView作为顶级View,其实是一个FrameLayout,它包含一个竖直方向的LinearLayout,这个LinearLayout分为标题栏和内容栏两个部分。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是R.android.id.content,通过ViewGroup content = findViewById(R.android.id.content);可以得到这个contentView。View层的事件都是先经过DecorView,然后才传递到子View。 DecorView

4. 理解MeasureSpec

测量过程,系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。

4.2.1 MeasureSpec

  1. MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某个测量模式下的规格大小)。
  2. SpecMode有三种:
    1. UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
    2. EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式
    3. AT_MOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应LayoutParams中的wrap_content

4.2.2 MeasureSpec和LayoutParams的对应关系

View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
View的measure过程由ViewGroup传递而来,参考ViewGroup的measureChildWithMargins方法,通过调用子元素的getChildMeasureSpec方法来得到子元素的MeasureSpec,再调用子元素的measure方法。书中根据源码总结出以下View的MeasureSpec创建规则: View的MeasureSpec创建规则 稍有Android开发经验的人应该都能感受到这种规则:

  1. 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
  2. 当View的宽/高是match_parent时,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空间。
  3. 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
  4. 父容器的UNSPECIFIED模式,一般用于系统内部多次Measure时,表示一种测量的状态,一般来说我们不需要关注此模式。

questions:
UNSPECIFIED模式在系统内部多次Measure情况下被使用,具体是什么时候?表现又是什么?是在View的测量过程中发生的吗?书中没有说清楚,但是作为开发者,的确是没有接触到UNSPECIFED的情况。

4.3 View的工作流程

4.3.1 measure过程

分两种情况:

  1. View通过measure方法就完成了测量过程
  2. ViewGroup除了完成自己的测量过程还会便利调用所有子View的measure方法,而且各个子View还会递归执行这个过程。

1 View的measure过程

直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content(即specMode是AT_MOST模式)时的自身大小,否则在布局中使用wrap_content相当于使用match_parent。对于非wrap_content的情形,我们沿用系统的测量值即可。

2 ViewGroup的measure过程

ViewGroup是一个抽象类,没有重写View的onMeasure方法,但是它提供了一个measureChildren方法。这是因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,比如LinearLayoutRelativeLayout,因此ViewGroup没办法同一实现onMeasure方法。 measureChildren方法的流程:

  1. 取出子View的LayoutParams
  2. 通过getChildMeasureSpec方法来创建子元素的MeasureSpec
  3. MeasureSpec直接传递给View的measure方法来进行测量

文章通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程。

  1. LinearLayout在布局中如果使用match_parent或者具体数值,测量过程就和View一直,即高度为specSize
  2. LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度总和,并且不超过它的父容器的剩余空间。
  3. 当然LinearLayout的的最终高度同时也把竖直方向的padding考虑在内

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。同时View的measure过程和Activity的生命周期并不是同步执行,因此无法保证在Activity的onCreate``onStart``onResume时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:

  1. Activity/View#onWindowFocusChanged。 onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
  2. view.post(runnable)。 通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
  3. ViewTreeObserver。 使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout方法会被回调。需要注意的是,伴随着View树状态的改变,onGlobalLayout会被回调多次。
  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)。

    1. match_parent: 无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
    2. 具体的数值(dp/px):

       int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
       int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
       view.measure(widthMeasureSpec,heightMeasureSpec);
      
    3. wrap_content:

       int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
       //View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
       int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
       view.measure(widthMeasureSpec,heightMeasureSpec);
      

4.3.2 layout过程

layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在layout方法中,onLayout方法又会被调用。
layout方法确定View本身的位置,源码流程如下: n

  1. setFrame确定View的四个顶点位置,即确定了View在父容器中的位置
  2. 调用onLayout方法,确定所有子View的位置,View和ViewGroup均没有真正实现onLayout方法。

文章通过LinearLayout的onLayout方法里来分析ViewGroup的onLayout过程。

  1. 遍历所有子View并调用setChildFrame方法来为子元素指定对应的位置
  2. setChildFrame方法实际上调用了子View的layout方法,形成了递归

4.3.3 draw过程

View的绘制过程遵循如下几步:

  1. 绘制背景drawBackground(canvas)
  2. 绘制自己onDraw
  3. 绘制childrendispatchDraw遍历所有子View的draw方法
  4. 绘制装饰onDrawScrollBars

tips:: ViewGroup会默认启用setWillNotDraw为ture,导致系统不会去执行onDraw,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭WILL_NOT_DRAW这个标记位,即调用setWillNotDraw(false);

4.4 自定义View

4.4.1 自定义View的分类

  1. 继承View 通过onDraw方法来实现一些效果,需要自己支持wrap_content,并且padding也要去进行处理
  2. 继承ViewGroup 实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程
  3. 继承特定的View子类(如TextView、Button) 扩展某种已有的控件的功能,且不需要自己去管理wrap_content和padding
  4. 继承特定的ViewGroup子类(如LinearLayout)

4.4.2 自定义View须知

  1. 直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。指定wrap_content模式下的默认宽/高
  2. 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
  3. 尽量不啊哟在View中使用Handler,因为没必要。View内部提供了post系列的方法,完全可以替代Handler的作用。
  4. View中有线程和动画,需要在View的onDetachedFromWindow中停止。
  5. View带有滑动嵌套情形时,需要处理好滑动冲突

4.4.3 自定义View的思想

  1. 掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等
  2. 面对新的自定义View时,对其分类并选择合适的实现思路。

results matching ""

    No results matching ""