Android如何绘制视图

当一个Activity接收到焦点时,它将被请求绘制其布局(layout)。Android框架将处理这个绘制过程,同时Activity必须提供其布局层级的根节点。

绘制过程从布局的根节点开始。它被请求测量并绘制布局树。绘制过程通过遍历布局树并渲染每一个穿插于invalid区域View进行。每一个视图组顺次负责请求其每一个子视图进行绘制(以draw()方法),而每一个View则负责绘制自身。因为布局树是依次遍历的,这就意味着父视图将在其子视图之前(即位于其下方)被绘制,而兄弟视图根据其出现在数中的位置按顺序被绘制。

绘制布局是一个分为两条线路的过程:测量线路(measuring pass)和布局线路(layout pass)。测量线路由measure(int ,int)完成,是视图树的一次从上至下的遍历。在递归过程中每一个View沿着树向下推送它的尺寸。在测量线路的最后,每一个View就都已储存了其测量尺寸值。第二条线路在layout(int, int, int, int)中完成,它同样也是自上而下的。在这个线路中父视图负责通过在测量线路中计算得到的尺寸来定位其所有的子视图。

Android框架不允许绘制不在invalid区域内的View,同时它将负责绘制View的背景。

可以通过调用invalidate()来强制绘制一个View。

当一个View的measure()方法返回时,必须设定它与它的子孙类的getMeasuredWidth()和getMeasuredHeight()值。一个View的测量宽度和测量高度必须满足其父类的约束。这保证了在测量线路的最后,所有的父类可以符合所有子类的测量值。一个父类View可以不止一次对其子类调用measure()。例如,父类可以以未指定的尺寸来测量每一个子类以确定他们所需的尺寸,之后以实际值对其再次调用measure()来检查所有子类的未约束的尺寸是否过大或是过小。(即,如果子类和其应该占用的尺寸不符,父类将会进行干预,将规则设置为第二条线路)。【抱歉这部分翻译质量欠佳,之后会找时间考虑如何改进(其实很多部分翻译质量都需要提高,总之现在先完成第一遍再说了。渐渐开始理解那些中文版教材令人无法满意的翻译背后的翻译者了……)】

测量线路使用两个类来交流尺度。View.MeasureSpec类被View用来告诉其父类希望被测量和定位的情况。基本的LayoutParams类用来描述View所希望的高宽尺寸。对于每一个维度,可以指定以下之一:

  • 一个确切的数字
  • FILL_PARENT 表示View希望和其父类一样大(减去间隙)
  • WRAP_CONTENT 表示View希望其大小足以包含其内容(加上间隙)。

要实例化一个布局,需要调用requestLayout()。该方法也将在一个View认为自身不可能适合当前边框时自调用。

对于ViewGroup的不同子类有相对应的LayoutParams的子类。丽日,RelativeLayout有其专属的LayoutParams子类,它可以水平及垂直定位其子视图。

MeasureSpec用于将需求沿着布局树从父至子向下推送。一个MeasureSpec可以以下三种模式存在:

  • UNSPECIFIED: 这被父类用于决定某一子视图的期望尺度。例如,一个LineraLayout可以对其子类调用measure(),将高度设置为UNSPECIFIED,宽度设置为EXACTLY 240以确定该子类在被分配了240像素宽度时所需的高度是多少。
  • EXACTLY: 这被父类用于强制设置其子类的确切尺寸。子类必须使用该尺寸,且保证它的所有子嗣也都满足这个尺寸限定。
  • AT_MOST: 这被父类用语强制设置其子类的最大尺寸。子类必须保证它及其子嗣满足该尺寸。

返回“用户界面”

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

用户界面

Android程序通过View和ViewGroup对象来构建用户界面。有许多种类的View和ViewGroup,他们都继承于View类。

View对象是Android平台的基本用户界面表现单元。View类是其子类“widgets”(提供诸如文本区域和按钮等全功能UI部件)的基础。ViewGroup类是其子类“layouts”(提供诸如线性、列表和相对等不同类型的布局结构)的基础。

View对象是一种数据结构,塔储存了布局参数和一块特定屏幕矩形区中的内容。View对象处理其自身所属屏幕矩形区域的测量、布局、绘图、焦点改变、滚动、及按键/手势交互。作为用户界面中的一个对象,一个View也是交互事件中用户和接收者的一个交互点。

视图层级(View Hierarchy)

在Android平台上,可以通过View和ViewGroup结点的层级结构来定义Android的UI,就像下图那样。这棵层级树可以随你所需或简单或复杂,你可以使用Android预定义的widget和layout来搭建,或是使用自己创建的自定义View。

为了让视图层级树可以在屏幕上被渲染,一个活动必须调用setContentView()方法并把根节点对象传递给它。Android收到这个引用后用它来擦除、测量、绘制这棵树。树的根节点要求其子节点绘制出自身——这点对于子结点也相同,子节点会要求其子结点自绘。这些子节点会向父结点请求尺寸和位置,不过父结点对子结点的大小位置有最终决定权。Android依次处理布局元素(从层级树的顶部开始),初始化View并将其加入其父结点。正因如此,当有元素重叠时,较后绘制的会覆盖先前绘制的内容。

更多关于视图是如何层级化并被测量绘制的,请阅读“Android如何绘制视图”。

布局(Layout)

使用XML布局文件是最常用的定义布局并表达视图层级的方法。XML为布局提供了一种可读性很高的结构,和HTML相似。XML中的每一个元素都是一个View或者ViewGroup对象(或其子嗣)。View对象是树的叶结点,ViewGroup对象是树的枝结点(如上图所示)。

一个XML元素的名称与其在所显示的Java类相对应。因此一个<TextView>元素会在UI中创建一个TextView,一个<LinearLayout>元素会在视图组中创建一个线性布局。当载入布局资源时,Android系统会根据布局中的元素来初始化这些运行对象。

举例来说,一个有着一个文本视图和一个按钮的垂直视图(vertical layout)是这样的:

注意LinearLayout元素中同时包含有文本视图和按钮。你可以在这里嵌套另一个LinearLayout(或是其他类型的视图组(view group))来加长视图层级,创建一个更为复杂的布局。

关于构建用户界面布局的更多情报请阅读“声明布局”。

Tip:还可以通过Java代码绘制View和ViewGroup对象。使用addView(View)方法来动态地插入新的View和ViewGroup对象。

可以通过多种途径来设计视图的布局。通过使用更多不同类型的视图组可以构造出无数的子视图和视图组。Android提供了一些预定义的视图组(被称为布局)。包括LinearLayout,RelativeLayout,TableLayout,GridLayout等。它们都提供了一些特定的参数用以定义其子视图的位置和布局的结构。

想要获知关于用于布局的不同视图组的更多情报,请阅读“常用Layout对象”。

部件(Widget)

部件是一种View对象,是用户交互的一种接口。Android提供了一套完整可用的部件,比如按钮、勾选框、文本输入框,因此能够快速地构建UI。Android还提供一些更为复杂的部件,比如日期采集器(date picker)、闹钟、缩放控制。不过能使用的部件并不受限于Android平台所提供的这些。通过声明自有的View对象或是继承整合已有的部件,可以创建出更为定制化的部件来实现自定义的功能。

更多内容请阅读“构建自定义组件”。

关于Android所提供的部件列表,参阅android.widget包。

UI事件

在向UI添加了视图/部件之后,需要了解它们如何进行用户交互以实现功能。为了让UI收到事件通知,需要完成以下两件事中的一件:

  • 定义一个事件侦听器并将其在View中注册。通常会通过它来侦听事件。View类包含一系列的嵌套接口,以On<XXXX>Listener命名,并分别拥有一个On<XXXX>()的回调方法。比如,View.OnClickListener(处理View的“点击”),View.OnTouchListener(处理View中的触屏事件),以及View.OnKeyListener(处理View中的设备按键)。如果希望View在“点击”时(比如选中一个按钮)收到通知,就使用OnClickListener并定义其onClick()回调方法(当点击时要执行的动作),之后通过setOnClickListener()将其注册给View。
  • 为View覆写一个已有的回调方法。当使用自建的View类并希望其侦听特殊的事件时需要这样做。可以处理的事件有比如屏幕触摸(onTouchEvent()),轨迹球移动(onTrackballEvent()),或设备键盘操作(onKeyDown())。允许在自定义View中定义每一个事件的默认行为以及事件是否应被传递给其他子视图。再次说明,这些是View类的回调方法,因此只有在构建自定义组件时才能定义它们。

可以在“处理UI事件”一文中继续阅读处理用户界面交互的更多内容。

菜单(Menu)

程序菜单是程序用户界面的另一重要组成部分。菜单提供了一种可靠的接口来显示程序的功能与设置。通常按下设备的菜单键可以调出程序菜单。然而,也可以添加当用户长按某一项目时调出的上下文菜单(Context Menu)。

菜单也是通过视图层级构建的,但是不需要亲自定义其结构。只要为程序定义onCreateOptionsMenu()或onCreateContextMenu()回调方法,之后声明想要包含于菜单内的项目即可。在合适的时刻,Android会自动为菜单创建必要的View等级,并在其中绘制每一个菜单项目。

菜单也会处理其自身的事件,因此不必为菜单中的项目注册事件侦听器。当菜单中的项目被选择时,onOptionItemSelected()或是onContextItemSelected()方法会被框架调用。

如同程序布局,可以在XML文件中声明菜单项目。

阅读“创建菜单”以了解更多。

高级主题

在掌握了创建用户界面的基本内容之后,可以试着用一些高级特性来创建更为复杂的程序接口。

适配器(Adapter)

有时需要把一些无法硬解码的信息装入一个视图组,这时要将视图与外部数据源绑定。为此,需要将AdapterView作为视图组,并将其子视图通过适配器的数据初始化后装入。

AdapterView对象是ViewGroup的一种实现,它说明其子视图是基于Adapter对象的。Adapter充当数据源(比如一列外部字符串)与AdapterView之间的信使角色来显示数据。Adapter类对于专门的任务有这多种不同的实现,比如从Cursor读取数据库数据的CursorAdapter,或是任意读取数组的ArrayAdapter。

欲了解更多关于使用Adapter装入视图的信息,参阅“使用AdapterView绑定数据”。

风格(Style)和主题(Theme)

如果不满意标准部件的外观,可以自己建立风格和主题来修改它们。

  • 风格是一组若干个格式属性,可以将其作为一个单位使用于布局的单个元素中。比如,可以定义一个具有特定文本尺寸和颜色的风格,然后将其应用到特定的View元素中。
  • 主题是一组若干个格式属性,可以将其作为一个单位使用于一个程序的所有活动,或是单个活动中。比如,可以定义一个具有特定窗口框架颜色、面板背景颜色、文本尺寸和菜单颜色的主题。该主题可以被应用于特定活动或是整个程序。

风格和主题是资源。Android提供了一些默认的风格和主题供使用。也可以声明自定义的风格与主题资源。

在“应用风格与主题”一文中可以了解更多关于使用风格和主题的信息。

本页部分内容根据Android Open Source Project创作并共享的内容修改,并在知识共享 署名2.5许可协议中所述条款的限制下使用。