《Android 开发艺术探索》学习笔记之View的工作原理

一、ViewRoot与DecorView

1、ViewRoot
  • ViewRoot对应于ViewRootImpl类
  • 是链接WindowManager和DecorView的纽带
  • View的三大流程均是通过ViewRoot来完成的
    • 在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将其与DecorView建立关联
    • View的绘制流程是从ViewRoot的performTraversals方法开始的
      《Android 开发艺术探索》学习笔记之View的工作原理_第1张图片
2、DecorView
  • 顶级View(是一个ViewGroup)
  • 是一个FrameLayout
  • 一般情况下内部包含一个竖直方向上的LinearLayout,有上下两个部分
    • 上面是标题栏
    • 下面是内容栏(即Activity中的setContentView设置的布局文件
      • 得到Content

        ViewGroup content = findViewById(ViewGroup)findViewById(android.id.content);
      • 得到设置的View

        content.getChildAt(0);

二、理解MeasurSpec

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

1、MeasureSpec
  • Measure代表一个32位的int值。打包两个属性避免过多的对象内存分配,提供了打包和解打包方法
    • 高2位表示SpecMode(测量模式)
    • 低30位表示SpecSize(某种测量模式下的规格大小)
  • SpecMode有三类
    • UNSPECIFIED:父容器不对View有任何限制,要多大给多大。一般用于系统内部测量过程,不需要过多关注
    • EXACTLY:父容器已经检测出View所需要的精确的大小,此时View的最终大小就是SpecSize所指定的值
      • 对应于LayoutParamas中的match_parent和具体的数值这两种模式
    • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。
      • 对应于LayoutParamas中的wrap_content
2、MeasureSpec和LayoutParams的对应关系
  • LayoutParamas和父容器一起决定View的MeasureSpec(源码中还体现了与padding和margin有关)
    • DecorView:窗口尺寸 + 自身的LayoutParamas决定
      • LayoutParamas.MATCH_PARENT:精确模式,大小就是窗口的大小
      • LayoutParamas.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
      • 固定大小:精确模式,大小为LayoutParamas中指定的大小
    • 普通的View:父容器的MeasureSpec + 自身的LayoutParamas决定
      • 当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小。
      • 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是最大模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且不会超过父容器的剩余空间。
      • 当View的宽和高是wrap_content时,不管父容器的模式是精准还是最大化,View也是最大模式并且不会超过父容器的剩余空间。

三、View的工作流程

1、measure过程
  • 确定View的测量宽/高(并不一定是最终宽高)
  • (1)对View来说

    • 由measure方法完成。

      • measure方法是一个final类型的方法,子类不能重写。
      • 方法体中会调用onMeasure方法
        //onMeasure
        
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //此方法中的最小宽高为背景大小和android:minWidth/Height属性二者的最大值,默认为0
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
                                    getDefaultSize(getSuggestedMinimumHeight(), HeightMeasureSpec));
        }
      //getDefaultSize
      
      public static int getDefaultSize(int size, int measureSpec) {
          int result = size;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
      
          switch (specMode) {
              case MeasureSpec.UNSPECIFIED:
                  result = size;
                  break;
              case MeasureSpec.AT_MOST:
              case MeasureSpec.EXACTLY:
                  result = specSize;
                  break;
          }
      
          return result;
      }
    • 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小。
      • 否则wrap_content属性与match_parent效果一致
      • 给setMeasuredDimension方法的参数设置一个默认的宽高来解决wrap_content的问题
  • (2)对ViewGroup来说
    • ViewGroup除了完成自己的measure过程外,还会遍历所有子元素的measure方法
      • 在measureChildren方法中有体现
        • measureChildren方法体内部:创建View[] children,然后循环遍历,调用measureChild方法
          • measureChild方法体内部:取出子元素的LayoutParamas,然后通过getChildMeasureSpec来创建子元素的MeasureSpec,最后将MeasureSpec直接传递给View的measure方法测量
      • ViewGroup是一个抽象类,所以并没有重写View的onMeasure方法
        • 没有定义其具体测量过程的原因是需要ViewGroup的各个子类依据其布局特性去具体实现
  • Measure完成之后,通过getMeasuredWidth/Height方法可以正确的获取到View的测量宽高
    • 有的时候系统需要多次measure才能确定最终的测量宽高
    • 所以最好在onLayout方法中去获取View的测量宽高或最终宽高
如何在Activity已启动的时候获取View的宽高?
  • 法1:Activity/View#onWindowFocusChanged

    • 该方法的含义是:View已经初始化完毕,宽高已准备好并可获取
    • 该方法会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次
      //典型示例
      
      public void onWindowFocusChanged(boolean hasFocus) {
          super.onWindowFocusChanged(hasFocus);
          if (hasFocus) {
              int width = view.getMeasuredWidth();
              int height = view.getMeasuredHeight();
          }
      }
  • 法2:view.post(runnable)

    • 通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也初始化好了
      //典型示例
      
      protected void onStart() {
          super.onStart();
      
          view.post(new Runnable(){
             @Override
             public void run() {
                 int width = view.getMeasuredWidth();
                 int height = view.getMeasuredHeight();
             }
          });
      }
  • 法3:ViewTreeObserver
    • 通过ViewTreeObserver的众多回调接口实现
      • 比如onGlobalLayout:当View树的状态发生改变或者View树内部的View的可见性发生改变时,此方法被回调
  • 法4:view.measure(int widthMeasureSpec, int heightMeasureSpec)
    • 通过手动对View进行measure来得到View的宽高,需要根据View的LayoutParamas来分情况处理
      • match_parent:直接放弃。获取不到parent
      • 具体数值:Measure.makeMeasureSpec()获取数值,然后view.measure()传入该数值绘制
      • wrap_content:Measure.makeMeasureSpec()
2、layout过程
  • 在ViewGroup中,layout的作用是用来确定子元素的位置。
    • 首先通过setFrame方法来设定View的四个顶点的位置
    • 当ViewGroup的位置被确定后,方法体中onLayout会遍历所有的子元素并调用其layout方法确定View在父容器中的位置
    • 在子元素的layout方法中再调用子元素的onlayout方法。如此递推。
  • 与onMeasure方法类似,因为与具体的布局有关,onLayout方法同样在View和ViewGroup中没有具体实现(在其具体子类中有实现)

    //LinearLayout的onLayout()
    
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(1, t, r, b);
        } else {
            layoutHorizontial(1, t, r, b);
        }
    }
  • View的测量宽高与最终宽高的区别:
    • 如过在layout方法中没有修改则认为相等
    • getWidth/Height与getMeasuredWidth/Height的返回值相等
    • 测量宽高形成于measure过程,最终宽高形成于layout过程
3、draw过程
  • 步骤:
    1. 绘制背景 background.draw(canvas)
    2. 绘制自己 onDraw
    3. 绘制children dispatchDraw (实现View绘制的传递)
    4. 绘制装饰 onDrawScrollBars
  • setWillNotDraw(boolean willNotDraw):如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统将会进行相应的优化
    • ViewGroup默认启用此标记位。当明确知道一个ViewGroup需要onDraw来绘制内容时,需要显示地关闭WILL_NOT_DRAW这个标记位
    • View默认不启用此标记位

四、自定义View

1、分类
  • (1)继承View重写onDraw方法
    • 主要用于实现一些不规则的效果
      • 重写onDraw方法,需要自己支持wrap_content,并且padding也需要自己处理
  • (2)继承ViewGroup派生特殊的layout
    • 主要用于自定义布局
      • 需要合适的处理ViewGroup的测量、布局,并处理子View的测量和布局
  • (3)继承特定的View
    • 扩展某种已有View的功能
  • (4)继承特定的ViewGroup(如LinearLayout)
    • 实现某种很像几种View组合在一起的效果
    • 与方法2类似,但方法2更底层
2、构造方法的使用时机
  • (1)一个参数:在代码中直接new一个自定义View实例的时候调用

  • (2)两个参数:在xml布局文件中调用自定义 View的时候调用

  • (3)三个参数:在xml布局文件中调用自定义View,并且自定义View标签中还有自定义属性(主题中优先级最高的属性)时

  • (4)四个参数:一般三个就够用了

    • int defStyleRes:优先级次之的内置于View的style(只有在第三个参数defStyleAttr为0,或者主题中没有找到这个defStyleAttr属性的赋值时,才可以启用)

      • 这个参数不再是Attr了,而是真正的style。其实这也是一种低级别的“默认主题”,即在主题未声明属性值时,我们可以主动的给一个style,使用这个构造函数定义出的View,其主题就是这个定义的defStyleRes(是一种写死的style,因此优先级被调低)
    • int defStyleAttr:主题中优先级最高的属性

  • 属性赋值优先级 Xml定义 > xml的style定义 > defStyleAttr > defStyleRes> theme直接定义

  • 参考博客地址:https://blog.csdn.net/zhao123h/article/details/52210732

  • 示例:使用this连续调用

    /**
     * 第一个构造函数
     */
    public MyCustomView(Context context) {
        this(context, null);
    }
    
    /**
     * 第二个构造函数
     */
    public MyCustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    /**
     * 第三个构造函数
     */
    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO:获取自定义属性
    }
3、注意
  • 直接继承View或ViewGroup的控件,要让View支持wrap_content
  • 直接继承View的控件在draw方法中处理padding,直接继承ViewGroup的控件,在onMeasure和onLayout中处理padding和子元素的margin
  • 尽量不要在View中使用Handler,View内部提供了post方法
  • View中如果有线程或动画,需要及时停止
    • View#onDetachedFromWindow:包含此View的Activity退出或者当前View被remove时调用
    • View#onAttachedToWindow:包含此View的Activity启动时调用
  • 记得处理滑动冲突
4、过程示例
  • 重写三个构造方法并在其中完成初始化工作
    • 参数数量分别为1、2、3
    • 三个参数的构造方法用于调用自定义属性
  • 重写onDraw方法
  • 布局中通过完整的包名+类名使用
  • 通过getPaddingLeft/Right/Top/Bottom获得padding并处理
  • wrap_content需要指定一个默认宽高
5、添加自定义属性
  • (1)在values目录下创建自定义属性的XML
  • (2)在View的三个参数的构造方法中解析并作相应的处理
  • (3)在布局文件中自定义属性
    • 需在布局中添加schemas声明

      xmlns:app=http://schemas.android.com/apk/res-auto

你可能感兴趣的:(Android开发艺术探索)