自定义 Dialog 中遇到的宽度不能匹配父窗口问题分析

5,216 阅读4分钟

Bug场景描述

大致场景就是在项目中需要自定义一个对话框,那么我就继承了一个Dialog然后理所当然的setContentView()咯,其它的业务逻辑就不讲了。
布局将下吧:
最外层是一个verticalLinearLayout,然后有个title,有一个EditText,最后底部有2个对称的Button,布局很简单。
当我没有手动在代码中设置对话框的高度的时候,出现的情况是:对话框的高度只有title的高度,奇怪吧。
当我加上了下面的代码:

WindowManager m = getWindow().getWindowManager();
      Display d = m.getDefaultDisplay();
      WindowManager.LayoutParams p = getWindow().getAttributes();
      p.width = d.getWidth();
      getWindow().setAttributes(p);

就正常了。

当时自欺欺人,就糊弄过去了。

直到今天看了Android中的View的绘制过程,才知道当初的问题所在。

首先简单的介绍下View的绘制过程:

  1. measure:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:
    mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。
  2. layout:根据子视图的大小以及布局参数将View树放到合适的位置上。
  3. draw:ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。

View树是遍历绘制的,内部的主体逻辑是判断是否需要重新测量视图大小(measure),是否需要重新布局(layout),是否重新需要绘制(draw)。measure过程是遍历的前提,只有measure后才能进行布局(layout)和绘制(draw),因为在layout的过程中需要用到measure过程中计算得到的每个View的测量大小,而draw过程需要layout确定每个view的位置才能进行绘制。

我们在编写layout的xml文件时会碰到layout_width和layout_height两个属性,对于这两个属性我们有三种选择:赋值成具体的数值,match_parent或者wrap_content,而measure过程就是用来处理match_parent(低版本叫fill_parent)或者wrap_content,假如layout中规定所有View的layout_width和layout_height必须赋值成具体的数值,那么measure其实是没有必要的,但是google在设计Android的时候考虑加入match_parent或者wrap_content肯定是有原因的,它们会使得布局更加灵活。

看下measue(int widthMeasureSpec, int heightMeasureSpec)中的两个参数, 这两个参数分别是父视图提供的测量规格,当父视图调用子视图的measure函数对子视图进行测量时,会传入这两个参数,通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格,在ViewGroup的measureChildWithMargins函数中体现了这个过程,稍后会介绍。

MeasureSpec参数的值为int型,分为高32位和低16为,高32位保存的是specMode,低16位表示specSize,specMode分三种:

1、MeasureSpec.UNSPECIFIED(int值为0),父视图不对子视图施加任何限制,子视图可以得到任意想要的大小;
2、MeasureSpec.EXACTLY,父视图希望子视图的大小是specSize中指定的大小;
3、MeasureSpec.AT_MOST,子视图的大小最多是specSize中的大小。

父视图对子视图无限制时,一般使用measue(0,0),即specModeMeasureSpec.UNSPECIFIED

MeasureSpec有3种模式分别是UNSPECIFIED, EXACTLY和AT_MOST, 那么这些模式和我们平时设置的layout参数fill_parent, wrap_content有什么关系呢。经过代码测试就知道,当我们设置width或height为fill_parent时,容器在布局时调用子view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。而当设置为wrap_content时,容器传进去的是AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。当子view的大小设置为精确值时,容器传入的是EXACTLY

fill_parent应该是子view会占据剩下容器的空间,而不会覆盖前面已布局好的其他view空间,当然后面布局子view就没有空间给分配了,所以fill_parent属性对布局顺序很重要。

之前的问题是由于我在写title的样式的时候宽度写成了wrap_content,当在进行measure的时候首先测量的是title那么自然dialog的宽度是按照title来了,之后测量的控件都是match_parent,显然已经没有意义了,因为此时的parent只有title的宽度了。

最后啰嗦一句,我仿佛明白了,为什么当初Google为什么要把fill_parent改成match_parent了,你明白了吗!