创建自定义视图组件

试用 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. 使用您的新扩展类。完成后,您可以使用新的扩展类代替它所基于的视图。

完全自定义的组件

您可以创建完全自定义的图形组件,按照您希望的方式显示。也许您需要一个看起来像老式模拟仪表的图形 VU 计,或者一个跟唱文本视图,其中随着您跟着卡拉 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. 类初始化

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

  3. 替换的方法

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

    在此示例中,通过替换 onDraw() 方法,您可以在 EditText 视图画布上绘制蓝色线条。画布会传递到已替换的 onDraw() 方法中。系统会在 super.onDraw() 方法结束之前调用该方法。必须调用父类方法。在这种情况下,请在绘制要添加的线条后在最后调用它。

  4. 自定义组件

    现在,您已经有了自定义组件,但如何使用它呢?在 NotePad 示例中,自定义组件直接在声明式布局中使用,因此请查看 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 方法并引入自己的辅助方法,从而实质性地自定义其属性和行为。唯一的限制是您的想象力以及您需要组件执行的操作。