创建自定义视图组件

试用 Compose 方式
Jetpack Compose 是推荐用于 Android 的界面工具包。了解如何在 Compose 中使用布局。

Android 提供了一个复杂且强大的组件化模型,可帮助您根据基本布局类 和 View ViewGroup构建界面。该平台包含各种预构建的 ViewViewGroup 子类,分别称为微件和布局,可供用来构建界面。

可用的部分微件包括 Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner,以及具有特殊用途的 AutoCompleteTextView, ImageSwitcherTextSwitcher

可用布局包括 LinearLayoutFrameLayoutRelativeLayout 等。如需查看更多示例,请参阅 常见布局

如果预构建的微件或布局都不能满足您的需求,您可以创建自己的 View 子类。如果您只需要对现有微件或布局进行细微调整,则只需将相应微件或布局子类化并替换其方法即可。

通过创建自己的 View 子类,您可以精确控制屏幕元素的外观和功能。为了帮助您了解通过自定义视图获得的控件,下面的一些示例说明了您可以执行的相关操作:

  • 您可以创建一个完全采用自定义渲染方式的 View 类型,例如一个使用 2D 图形渲染的“音量 控制”调节按钮,外形类似于传统的模拟式电子控制旋钮。
  • 您可以将一组 View 组件合并为一个新的组件,比如可以 制作组合框(弹出式列表与自由输入文本字段的组合)、 双窗格选择器控件(左右各一个窗格,分别列出了您可以将哪个项重新分配 到哪个列表)等等。
  • 您可以替换 EditText 组件在屏幕上的渲染方式。 记事本示例应用使用此功能成功创建了带线条的记事本页面。
  • 您可以捕获其他事件(例如按键),并以某种自定义方式处理这些事件(例如在游戏中)。 例如在游戏中。

以下各部分介绍了如何创建自定义视图并在应用中使用它们。如需详细的参考信息,请参阅 View 类。

基本方法

下文概要介绍了创建您自己的 View 组件时需要了解的事项:

  1. 使用您自己的类扩展现有 View 类或子类。
  2. 替换父类中的某些方法。要替换的父类方法以 on开头,例如 onDraw()onMeasure()onKeyDown()。 这类似于您为生命周期和其他功能钩子替换的 on 事件在 ActivityListActivity 中。
  3. 使用您的新扩展类。完成后,可以使用您的新扩展类来代替其所基于的视图。

完全自定义的组件

您可以创建外观完全如您所需的完全自定义图形组件。可能是外观类似于旧模拟计量器的图形 VU 计,也可能是跟唱的文本视图(其中有个弹力球会随着歌词移动,以便您能够跟着卡拉 OK 设备一起唱歌)。您可能需要内置组件无法执行的操作,无论您以何种方式将它们组合起来。

幸运的是,您可以根据自己的喜好轻松创建您所能想象出的具有任意外观和行为方式的组件,只需考虑屏幕大小及可用处理能力,请注意,最终应用可能需要在处理能力比桌面工作站低得多的平台上运行。

如需创建完全自定义的组件,请考虑以下事项:

  • 毫无疑问,您可以扩展的最通用的视图是 View,因此您通常需要先扩展 此视图,以创建新的父组件。
  • 您可以提供一个构造函数(从 XML 获取属性和参数),也可以使用您自己的此类属性和参数(例如声量计的颜色和范围,或指针的宽度和阻尼)。
  • 您可能需要创建自己的事件监听器、属性存取器和修饰符,以及 在组件类中创建可能更为复杂的行为。
  • 您几乎肯定需要替换 onMeasure();如果您希望组件显示某些内容,也可能需要 替换 onDraw()。虽然两者都具有 默认行为,但默认的onDraw()不会执行任何操作,而默认的onMeasure()始终会设置 100x100 的大小,这可能不是您所希望的。
  • 您还可以根据需要替换其他 on 方法。

扩展 onDraw() 和 onMeasure()

onDraw() 方法为您提供了一个 Canvas,您可以在其上实现所需的任何东西:2D 图形、其他标准或自定义组件、样式文本或您可以想到的其他任何东西。

onMeasure() 涉及更多。onMeasure() 是组件与其容器之间的渲染约定的关键部分。onMeasure() 必须替换,以高效且准确地报告其所含部分的测量结果。由于父级的限制要求(传入 onMeasure() 方法),以及一旦计算出宽度和高度就要使用测量的宽度和高度调用 setMeasuredDimension() 方法的要求,这变得稍微有些复杂。如果未通过已替换的 onMeasure() 方法调用此方法,则结果会在测量时出现异常。

简要来说,实现 onMeasure() 的方式如下所示:

  • 调用已替换的 onMeasure() 方法时,应指定宽度和高度 测量规范,应将这些规范视为应该生成的宽度和高度 测量值的限制要求。widthMeasureSpecheightMeasureSpec 参数都是表示尺寸的整数代码。如需对这些规范可能要求的限制的完整参考,请参阅参考文档。此参考文档也清楚地说明了整个测量操作。View.onMeasure(int, int)
  • 组件的 onMeasure() 方法会计算渲染组件所需的测量宽度和高度。此方法应尽量符合传入的规范 ,尽管可以选择超出这些规范。在这种情况下,父级可以根据不同的测量规范选择执行哪些操作,例如 裁剪、滚动、抛出异常或要求 onMeasure() 重试。
  • 计算宽度和高度后,使用计算得出的 测量值调用 setMeasuredDimension(int width, int height) 方法。如果不执行此操作,则会导致系统抛出异常。

下面汇总了框架针对视图调用的一些其他标准方法:

类别 方法 说明
创建 构造函数 包含在从代码创建视图时调用的构造函数形式 和在从布局文件扩充视图时调用的构造函数形式。第二种形式 的构造函数会解析并应用布局文件中定义的属性。
onFinishInflate() 在视图及其所有子级都已从 XML 扩充之后调用。
布局 onMeasure(int, int) 调用以确定此视图及其所有 子级的大小要求。
onLayout(boolean, int, int, int, int) 在此视图必须为其所有子级分配大小和位置时调用。
onSizeChanged(int, int, int, int) 在此视图的大小发生变化时调用。
绘图 onDraw(Canvas) 在视图必须渲染其内容时调用。
事件处理 onKeyDown(int, KeyEvent) 在发生 key down 事件时调用。
onKeyUp(int, KeyEvent) 在发生 key up 事件时调用。
onTrackballEvent(MotionEvent) 在发生轨迹球动作事件时调用。
onTouchEvent(MotionEvent) 在发生触屏动作事件时调用。
专注 onFocusChanged(boolean, int, Rect) 在视图获得或失去焦点时调用。
onWindowFocusChanged(boolean) 在包含视图的窗口获得或失去焦点时调用。
附加 onAttachedToWindow() 在视图附加到窗口时调用。
onDetachedFromWindow() 在视图与其窗口分离时调用。
onWindowVisibilityChanged(int) 在包含视图的窗口的可见性发生变化时调用。

复合控件

如果您不想创建完全自定义的组件,而是希望整合包含一组现有控件的可再用组件,那么创建复合组件(或复合控件)可能就足够了。简而言之,这会将许多更原子的控件或视图整合到可被视为一件事的项的逻辑分组中。 例如,组合框可以是一行 EditText 字段以及一个附加有弹出式列表的相邻按钮的组合。如果用户点按该按钮并从列表中选择内容,则会填充 EditText 字段,但用户也可以根据需要直接在 EditText 中输入内容。

在 Android 中,实际上有另外两个视图可用于执行此操作:SpinnerAutoCompleteTextView。无论如何,组合框的概念都是一个很好的示例。

如要创建复合组件,请执行以下操作:

  • 就像使用 Activity 一样,您可以使用声明式(基于 XML)方法 来创建所包含的组件,也可以通过编程方式从代码中嵌套组件。通常从某种类型的 Layout 入手,因此请创建可扩展 Layout 的类。对于组合框,我们可以使用 LinearLayout 和 水平方向。您可以嵌套其他布局,因此复合组件可以 任意复杂化和结构化。
  • 在新类的构造函数中,获取父类所需的任何参数,将它们 传递给父类构造函数。然后,您可以设置其他视图以在新组件中使用 。您可以在其中创建 EditText 字段和 弹出式列表。您可以将自己的属性和参数引入到 XML 中,以供 构造函数提取和使用。
  • (可选)为包含的视图可能生成的事件创建监听器。例如,如果选择了列表,则可以为列表项点击监听器创建监听器方法以更新 EditText 的内容。
  • (可选)使用存取器和修饰符创建自己的属性。例如,允许一开始在组件中设置 EditText 值,并在需要时查询其内容。
  • (可选)替换 onDraw()onMeasure()。如果要扩展 Layout,通常不需要这样做,因为布局的默认行为会正常发挥作用。
  • (可选)替换其他 on 方法(如 onKeyDown()),例如在点按某个键时从组合框的弹出式列表中选择特定的默认值。

Layout 着手开始使用自定义控件有很多优点,包括:

  • 您可以像使用 Activity 屏幕一样使用声明式 XML 文件指定布局, 也可以通过编程方式从代码中创建视图并将其嵌套在布局中。
  • onDraw()onMeasure() 方法(加上大多数其他 on 方法)具有适当的行为,因此您无需替换它们。
  • 您可以非常快速地构建任意复杂化的复合视图,并像使用单个组件一样重复使用它们。

修改现有视图类型

如果已经有一个组件非常契合您的需要,则只需扩展该组件并替换您希望更改的行为即可。您可以使用完全自定义的组件来完成所有操作,但是通过从 View 层次结构中更专用的类着手,您可以免费获得一些符合您需求的行为。

例如, 记事本 示例应用演示了使用 Android 平台的多个方面。其中包括扩展 EditText 视图以使记事本带有线条。这并非理想示例,并且用于执行此操作的 API 可能会发生变化,但该示例确实演示了相关原则。

如果您尚未这样做,请将记事本示例导入 Android Studio,或者通过提供的链接查看源代码。请特别注意 LinedEditText 中的 NoteEditor.java 文件的定义。

下面是此文件中的一些注意事项:

  1. 定义

    此类使用以下行进行定义:
    public static class LinedEditText extends EditText

    LinedEditText 定义为 NoteEditor activity 中的内部类,但它是公开类,因此可以作为 NoteEditor.LinedEditTextNoteEditor 类的外部访问。

    此外,LinedEditTextstatic,这意味着它不会生成允许其从父类访问数据的所谓“合成方法”。这意味着它的行为方式其实就像单独的类(而不是与 NoteEditor 密切相关的类)。如果内部类不需要从外部类访问状态,那么这是创建内部类的更简洁的方法。可以使生成的类一直比较小,并允许其他 类轻松使用。

    LinedEditText 扩展了 EditText,即我们在 这种情况下选择自定义的视图。之后,新类将能够取代普通的 EditText 视图。

  2. 类初始化

    与往常一样,首先调用父类。这不是默认构造函数,而是 参数化构造函数。可以在 EditText 从 XML 布局文件 扩充时使用这些参数创建它。因此,构造函数需要同时获取这些参数并将其传递给 父类构造函数。

  3. 替换方法

    此示例仅替换 onDraw() 方法,但您可能需要在创建自己的自定义组件时替换 其他方法。

    对于此示例,通过替换 onDraw() 方法,可以在 EditText 视图画布上绘制蓝色线条。此画布将传入已替换的 onDraw() 方法。在 方法结束之前调用 super.onDraw() 方法。必须调用父类方法。在这种情况下,应在绘制要包含的行之后 执行此操作。

  4. 自定义组件

    现在我们有了自定义组件,但该如何使用它呢?在记事本示例中,可以在声明式布局中直接使用自定义组件,因此请查看note_editor.xml中的res/layout文件夹:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />

    您可以将自定义组件创建为 XML 中的通用视图,并使用完整软件包指定类。 您定义的内部类是使用 NoteEditor$LinedEditText 标记引用的,这是以 Java 编程语言引用内部 类别的标准方式。

    如果您的自定义视图组件未定义为内部类,则您可以选择使用 XML 元素名称声明视图 组件,并排除 class 属性。例如:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />

    请注意,LinedEditText 类现在是一个单独的类文件。如果该 类嵌套在NoteEditor类中,则此方法将不起作用。

    定义中的其他属性和参数传入自定义 组件构造函数中,然后传递给 EditText 构造函数,因此 它们与用于 EditText 视图的参数是一样的。您也可以添加 自己的参数。

创建何种复杂程度的自定义组件完全取决于您的需要。

更复杂的组件可以替换更多的 on 方法,并引入一些自己的辅助方法,从而充分地自定义其属性和行为。唯一的限制是您的想象力以及您希望组件执行的操作。