创建自定义视图

此 Codelab 是“使用 Kotlin 进行高级 Android 开发”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘课程的价值,但并不强制要求这样做。“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页列出了所有课程 Codelab。

简介

Android 提供了一组丰富的 View 子类,例如 ButtonTextViewEditTextImageViewCheckBoxRadioButton。您可以使用这些子类构建可实现用户互动并在应用中显示信息的界面。如果没有任何 View 子类能满足您的需求,您可以创建一个 View 子类,即自定义 视图。

如需创建自定义视图,您可以扩展现有的 View 子类(例如 ButtonEditText),也可以创建自己的 View 子类。通过直接扩展 View,您可以替换 ViewonDraw() 方法来绘制任意大小和形状的互动式界面元素。

创建自定义视图后,您可以像添加 TextViewButton 一样将其添加到 activity 布局中。

本课将介绍如何通过扩展 View 从头开始创建自定义视图。

您应当已掌握的内容

  • 如何使用 Android Studio 创建包含 activity 的应用并运行该应用。

学习内容

  • 如何扩展 View 以创建自定义视图。
  • 如何绘制圆形自定义视图。
  • 如何使用监听器来处理用户与自定义视图的互动。
  • 如何在布局中使用自定义视图。

您将执行的操作

  • 扩展 View 以创建自定义视图。
  • 使用绘制和涂绘值初始化自定义视图。
  • 替换 onDraw() 以绘制视图。
  • 使用监听器来提供自定义视图的行为。
  • 将自定义视图添加到布局中。

CustomFanController 应用演示了如何通过扩展 View 类来创建自定义视图子类。新子类名为 DialView

应用显示一个类似于实体风扇控制器的圆形界面元素,其中包含关闭 (0)、低 (1)、中 (2) 和高 (3) 设置。当用户点按视图时,选择指示器会移至下一个位置:0-1-2-3,然后返回到 0。此外,如果所选值大于或等于 1,视图圆形部分的背景颜色会从灰色变为绿色(表示风扇电源已开启)。

视图是应用界面的基本构建块。View 类提供了许多子类(称为 界面 widget),可满足典型 Android 应用界面的许多需求。

ButtonTextView 等界面构建块是扩展 View 类的子类。为节省时间和开发精力,您可以扩展这些 View 子类之一。自定义视图会继承其父视图的外观和行为,并且您可以替换想要更改的行为或外观方面。例如,如果您扩展 EditText 以创建自定义视图,则该视图的行为与 EditText 视图完全相同,但也可以进行自定义,以显示一个用于清除文本输入字段中的文本的 X 按钮。

您可以扩展任何 View 子类(例如 EditText)来获取自定义视图,选择最接近您要实现的目标的子类。然后,您可以在一个或多个布局中将该自定义视图用作具有属性的 XML 元素,就像使用任何其他 View 子类一样。

如需从头开始创建自己的自定义视图,请扩展 View 类本身。您的代码会替换 View 方法来定义视图的外观和功能。创建自定义视图的关键在于,您负责将任意大小和形状的整个界面元素绘制到屏幕上。如果您对现有视图(例如 Button)进行子类化,该类会为您处理绘制。(本 Codelab 后面会详细介绍绘制。)

如需创建自定义视图,请按照以下常规步骤操作:

  • 创建一个扩展 View 或扩展 View 子类(例如 ButtonEditText)的自定义视图类。
  • 如果您扩展了现有的 View 子类,请仅替换您想要更改的行为或外观方面。
  • 如果您扩展了 View 类,请通过替换新类中的 View 方法(例如 onDraw()onMeasure())来绘制自定义视图的形状并控制其外观。
  • 添加代码以响应用户互动,并在必要时重新绘制自定义视图。
  • 在 activity 的 XML 布局中,将自定义视图类用作界面 widget。您还可以为视图定义自定义属性,以便在不同的布局中自定义视图。

在此任务中,您将执行以下操作:

  • 创建一个应用,其中包含一个 ImageView 作为自定义视图的临时占位符。
  • 扩展 View 以创建自定义视图。
  • 使用绘制和涂绘值初始化自定义视图。

第 1 步:创建包含 ImageView 占位符的应用

  1. 使用 Empty Activity 模板创建一个标题为 CustomFanController 的 Kotlin 应用。确保软件包名称为 com.example.android.customfancontroller
  2. 文本标签页中打开 activity_main.xml 以修改 XML 代码。
  3. 将现有的 TextView 替换为以下代码。此文本充当 activity 中自定义视图的标签。
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. 将此 ImageView 元素添加到布局中。这是您将在此 Codelab 中创建的自定义视图的占位符。
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. 提取两个界面元素中的字符串和维度资源。
  2. 点击设计标签页。布局应如下所示:

第 2 步:创建自定义视图类

  1. 创建一个名为 DialView 的新 Kotlin 类。
  2. 修改类定义以扩展 View。出现提示时,导入 android.view.View
  3. 点击 View,然后点击红色灯泡。选择 Add Android View constructors using '@JvmOverloads'。Android Studio 会添加 View 类中的构造函数。@JvmOverloads 注解指示 Kotlin 编译器为此函数生成替换默认形参值的重载。
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. DialView 类定义上方(导入下方),添加一个顶级 enum 来表示可用的风扇转速。请注意,此 enum 的类型为 Int,因为这些值是字符串资源,而不是实际的字符串。Android Studio 会针对这些值中缺少的字符串资源显示错误;您将在后续步骤中修复该问题。
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. enum 下方,添加以下常量。您将使用这些值来绘制表盘指示器和标签。
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. DialView 类中,定义绘制自定义视图所需的多个变量。如果收到请求,则导入 android.graphics.PointF
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • radius 是圆的当前半径。此值是在视图绘制到屏幕上时设置的。
  • fanSpeed 是风扇的当前速度,是 FanSpeed 枚举中的一个值。默认情况下,该值为 OFF
  • 最后postPosition 是一个 X、Y 点,用于在屏幕上绘制视图的多个元素。

这些值是在此处创建和初始化的,而不是在实际绘制视图时创建和初始化的,以确保实际绘制步骤尽可能快速地运行。

  1. 同样在 DialView 类定义中,使用一些基本样式初始化 Paint 对象。在收到请求时,导入 android.graphics.Paintandroid.graphics.Typeface。与之前的变量一样,这些样式在此处进行初始化,以加快绘制步骤。
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. 打开 res/values/strings.xml 并添加风扇速度的字符串资源:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

创建自定义视图后,您需要能够绘制该视图。当您扩展 View 子类(例如 EditText)时,该子类会定义视图的外观和属性,并在屏幕上自行绘制。因此,您无需编写代码来绘制视图。您可以替换父级的方法,改为自定义视图。

如果您要从头开始创建自己的视图(通过扩展 View),则每次屏幕刷新时,您都必须负责绘制整个视图,并替换处理绘制的 View 方法。为了能正确绘制扩展 View 的自定义视图,您需要:

  • 通过替换 onSizeChanged() 方法,在视图首次显示时以及每次视图大小发生变化时计算视图的大小。
  • 替换 onDraw() 方法,以使用由 Paint 对象设置样式的 Canvas 对象绘制自定义视图。
  • 在响应用户点击(该点击会更改视图的绘制方式)时调用 invalidate() 方法,以使整个视图失效,从而强制调用 onDraw() 来重新绘制视图。

每次屏幕刷新时都会调用 onDraw() 方法,这可能每秒发生多次。出于性能考虑并为避免出现视觉故障,您应尽可能减少在 onDraw() 中的操作。尤其是不要在 onDraw() 中放置分配,因为分配可能会引起垃圾回收,从而造成视觉卡顿。

CanvasPaint 类提供了一些实用的绘制快捷方式:

在后续 Codelab 中,您将详细了解 CanvasPaint。如需详细了解 Android 如何绘制视图,请参阅Android 如何绘制视图

在此任务中,您将使用 onSizeChanged()onDraw() 方法在屏幕上绘制风扇控制器自定义视图(包括拨盘本身、当前位置指示器和指示器标签)。您还将创建一个辅助方法 computeXYForSpeed(),,用于计算表盘上指示器标签的当前 X、Y 位置。

第 1 步:计算位置并绘制视图

  1. DialView 类中,在初始化下方,替换 View 类中的 onSizeChanged() 方法,以计算自定义视图表盘的大小。导入 kotlin。在收到请求时,导入 math.min

    每当视图的大小发生变化时(包括在布局膨胀时首次绘制视图时),系统都会调用 onSizeChanged() 方法。替换 onSizeChanged() 以计算位置、尺寸以及其他与自定义视图大小相关的任何值,而不要在每次绘制时都重新计算。在这种情况下,您可以使用 onSizeChanged() 来计算表盘圆形元素的当前半径。
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. onSizeChanged() 下方,添加以下代码,为 PointF 类定义 computeXYForSpeed() 扩展函数。在收到请求时,导入 kotlin.math.coskotlin.math.sin。此 PointF 类的扩展函数用于计算文本标签和当前指示器(0、1、2 或 3)在屏幕上的 X、Y 坐标,前提是已知当前 FanSpeed 位置和表盘半径。您将在 onDraw(). 中使用此功能
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. 替换 onDraw() 方法,以使用 CanvasPaint 类在屏幕上渲染视图。在收到请求时,导入 android.graphics.Canvas。以下是骨架替换:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. onDraw() 内,添加以下行,以根据风扇速度是 OFF 还是任何其他值,将绘制颜色设置为灰色 (Color.GRAY) 或绿色 (Color.GREEN)。在收到请求时,导入 android.graphics.Color
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. 添加以下代码,使用 drawCircle() 方法绘制表盘的圆圈。此方法使用当前视图的宽度和高度来查找圆的中心、圆的半径和当前的绘制颜色。widthheight 属性是 View 超类的成员,用于指示视图的当前尺寸。
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. 添加以下代码以绘制风速指示标记的小圆圈,同样使用 drawCircle() 方法。此部分使用 PointFcomputeXYforSpeed() 扩展方法,用于根据当前风扇转速计算指示器中心的 X、Y 坐标。
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. 最后,在表盘周围的适当位置绘制风扇速度标签(0、1、2、3)。此方法的一部分会再次调用 PointF.computeXYForSpeed() 以获取每个标签的位置,并每次重复使用 pointPosition 对象以避免分配。使用 drawText() 绘制标签。
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

完成后的 onDraw() 方法如下所示:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

第 2 步:将视图添加到布局中

如需向应用的界面添加自定义视图,请在 activity 的 XML 布局中将其指定为元素。使用 XML 元素属性控制其外观和行为,就像控制任何其他界面元素一样。

  1. activity_main.xml 中,将 dialViewImageView 标记更改为 com.example.android.customfancontroller.DialView,并删除 android:background 属性。DialView 和原始 ImageView 都从 View 类继承标准属性,因此无需更改任何其他属性。新的 DialView 元素如下所示:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. 运行应用。风扇控制视图会显示在 activity 中。

最后一步是让自定义视图在用户点按该视图时执行操作。每次点按都应将选择指示器移至下一个位置:关闭-1-2-3,然后返回到关闭。此外,如果选择为 1 或更高,则将背景从灰色更改为绿色,表示风扇电源已开启。

如需使自定义视图可点击,您需要:

  • 将视图的 isClickable 属性设置为 true。这样,您的自定义视图就可以响应点击操作。
  • 实现 View 类的 performClick() 以在点击视图时执行操作。
  • 调用 invalidate() 方法。这会告知 Android 系统调用 onDraw() 方法来重新绘制视图。

通常,对于标准 Android 视图,您需要实现 OnClickListener(),以便在用户点击该视图时执行操作。对于自定义视图,您需要实现 View 类的 performClick() 方法,并调用 superperformClick(). 默认的 performClick() 方法也会调用 onClickListener(),因此您可以将操作添加到 performClick(),并使 onClickListener() 可供您或其他可能使用您的自定义视图的开发者进一步自定义。

  1. DialView.kt 中,于 FanSpeed 枚举内添加一个扩展函数 next(),该函数会将当前风扇速度更改为列表中的下一个速度(从 OFF 更改为 LOWMEDIUMHIGH,然后再返回到 OFF)。现在,完整的枚举如下所示:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. DialView 类中,紧挨着 onSizeChanged() 方法之前,添加一个 init() 代码块。将视图的 isClickable 属性设置为 true 可使该视图接受用户输入。
init {
   isClickable = true
}
  1. init(), 下方,使用以下代码替换 performClick() 方法。
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

super 的调用。必须先执行 performClick(),这样才能启用无障碍事件以及调用 onClickListener()

接下来的两行代码使用 next() 方法增加风扇的速度,并将视图的内容说明设置为表示当前速度(关闭、1、2 或 3)的字符串资源。

最后,invalidate() 方法会使整个视图失效,从而强制调用 onDraw() 来重新绘制视图。如果自定义视图中的某些内容因任何原因(包括用户互动)而发生变化,并且需要显示该变化,请调用 invalidate().

  1. 运行应用。点按 DialView 元素,将指示器从关闭状态移至 1。表盘应变为绿色。每次点按时,指示器都应移至下一个位置。当指示器返回到关闭状态时,表盘应再次变为灰色。

此示例展示了如何将自定义属性与自定义视图搭配使用的基本机制。您可以为 DialView 类定义自定义属性,并为每个风扇旋钮位置设置不同的颜色。

  1. 创建并打开 res/values/attrs.xml
  2. <resources> 内,添加 <declare-styleable> 资源元素。
  3. <declare-styleable> 资源元素内,添加三个 attr 元素(每个属性对应一个),并添加 nameformatformat 类似于类型,在本例中为 color
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. 打开 activity_main.xml 布局文件。
  2. DialView 中,添加 fanColor1fanColor2fanColor3 的属性,并将它们的值设置为如下所示的颜色。请使用 app: 作为自定义属性的前缀(如 app:fanColor1),而不是 android:,因为您的自定义属性属于 schemas.android.com/apk/res/your_app_package_name 命名空间,而不是 android 命名空间。
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

如需使用 DialView 类中的属性,您需要检索这些属性。它们存储在 AttributeSet 中,如果存在,则在创建时传递给您的类。您可以在 init 中检索属性,并将属性值分配给本地变量以进行缓存。

  1. 打开 DialView.kt 类文件。
  2. DialView 内,声明用于缓存属性值的变量。
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. init 代码块中,使用 withStyledAttributes 扩展函数添加以下代码。您提供属性和视图,并设置本地变量。导入 withStyledAttributes 也会导入正确的 getColor() 函数。
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. 使用 onDraw() 中的局部变量可根据当前风扇转速设置表盘颜色。将设置绘制颜色的代码行 (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN) 替换为以下代码。
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. 运行应用,点击拨号器,每个位置的颜色设置应有所不同,如下所示。

如需详细了解自定义视图属性,请参阅创建视图类

无障碍是一组设计、实现和测试技术,可让所有人(包括残障人士)都能使用您的应用。

可能会影响用户使用 Android 设备的常见障碍包括:失明、弱视、色盲、失聪或听力受损,以及动作技能受限。如果您在开发应用时考虑无障碍功能,那么不仅可以改善有这些障碍的用户体验,还可以改善所有其他用户的用户体验。

Android 在标准界面视图(例如 TextViewButton)中默认提供多项无障碍功能。不过,在创建自定义视图时,您需要考虑该自定义视图如何提供无障碍功能,例如屏幕内容的语音描述。

在此任务中,您将了解 Android 的屏幕阅读器 TalkBack,并修改应用以包含 DialView 自定义视图的可读提示和说明。

第 1 步:探索 TalkBack

TalkBack 是 Android 的内置屏幕阅读器。启用 TalkBack 后,用户无需查看屏幕即可与 Android 设备互动,因为 Android 会大声描述屏幕元素。视障用户在使用您的应用时可能需要依赖于 TalkBack。

在此任务中,您将启用 TalkBack,以了解屏幕阅读器的工作方式以及如何浏览应用。

  1. 在 Android 设备或模拟器上,依次前往设置 > 无障碍 > TalkBack
  2. 点按开启/关闭切换按钮即可开启 TalkBack。
  3. 点按确定以确认权限。
  4. 根据提示,确认您的设备密码。如果您是首次运行 TalkBack,系统会启动教程。(旧款设备可能不支持此教程。)
  5. 闭着眼睛浏览教程可能很有帮助。如果以后想再次打开该教程,请依次前往设置 > 无障碍 > TalkBack > 设置 > 启动 TalkBack 教程
  6. 编译并运行 CustomFanController 应用,或使用设备上的概览最近按钮打开该应用。开启 TalkBack 后,请注意系统会读出应用的名称以及标签 TextView 的文字(“风扇控制”)。不过,如果您点按 DialView 视图本身,系统不会读出有关该视图状态(拨号盘的当前设置)或点按该视图以将其激活时将发生的操作的任何信息。

第 2 步:为表盘标签添加内容说明

内容说明用于描述应用中视图的含义和用途。借助这些标签,屏幕阅读器(例如 Android 的 TalkBack 功能)可以准确说明每个元素的功能。对于 ImageView 等静态视图,您可以使用 contentDescription 属性在布局文件中向视图添加内容说明。文本视图(TextViewEditText)会自动使用视图中的文本作为内容说明。

对于自定义风扇控制视图,您需要在每次点击该视图时动态更新内容说明,以指示当前风扇设置。

  1. DialView 类的底部,声明一个不带实参或返回类型的函数 updateContentDescription()
fun updateContentDescription() {
}
  1. updateContentDescription() 内,将自定义视图的 contentDescription 属性更改为与当前风扇速度(关闭、1、2 或 3)关联的字符串资源。这些标签与在屏幕上绘制表盘时 onDraw() 中使用的标签相同。
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. 向上滚动到 init() 代码块,并在该代码块的末尾添加对 updateContentDescription() 的调用。在初始化视图时初始化内容说明。
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. performClick() 方法中,在 invalidate() 之前添加对 updateContentDescription() 的另一个调用。
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. 编译并运行应用,并确保 TalkBack 已开启。点按以更改拨号视图的设置,并注意现在 TalkBack 会播报当前标签(关闭、1、2、3)以及“点按两次即可激活”短语。

第 3 步:为点击操作添加更多信息

到这里可以告一段落了,您的视图可以在 TalkBack 中使用。不过,如果视图不仅能指示其可以激活(“点按两次即可激活”),还能说明激活后发生什么情况(“点按两次即可更改”或“点按两次即可重置”),那就更好了。

为此,您可以通过无障碍服务委托将有关视图操作(此处为点击或点按操作)的信息添加到无障碍服务节点信息对象。借助无障碍功能委托,您可以通过组合(而非继承)自定义应用的无障碍功能相关特性。

在此任务中,您将使用 Android Jetpack 库 (androidx.*) 中的无障碍功能类,以确保向后兼容性。

  1. DialView.ktinit 块中,将视图的无障碍功能委托设置为新的 AccessibilityDelegateCompat 对象。在收到请求时,导入 androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat。此策略可在应用中实现最大程度的向后兼容性。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. AccessibilityDelegateCompat 对象内,使用 AccessibilityNodeInfoCompat 对象替换 onInitializeAccessibilityNodeInfo() 函数,并调用父类的方法。出现提示时,导入 androidx.core.view.accessibility.AccessibilityNodeInfoCompat
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

每个视图都有一个无障碍节点树,该树可能与视图的实际布局组件相对应,也可能不对应。Android 的无障碍服务会浏览这些节点,以了解视图的相关信息(例如可朗读的内容说明或可对该视图执行的操作)。创建自定义视图时,您可能还需要替换节点信息,以便提供自定义信息来实现无障碍功能。在这种情况下,您将替换节点信息,以表明视图的操作具有自定义信息。

  1. onInitializeAccessibilityNodeInfo() 内,创建一个新的 AccessibilityNodeInfoCompat.AccessibilityActionCompat 对象,并将其分配给 customClick 变量。将 AccessibilityNodeInfo.ACTION_CLICK 常量和一个占位字符串传入构造函数。在收到请求时,导入 AccessibilityNodeInfo
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

AccessibilityActionCompat 类表示出于无障碍功能目的而对视图执行的操作。如您在此处使用的那样,典型的操作是点击或点按,但其他操作可能包括获得或失去焦点、剪贴板操作(剪切/复制/粘贴)或在视图中滚动。此类的构造函数需要一个操作常量(此处为 AccessibilityNodeInfo.ACTION_CLICK)和一个字符串,该字符串供 TalkBack 用于指示操作是什么。

  1. "placeholder" 字符串替换为对 context.getString() 的调用,以检索字符串资源。针对特定资源,测试当前风扇转速。如果当前速度为 FanSpeed.HIGH,则字符串为 "Reset"。如果风扇速度是其他值,则字符串为 "Change."。您将在后续步骤中创建这些字符串资源。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. customClick 定义的右括号之后,使用 addAction() 方法将新的无障碍功能操作添加到节点信息对象。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. res/values/strings.xml 中,添加“更改”和“重置”的字符串资源。
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. 编译并运行应用,并确保 TalkBack 已开启。请注意,现在“点按两次以启动”短语已更改为“点按两次以更改”(如果风扇转速低于高或 3)或“点按两次以重置”(如果风扇转速已达到高或 3)。请注意,“点按两次即可...”提示由 TalkBack 服务本身提供。

下载已完成的 Codelab 的代码。

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


或者,您也可以下载 ZIP 文件形式的代码库,将其解压缩并在 Android Studio 中打开。

下载 Zip 文件

  • 如需创建继承 View 子类(例如 EditText)的外观和行为的自定义视图,请添加一个扩展该子类的新类,并通过替换该子类的某些方法进行调整。
  • 如需创建任意大小和形状的自定义视图,请添加一个扩展 View 的新类。
  • 替换 View 方法(例如 onDraw())以定义视图的形状和基本外观。
  • 使用 invalidate() 强制绘制或重新绘制视图。
  • 为了优化性能,请在 onDraw() 中使用变量之前,先分配变量并为绘制和涂绘分配任何所需的值,例如在成员变量的初始化中。
  • 重写自定义视图的 performClick() 而不是 OnClickListener(),以提供视图的互动行为。这样一来,您或其他可能使用您的自定义视图类的 Android 开发者就可以使用 onClickListener() 来提供更多行为。
  • 将自定义视图添加到 XML 布局文件,并使用属性定义其外观,就像处理其他界面元素一样。
  • values 文件夹中创建 attrs.xml 文件,以定义自定义属性。然后,您可以在 XML 布局文件中为自定义视图使用自定义属性。

Udacity 课程:

Android 开发者文档:

视频:

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

问题 1

如需在首次为自定义视图分配大小时计算位置、尺寸以及其他任何值,您会替换哪个方法?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

问题 2

为了表明您希望使用 onDraw() 重新绘制视图,在属性值发生更改后,您从界面线程调用哪个方法?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

问题 3

您应替换哪个 View 方法才能向自定义视图添加互动功能?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

如需本课程中其他 Codelab 的链接,请参阅“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页。