创建自定义视图组件

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

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

可用小部件的部分列表包括 ButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinner,以及更专业的 AutoCompleteTextViewImageSwitcherTextSwitcher

可用的布局包括 LinearLayoutFrameLayoutRelativeLayout 等。有关更多示例,请参阅 常见布局

如果没有任何预构建的小部件或布局满足您的需求,您可以创建自己的 View 子类。如果您只需要对现有的小部件或布局进行少量调整,您可以对小部件或布局进行子类化并覆盖其方法。

创建自己的 View 子类可以让您精确控制屏幕元素的外观和功能。为了说明使用自定义视图可以获得的控制权,以下是一些您可以使用它们执行的操作示例

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

以下部分说明如何创建自定义视图并在您的应用程序中使用它们。有关详细参考信息,请参阅 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 文件指定布局,就像使用活动屏幕一样,或者您可以以编程方式创建视图并将它们嵌套到代码中的布局中。
  • onDraw()onMeasure()方法以及大多数其他on方法都具有合适的行为,因此您无需覆盖它们。
  • 您可以快速构建任意复杂的复合视图,并像使用单个组件一样重用它们。

修改现有视图类型

如果有一个与您想要的类似的组件,您可以扩展该组件并覆盖您想要更改的行为。您可以对完全自定义的组件执行所有操作,但是通过从View层次结构中更专业的类开始,您可以获得一些免费实现您想要的行为。

例如,NotePad示例应用演示了使用 Android 平台的许多方面。其中包括扩展EditText视图以创建带线的记事本。这不是一个完美的示例,执行此操作的 API 可能会发生变化,但它演示了原理。

如果您还没有这样做,请将 NotePad 示例导入 Android Studio 或使用提供的链接查看源代码。特别是,请参阅NoteEditor.java文件中LinedEditText的定义。

以下是一些需要注意的事项

  1. 定义

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

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

    此外,LinedEditTextstatic的,这意味着它不会生成所谓的“合成方法”,这些方法允许它访问父类中的数据。这意味着它表现为一个单独的类,而不是与NoteEditor紧密相关的类。如果内部类不需要访问外部类的状态,这是一种更简洁的创建内部类的方法。它使生成的类保持较小,并允许其他类轻松使用它。

    LinedEditText扩展了EditText,在本例中,它是要自定义的视图。完成后,新类可以替代普通的EditText视图。

  2. 类初始化

    与往常一样,首先调用超类。这不是默认构造函数,而是一个参数化构造函数。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方法并引入其自己的帮助器方法,从而大幅自定义其属性和行为。唯一的限制是您的想象力和您需要组件执行的操作。