创建自定义视图组件

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

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

可用的 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) 在松开键事件发生时调用。
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

    LinedEditTextNoteEditor Activity 中定义为一个内部类,但它是公共的,因此可以从 NoteEditor 类外部作为 NoteEditor.LinedEditText 访问。

    此外,LinedEditTextstatic 的,这意味着它不会生成所谓的“合成方法”来让它访问父类中的数据。这意味着它作为一个独立的类来行为,而不是与 NoteEditor 密切相关的事物。如果内部类不需要访问外部类的状态,这是一种更简洁的创建内部类的方法。它使生成的类较小,并且可以轻松地从其他类中使用。

    LinedEditText 扩展了 EditText,在这种情况下是要自定义的视图。完成后,新类可以替代普通的 EditText 视图。

  2. 类初始化

    和往常一样,首先调用 super。这并非默认构造函数,而是参数化构造函数。EditText 在从 XML 布局文件膨胀时会使用这些参数创建。因此,构造函数需要接受它们并将其传递给超类构造函数。

  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 方法并引入自己的 helper 方法,从而大幅自定义其属性和行为。唯一的限制是您的想象力以及组件需要实现的功能。