创建自定义视图组件

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

Android 提供了一个复杂而强大的组件化模型,用于基于基本布局类 ViewViewGroup 构建界面。该平台包含各种预构建的 ViewViewGroup 子类(分别称为 widget 和布局),可供您用来构建界面。

可用的部分微件包括 ButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinner,以及具有特殊用途的 AutoCompleteTextViewImageSwitcherTextSwitcher

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

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

通过创建自己的 View 子类,您可以精确控制屏幕元素的外观和功能。为了让您了解自定义视图可以实现哪些控制,下面列举了一些示例来说明您可以如何使用自定义视图:

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

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

基本方法

下面简要介绍了创建您自己的 View 组件需要了解的内容:

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

完全自定义的组件

您可以创建外观完全自定义的图形组件。您可能想要一个看起来像旧模拟量表的图形声量计,或者想要一个跟唱文本视图(在您跟着卡拉 OK 机唱歌时,一个弹力球会随着歌词移动。您可能会需要内置组件无法执行的操作,无论您以何种方式组合使用它们。

幸运的是,您可以根据自己的想象力、屏幕大小和可用处理能力来创建外观和行为符合自己要求的组件。但请注意,应用可能需要在比桌面工作站低得多的功耗上运行。

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

  • 您可以扩展的最通用的视图是 View,因此您通常首先需要扩展此视图来创建新的超级组件。
  • 您可以提供一个构造函数,它可以从 XML 获取属性和参数,并且可以使用您自己的此类属性和参数,例如 VU 计的颜色和范围或指针的宽度和阻尼。
  • 您可能需要创建自己的事件监听器、属性存取器和修饰符,以及组件类中更复杂的行为。
  • 您几乎肯定需要替换 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) 在���生按键按下事件时调用。
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 作为自定义控件的基础具有如下优势:

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

修改现有视图类型

如果存在与您所需的组件类似的组件,您可以扩展该组件并替换您想要更改的行为。您可以使用完全自定义的组件执行所有操作,但通过从 View 层次结构中更专用的类着手,您可以免费获得一些执行您所需要的行为。

例如,NotePad 示例应用演示了使用 Android 平台的多个方面。其中包括扩展 EditText 视图,使之成为带线条的记事本。这并不是一个完美的示例,并且用于执行此操作的 API 可能会发生变化,但它演示了相关原则。

如果您尚未执行此操作,请将 NotePad 示例导入 Android Studio,或使用提供的链接查看源代码。请特别留意 NoteEditor.java 文件中 LinedEditText 的定义。

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

  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. 自定义组件

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

    <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 方法并引入自己的辅助方法,从而充分地自定义其属性和行为。唯一的限制是您的想象力以及您需要组件执行的操作。