Android开发Android开发经验谈自定义view相关

Android View的工作原理

2018-10-15  本文已影响61人  4e70992f13e7

ViewRoot和DecorView

1、ViewRoot是什么?

  1. ViewRoot对应于ViewRootImpl类
  2. 是连接WindowManager和DecorView的纽带
  3. 发起并完成View的三大流程(测量、布局、绘制)
  4. ViewRoot需要和DecorView建立联系。

2、ViewRoot如何完成View的三大流程?

WechatIMG125.jpeg
  1. ViewRoot的performTraversals()开始View的绘制流程,依次调用performMeasure()、performLayout()和performDraw()
  2. performMeasure()最终执行父容器的measure()方法,并依此执行所有子View的measure方法。
  3. performLayout()和performDraw()同理

3、View三大流程的作用

  1. measure决定了View的宽/高,测量后可以通过getMeasuredWidth/Height来获得View测量后的宽/高,除特殊情况外该值等于View最终的宽/高
  2. layout决定了View的顶点坐标以及实际View的宽/高:完成后可以通过getTop/Bottom/Left/Right获取顶点坐标,并通过getWidth/Height()获得View的最终宽/高
  3. draw决定了View的显示,最终将View显示出来
    MeasuredWidth/height != getWidth/Height()的场景:更改View的布局参数并进行重新布局后,就会导致测量 != 实际值

4、DecorView的作用

  1. DecorView是顶级View,本质就是一个FrameLayout
  2. 包含了两个部分,标题栏和内容栏
  3. 内容栏id是content,也就是activity中setContentView所设置的部分,最终将布局添加到id为content的FrameLayout中
  4. 获取content:ViewGroup content = findViewById(R.android.id.content)
  5. 获取设置的View:content.getChidlAt(0)

5、ViewRootIml如何和DecorView建立联系

  1. Activity对象在ActivityThread中创建完毕后,会将DecorView添加到Window中
  2. 同时会创建ViewRootImpl,调用ViewRoot的setView方法将ViewRootImpl和DevorView建立关联
root = new ViewRootImpl(view.getContext(), display)
root.setView(view, wparams, panelParentView)

6、ViewRoot为什么要和DecorView建立关联

  1. DecorView等View的三大流程需要通过ViewRoot完成

MeasureSpec

1、MeasureSpec是什么?

  1. MeasureSpec是一种“测量规则”或者“测量说明书”,决定了View的测量过程
  2. View的MeasureSpec会根据自身的LayoutParamse和父容器的MeasureSpec生成。
  3. 最终根据View的MeasureSpec测量出View的宽/高(测量时数据并非最终宽高)

2、MeasureSpec要点解析

  1. MeasureSpec代表一个32位int值,高2位是SpecMode,低30位是SpecSize
  2. SpecMode是指测量模式
  3. SpecSize是指在某种测量模式下的大小
  4. 类MesaureSpec提供了用于SpecMode和SpecSize打包和解包的方法

3、测量模式SpecMode的类型

  • UNSPECIFIED:父容器不对View有任何限制,一般用于系统内部
  • EXACTLY:精准模式,View的最终大小就是SpecSize指定的值(对应于LayoutParams的match_parent和具体的数值)
  • AT_MOST:最大值模式,大小不能大于父容器指定的值SpecSize(对应于wrap_content)

4、MeasureSpec和LayoutParams的对应关系

  1. View的MeasureSpec是需要通过自身的LayoutParams和父容器的MeasureSpec一起才能决定
  2. DecorView(顶级View)是例外,其本身MeasureSpec由窗口尺寸和自身LayoutParams共同决定
  3. MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高

5、普通View的Measure的创建规则

  1. View本身布局参数为具体dp/px数值,模式:EXACTLY,尺寸:自身尺寸(不管父容器的MeasureSpec)
  2. View为match_parent, 模式:EXACTLY/AT_MOST由父容器MeasureSpec决定,尺寸:父容器目前可用大小
  3. View为wrap_content,模式:AT_MOST,尺寸:父容器可用尺寸(不能超过该尺寸)
  4. 当父容器为UNSPECIFIED时,View为具体数值时规则不变;其余match_parent/wrap_content,模式均为:UNSPECIFIED,尺寸:0
  5. UNSPECIFIED一般用于系统内部多次measure的情况,不需要关注该模式。
WechatIMG126.jpeg

View的工作流程

  1. measure:测量——确定View的测量宽/高
  2. layout:布局——确定View的最终宽/高和四个顶点的位置
  3. draw:绘制——将View绘制到屏幕上

1、Measure 过程

1、View的measure过程及要点
  1. View的measure方法是final类型方法——表明该方法无法被重载
  2. View的measure方法会调用onMeasure方法,onMeasure会调用setMeasuredDimension方法设置View宽/高的测量值
2、View的onMeasure源码要点
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //1. setMeasuredDimension方法设置View宽/高的测量值
        setMeasuredDimension(
                //2. 第一个参数是获得的测量宽/高(通过getDefaultSize获取)
                getDefaultSize(getSuggestedMinimumWidth(),  //3. 获取的建议最小的宽/高
                                    widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(),
                                    heightMeasureSpec));
}
  1. setMeasuredDimension方法设置View宽/高的测量值(测量值通过getDefaultSize获取)
  2. getDefaultSize用于获取View的测量宽/高
3、View的getDefaultSize源码要点(决定了View宽高的测量值)
//1. 获取View宽和高的测量值
  public static int getDefaultSize(int size, int measureSpec) {
      int result = size;
      int specMode = MeasureSpec.getMode(measureSpec);
      int specSize = MeasureSpec.getSize(measureSpec);

      switch (specMode) {
      //2. UNSPECIFIED模式时,宽/高为第一个参数也就是getSuggestedMinimumWidth()获取的建议最小值
      case MeasureSpec.UNSPECIFIED:
          result = size;
          break;
      //3. AT_MOST(wrap_content)和EXACTLY(match_parent/具体值dp等)这两个模式下,View宽高的测量值为当前View的MeasureSpec(测量规格)中指定的尺寸specsize
      case MeasureSpec.AT_MOST:
      case MeasureSpec.EXACTLY:
          result = specSize;
          break;
      }
      return result;
  }
4、View的getSuggestedMinimumWidth/Height()源码要点
//获取建议的最小宽度
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
  1. 如果View没有背景,View的最小宽度就为android:minWidth这个参数指定的值(mMinWidth),没有指定则默认为0
  2. 如果View有背景,会从mMinWidth和背景的最小宽度中取最大值。
  3. 背景的最小宽度(getMinimumWidth())本质就是Drawable的原始宽度(ShapeDrawable无原始宽度,BitmapDrawable有原始宽度——图片的尺寸)
5、View的wrap_content和match_parent效果一致的原因分析
  1. 根据View的onMeasure方法中的getDefaultSize方法,我们可以发现在两种模式下,View的测量值等于该View的测量规格MeasureSpec中的尺寸。
  2. View的MeasureSpec本质是由自身的LayoutParams和父容器的MeasureSpec决定的。
  3. 当View为wrap_content时,该View的模式为AT_MOST,且尺寸specSize为父容器的剩余空间大小。
  4. 当View为match_parent时,该View的模式跟随父容器的模式(AT_MOST/EXACTLY), 且尺寸specSize为父容器的剩余空间大小。
  5. 因此getDefaultSize中无论View是哪种模式,最终测量宽/高均等于尺寸specSize,因此两种属性效果是完全一样的(View的大小充满了父容器的剩余空间)
  6. 除非给定View固定的宽/高,View的specSize才会等于该固定值。
6、自定义View需要重写onMeasure方法,并写明两种模式的处理方法
//1. 重写onMeasure,特殊处理wrap_content的情况
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);

        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //2. 均为wrap_content时, 将值设置为android:minWidth/Height属性指定的值
            setMeasuredDimension(mWidth, mHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            //3. 哪个为wrap_content哪个就用android:minXXX属性给定的最小值
            setMeasuredDimension(mWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }
7、ViewGroup(抽象类)的measure流程
  1. ViewGroup没有onMeasure方法,只定义了measureChildren方法(onMeasure根据不同布局难以统一)
  2. measureChildren中遍历所有子元素并调用measureChild方法
  3. measureChild方法中会获取子View的MeasureSpec,然后调用子元素View的measure方法进行测量
8、getChildMeasureSpec获取子元素MeasureSpec的要点
  1. 子View的MeasureSpec是根据自身的LayoutParams和父容器SpecMode生成
  2. 当子View的布局参数为wrap_content,且父容器模式为AT_MOST时,效果与子元素布局为match_parent是一样的。因此当子View的布局参数为wrap_content时,需要指定默认的宽/高
9、LinearLayout的onMeasure()分析
  1. ViewGroup因为布局的不同,无法统一onMeasure方法,具体内容根据布局的不同而不同,这里直接以LinearLayout进行分析
  2. onMeasure会根据orientation选择measureVertical或者measureHorizontal进行测量
  3. measureVertical本质是遍历子元素,并执行子元素的measure方法,并获得子元素的总高度以及子元素在竖直方向上的margin等。
  4. 最终LinearLayout会测量自己的大小,在orientation的方向上,如果布局是match_parent或者具体数值,测量过程与View一致(高度为specSize);如果布局是wrap_content,高度是所有子元素高度总和,且不会超过父容器的剩余空间,最终高度需要考虑在竖直方向上的padding
10、如何获取View的测量宽/高
  1. 在measure完成后,可以通过getMeasuredWidth/Height()方法,就能获得View的测量宽高
  2. 在一定极端情况下,系统需要多次measure,因此得到的值可能不准确,最好的办法是在onLayout方法中获得测量宽/高或者最终宽/高
11、如何在Activity启动时获得View的宽/高
  1. Activity的生命周期与View的measure不是同步运行,因此在onCreate/onStart/onResume均无法正确得到
  2. 若在View没有测量好时,去获得宽高,会导致最终结果为0
  3. 有四种办法去正确获得宽高
A. onWindowFocusChanged获得View的宽/高
//1. View已经初始化完毕,可以获得宽高
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
//2. Activity得到焦点和失去焦点均会调用一次(频繁onResume和onPause会导致频繁调用)
        if(hasFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
B. view.post(runnable)获得View的宽/高
//1. 通过post将一个runnable投递到消息队列尾部
        view.post(new Runnable() {
            @Override
//2. 等到Looper调用次runnable时,View已经完成初始化
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
C. ViewTreeObserver获得View的宽/高(Kotlin版)
val observer = imageView.viewTreeObserver
   //1. 使用ViewTreeObserver的接口,可以再View树状态改变或者View树内部View的可见性改变时,onGlobalLayout会被毁掉
   observer.addOnGlobalLayoutListener(object :ViewTreeObserver.OnGlobalLayoutListener {
           //2. 能正确获取View宽/高
           override fun onGlobalLayout() {
           //3. 随着View树状态改变,会多次调用。因此需要移除监听器
               imageView.viewTreeObserver.removeGlobalOnLayoutListener(this)
               val width = imageView.measuredWidth
               val height = imageView.measuredHeight
           }
       })
D. view.measure()获得View的宽/高(Kotlin)
  1. mathc_parent的情况下是不可以的,因为需要知道parent的size,这里无法获取。
  2. 具体数值
//1. 具体数值时(dp/px),让View重新测量
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
imageView.measure(widthMeasureSpec, heightMeasureSpec)
//2. 完成后就可以获得宽/高
val width = imageView.width
val height = imageView.height

wrap_content

    //1. wrap_content,将specSize设置为30位二进制的最大值 (1 << 30) - 1,让View重新测量(在AT_MOST情况下是合理的)
    val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST)
    val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST)
    imageView.measure(widthMeasureSpec, heightMeasureSpec)
    //2. 完成后就可以获得宽/高
    val width = imageView.width
    val height = imageView.height

Layout 过程

1、View的layout过程

  1. 使用layout方法确定View本身的位置
  2. layout中调用onLayout方法确定所有子View的位置

2、View的layout()源码分析

  1. 调用setFrame()设置View四个定点位置(即初始化mLeft,mRight,mTop,mBottom的值)
  2. 之后调用onLayout确定子View位置,该方法类似于onMeasure,View和ViewGroup中均没有实现,具体实现与具体布局有关。

3、LinearLayout的onLayout方法

  1. 根据orientation选择调用layoutVertical或者layoutHorizontal
  2. layoutVertical中会遍历所有子元素并调用setChildFrame(里面直接调用子元素的layout方法)
  3. 层层传递下去完成了整个View树的layout过程
  4. setChildFrame中的宽/高实际就是子元素的测量宽/高(getMeasure…后直接传入)

4、View的测量宽高和最终宽高有什么区别?

  1. 等价于getMeasuredWidth和getWidth有什么区别
  2. getWidth = mRight - mLeft,结合源码测量值和最终值是完全相等的。
  3. 区别在于:测量宽高形成于measure过程,最终宽高形成于layout过程(赋值时机不同)
  4. 也有可能导致两者不一致:强行重写View的layout方法,在传参方面改变最终宽/高(虽然这样毫无实际意义)
  5. 某些情况下,View需要多次measure才能确定自己的测量宽高,在前几次测量中等到的值可能有最终宽高不一致。但是最终结果上,测量宽高=最终宽高

Draw 过程

1、draw的步骤

  1. 绘制背景(drawBackground(canvas))
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)-遍历调用所有子View的draw方法
  4. 绘制装饰(如onDrawScollBars)

2、View特殊方法setWillNotDraw

  1. 若一个View不绘制任何内容,需要将该标志置为true,系统会进行相应优化
  2. 默认View不开启该标志位
  3. 默认ViewGroup开启该标志位
  4. 如果我们自定义控件继承自ViewGroup并且本身不进行绘制时,就可以开启该标志位
  5. 当该ViewGroup明确通过onDraw绘制内容时,就需要显式关闭WILL_NOT_DRAW标志位。

参照大牛的文章写了个Android自定义View-渐变的温度指示器,融入自定义处理wrap_content,和padding 的情况,欢迎指正~

上一篇下一篇

猜你喜欢

热点阅读