创建自定义视图

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

简介

Android 提供大量 View 子类,例如 ButtonTextViewEditTextImageViewCheckBoxRadioButton。您可以使用这些子类构造一个界面,以便在您的应用中启用用户互动并显示信息。如果所有 View 子类都无法满足您的需求,您可以创建一个 View 子类(称为自定义视图)。

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

创建自定义视图后,您可以按照与添加 TextViewButton 相同的方式将其添加到您的 activity 布局中。

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

您应当已掌握的内容

  • 如何创建具有 Activity 的应用以及如何使用 Android Studio 运行该应用。

学习内容

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

您将执行的操作

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

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

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

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

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

您可以扩展任何 View 子类(如 EditText)来获取自定义视图,请选择最接近于您想要完成的类的视图。然后,您可以将自定义视图与一个或多个布局中的任何其他 View 子类一样,用作具有属性的 XML 元素。

如需从头开始创建自己的视图,请扩展 View 类本身。您的代码会替换 View 方法来定义视图的外观和功能。创建自己的自定义视图的关键是,您应负责将任何尺寸和形状的整个界面元素绘制到屏幕上。如果您将现有视图(如 Button)子类化,该类将为您处理绘制。(稍后您将在此 Codelab 中详细了解绘制。)

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

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

在此任务中,您将:

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

第 1 步:使用 ImageView 占位符创建应用

  1. 使用 Empty Activity 模板创建标题为 CustomFanController 的 Kotlin 应用。确保软件包名称为 com.example.android.customfancontroller
  2. Text 标签页中打开 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. 点击 Design 标签页。布局应如下所示:

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

  1. 新建一个名为 DialView 的 Kotlin 类。
  2. 修改类定义以扩展 View。出现提示时,导入 android.view.View
  3. 点击 View,然后点击红色灯泡。选择 Add Android View builders 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() 方法,计算视图首次出现时以及每次视图大小发生变化时的大小。
  • 使用通过 Paint 对象设置样式的 Canvas 对象替换 onDraw() 方法以绘制自定义视图。
  • 在响应在用户点击视图(更改视图的绘制方式)时调用 invalidate() 方法,从而强制调用 onDraw() 重新绘制视图。

每次屏幕刷新(可能每秒多次)时,系统会调用 onDraw() 方法。考虑到性能原因并避免出现视觉干扰,您应在 onDraw() 中执行尽可能少的工作。特别是,不要在 onDraw() 中进行分配,因为分配可能会导致垃圾回收,从而造成视觉卡顿。

CanvasPaint 类提供了许多有用的绘图快捷方式:

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

在此任务中,您将使用 onSizeChanged()onDraw() 方法将风扇控制器自定义视图绘制到屏幕上(旋钮本身、当前位置指示器和指示器标签)。您还将创建一个帮助程序方法 computeXYForSpeed(),,以计算指示符在刻度条上的当前 X,Y 位置。

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

  1. DialView 类中的初始化下方,替换 View 类中的 onSizeChanged() 方法,以计算自定义视图表盘的大小。导入 kotlinmath.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。根据表盘的当前 FanSpeed 位置和半径,该 PointF 类上的此扩展函数计算文本标签和当前指示符(0、1、2 或 3)在屏幕上的 X、Y 坐标。您将在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
}

调用 superperformClick() 必须先发生,这样才能启用无障碍事件以及调用 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。不过,在创建自定义视图时,您需要考虑自定义视图如何提供无障碍功能,例如屏幕上内容的语音描述。

在此任务中,您将了解 TalkBack、Android 屏幕阅读器以及如何修改应用,使其包含针对 DialView 自定义视图的语音提示和说明。

第 1 步:探索 TalkBack

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

在此任务中,您将启用 TalkBack,以了解屏幕阅读器的工作原理以及如何在应用中导航。

  1. 在 Android 设备或模拟器上,依次转到设置 > 无障碍 > TalkBack
  2. 点按开启/关闭切换按钮以开启 TalkBack。
  3. 点按确定以确认权限。
  4. 在系统提示时,确认您的设备密码。如果这是您首次运行 TalkBack,系统会启动教程。(本教程可能不适用于旧版设备。)
  5. 闭上双眼找到教程或许有所帮助。如果以后想再次打开该教程,请依次转到设置 &gt;无障碍 > TalkBack &gt;设置 &gt;启动 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)以及短语“Double tap to activate.”

第 3 步:添加有关点击操作的更多信息

您可以在这里停止操作,在 TalkBack 中使用视图。不过,如果您的视图不仅可指示该视图可以激活(“点按两次可激活”)外,还会说明它在视图激活时会发生什么情况(“点按两次可更改”或“点按两次可重置”)。

为此,您可以通过无障碍代理将有关视图操作(此处为点击或点按操作)的信息添加到无障碍节点信息对象中。无障碍委托可让您通过组合(而不是继承)自定义与应用相关的无障碍功能。

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

  1. DialView.kt 中的 init 代码块中,将视图上的无障碍代理设置为新的 AccessibilityDelegateCompat 对象。在收到请求时,导入 androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat。此策略可最大程度地向后兼容您的应用。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. AccessibilityDelegateCompat 对象内,使用 AccessibilityNodeInfoCompat 对象替换 onInitializeAccessibilityNodeInfo() 函数,并调用 super's 方法。出现提示时,导入 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." You' you will create these string resource in a later step.
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 已开启。现在,请注意,“Double tap to activate”现更名为“Double tap to change”(如果风扇速度小于或高于 3)或“Double tap to reset”(如果风扇速度已经高于或高于 3)。请注意,TalkBack 服务本身会提供提示“Double to tap to...”。

下载已完成的 Codelab 的代码。

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


另一种方法是,以 Zip 文件的形式下载代码库,将其解压缩,然后在 Android Studio 中打开它。

下载 Zip 文件

  • 如需创建继承 View 子类(例如 EditText)外观和行为的自定义视图,请添加用于扩展该子类的新类,并通过替换子类的某些方法进行调整。
  • 要创建任何大小和形状的自定义视图,请添加一个用于扩展 View 的新类。
  • 替换 onDraw()View 方法,以定义视图的形状和基本外观。
  • 使用 invalidate() 强制绘制或重新绘制视图。
  • 为了优化性能,请先分配变量并分配绘图和绘制所需的任何值,然后再在 onDraw() 中使用这些变量和值,例如在成员变量的初始化中。
  • performClick()(而非 OnClickListener())替换到自定义视图,以提供视图的交互行为。这样一来,您或其他可能使用您的自定义视图类的 Android 开发者便可以使用 onClickListener() 提供进一步的行为。
  • 像定义其他界面元素一样,将自定义视图添加到具有属性来定义其 XML 布局文件的 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 着陆页。