Android 如何绘制视图

当某个 Activity 获得焦点时,系统会要求它绘制自己的布局。Android 框架会处理绘制流程,但该 Activity 必须提供其布局层次结构的根节点。

绘制从布局的根节点开始,需要测量并绘制布局树。系统通过遍历布局树并渲染与无效区域相交的每个 View 来处理绘制。反过来,每个 ViewGroup 负责请求绘制其每个子级(使用 draw() 方法),而每个 View 负责绘制其本身。由于布局树已经过系统预先遍历,这意味着父级将在它们的子级之前(即后面)进行绘制,而其同级会按照它们在布局树中出现的顺序进行绘制。

注意:该框架不会绘制有效区域之外的 View 对象,并且也不会负责为您绘制 View 背景。

您可以通过调用 invalidate() 来强制绘制 View

绘制布局包含两个遍历流程:一个测量遍历和一个布局遍历。

测量遍历

测量遍历在 measure(int, int) 中实现,是 View 树的自上而下遍历。在递归过程中,每个 View 都会将维度规范下推到布局树。在测量遍历结束时,每个 View 均存储了其测量值。第二次遍历发生在 layout(int, int, int, int) 中,也是自上而下遍历。在此次遍历中,每个父级负责使用测量遍历中计算的尺寸来定位其所有的子级。

当返回 View 对象的 measure() 方法时,必须设置其 getMeasuredWidth()getMeasuredHeight() 值,以及该 View 对象的所有子级的值。View 对象的测量宽度值和测量高度值必须遵守 View 对象的父级所施加的限制。这就保证了在测量遍历结束时,所有父级都会接受其子级的所有测量值。父级 View 可以对其子级多次调用 measure()。例如,父级可以使用未指定的维度测量每个子级一次,以确定它们希望的大小;然后,如果所有子级不受限制的尺寸的总和过大或过小,则再次使用实际的数字对它们调用 measure()(即,如果子级未就各自获得多少空间达成一致,则父级将会介入并针对第二次遍历设置规则)。

测量遍历使用两个类来传达维度。View 对象使用 ViewGroup.LayoutParams 类来告知父级它们想要如何测量和定位。基本的 ViewGroup.LayoutParams 类仅描述了 View 希望的宽度和高度。针对每个维度,它可以指定以下某一项:

  • 一个确切的数字
  • MATCH_PARENT,该参数意味着 View 想要和它的父级一样大(负填充)
  • WRAP_CONTENT,该参数意味着 View 想要足够大,以包含其内容(正填充)。

有适用于 ViewGroup 的不同子类的 ViewGroup.LayoutParams 子类。例如,RelativeLayout 有自己的 ViewGroup.LayoutParams 子类,其中包括使子级 View 对象水平和垂直居中的功能。

MeasureSpec 对象用于在树中将要求从父级下推到子级。MeasureSpec 可以为以下三种模式之一:

  • UNSPECIFIED:父级使用该模式来确定子级 View 所需的维度。例如,LinearLayout 可能会对其高度设置为 UNSPECIFIED 且宽度设置为 EXACTLY 240 的子级调用 measure(),从而确定宽度为 240 像素的子级 View 所需的高度。
  • EXACTLY:父级使用该模式来强制子级使用某个确切尺寸。子级必须使用尺寸,并保证其所有的子项都能放入此尺寸。
  • AT MOST:父级使用该模式来强制规定子级的最大尺寸。子级必须保证它及其所有的子项都能放入此尺寸。

布局遍历

如需启动布局,请调用 requestLayout()。当 View 认为自己无法再放入当前范围时,通常会调用此方法。