此 Codelab 是“使用 Kotlin 进行高级 Android 开发”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘课程的价值,但并不强制要求这样做。“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页列出了所有课程 Codelab。
简介
Android 提供大量 View
子类,例如 Button
、TextView
、EditText
、ImageView
、CheckBox
或 RadioButton
。您可以使用这些子类构造一个界面,以便在您的应用中启用用户互动并显示信息。如果所有 View
子类都无法满足您的需求,您可以创建一个 View
子类(称为自定义视图)。
如需创建自定义视图,您可以扩展现有的 View
子类(例如 Button
或 EditText
),也可以创建自己的 View
子类。通过直接扩展 View
,您可以创建任何大小和形状的交互式界面元素,只需替换 View
的 onDraw()
方法即可绘制它。
创建自定义视图后,您可以按照与添加 TextView
或 Button
相同的方式将其添加到您的 activity 布局中。
本课将介绍如何通过扩展 View
从头开始创建自定义视图。
您应当已掌握的内容
- 如何创建具有 Activity 的应用以及如何使用 Android Studio 运行该应用。
学习内容
- 如何扩展
View
以创建自定义视图。 - 如何绘制圆形的自定义视图。
- 如何使用监听器处理用户与自定义视图的互动。
- 如何在布局中使用自定义视图。
您将执行的操作
CustomFanController 应用演示了如何通过扩展 View
类来创建自定义视图子类。新的子类称为 DialView
。
该应用会显示圆形物理元素,其外观类似于实体风扇控件,包括关闭 (0)、低 (1)、中 (2) 和高 (3) 设置。当用户点按该视图时,选择指示器会移至下一个位置:0-1-2-3,然后再回到 0。此外,如果选择的是 1 或更高,视图圆形部分的背景颜色会从灰色变为绿色(表示风扇电源已开启)。
视图是应用界面的基本构建块。View
类提供了许多子类(称为界面微件),可满足典型 Android 应用界面的许多需求。
界面构建块(例如 Button
和 TextView
)是扩展 View
类的子类。为节省时间和开发工作,您可以扩展其中一个 View
子类。自定义视图会继承其父项的外观和行为,并且您可以替换想要更改的外观的行为或方面。例如,如果您扩展 EditText
以创建自定义视图,则该视图的行为与 EditText
视图类似,但也可以进行自定义以显示 X 按钮,该按钮用于清除文本输入字段中的文本。
您可以扩展任何 View
子类(如 EditText
)来获取自定义视图,请选择最接近于您想要完成的类的视图。然后,您可以将自定义视图与一个或多个布局中的任何其他 View
子类一样,用作具有属性的 XML 元素。
如需从头开始创建自己的视图,请扩展 View
类本身。您的代码会替换 View
方法来定义视图的外观和功能。创建自己的自定义视图的关键是,您应负责将任何尺寸和形状的整个界面元素绘制到屏幕上。如果您将现有视图(如 Button
)子类化,该类将为您处理绘制。(稍后您将在此 Codelab 中详细了解绘制。)
要创建自定义视图,请按照以下常规步骤操作:
- 创建用于扩展
View
或扩展View
子类(例如Button
或EditText
)的自定义视图类。 - 如果扩展现有的
View
子类,请仅替换要更改的外观的外观或方面。 - 如果扩展
View
类,请绘制自定义视图的形状,并通过替换新类中的onDraw()
和onMeasure()
等View
方法来控制其外观。 - 添加代码以响应用户互动,并根据需要重新绘制自定义视图。
- 在 activity 的 XML 布局中,使用自定义视图类作为界面微件。您还可以为视图定义自定义属性,以便在不同的布局中提供视图的自定义。
在此任务中,您将:
- 创建
ImageView
作为自定义视图的临时占位符的应用。 - 扩展
View
以创建自定义视图。 - 使用绘制和绘制值初始化自定义视图。
第 1 步:使用 ImageView 占位符创建应用
- 使用 Empty Activity 模板创建标题为
CustomFanController
的 Kotlin 应用。确保软件包名称为com.example.android.customfancontroller
。 - 在 Text 标签页中打开
activity_main.xml
,以修改 XML 代码。 - 将现有的
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"/>
- 将此
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"/>
- 提取这两个界面元素中的字符串和维度资源。
- 点击 Design 标签页。布局应如下所示:
第 2 步:创建自定义视图类
- 新建一个名为
DialView
的 Kotlin 类。 - 修改类定义以扩展
View
。出现提示时,导入android.view.View
。 - 点击
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) {
- 在
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);
}
- 在
enum
下方,添加以下常量。您在绘制表盘指示器和标签时会用到这些图标。
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35
- 在
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 点,用于在屏幕上绘制多个视图的元素。
此处创建和初始化这些值而不是视图的实际绘制时间,以确保实际绘制步骤尽快运行。
- 同样,在
DialView
类定义内,使用一些基本样式初始化Paint
对象。在收到请求时,导入android.graphics.Paint
和android.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)
}
- 打开
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()
中进行分配,因为分配可能会导致垃圾回收,从而造成视觉卡顿。
Canvas
和 Paint
类提供了许多有用的绘图快捷方式:
- 使用
drawText()
绘制文本。通过调用setTypeface()
指定字体,并通过调用setColor()
指定文本颜色。 - 使用
drawRect()
、drawOval()
和drawArc()
绘制基元形状。通过调用setStyle()
更改形状的填充和/或轮廓。 - 使用
drawBitmap()
绘制位图。
在后续 Codelab 中,您将详细了解 Canvas
和 Paint
。如需详细了解 Android 如何绘制视图,请参阅 Android 如何绘制视图。
在此任务中,您将使用 onSizeChanged()
和 onDraw()
方法将风扇控制器自定义视图绘制到屏幕上(旋钮本身、当前位置指示器和指示器标签)。您还将创建一个帮助程序方法 computeXYForSpeed(),
,以计算指示符在刻度条上的当前 X,Y 位置。
第 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()
}
- 在
onSizeChanged()
下添加以下代码,为PointF
类定义computeXYForSpeed()
扩展函数。在收到请求时,导入kotlin.math.cos
和kotlin.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
}
- 替换
onDraw()
方法以使用Canvas
和Paint
类在屏幕上呈现视图。在收到请求时,导入android.graphics.Canvas
。以下是框架替换项:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
- 在
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
- 添加以下代码以使用
drawCircle()
方法为表盘绘制一个圆形。此方法使用当前视图的宽度和高度查找圆的中心、圆的半径和当前绘制颜色。width
和height
属性是View
父类的成员,用于指明视图的当前尺寸。
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
- 添加以下代码,再使用
drawCircle()
方法为风扇速度指示器标记绘制一个较小的圆圈。该部分使用PointF
。computeXYforSpeed()
扩展方法,用于根据当前风扇速度计算指示灯中心的 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)
- 最后,在风箱周围的适当位置绘制风扇转速标签(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 元素属性来控制其外观和行为,就像其他任何界面元素一样。
- 在
activity_main.xml
中,将dialView
的ImageView
标记更改为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" />
- 运行应用。您的风扇控制视图显示在 Activity 中。
最后一个任务是支持自定义视图在用户点按视图时执行操作。每次点按时,应将选择标志移至下一个位置:关闭 1-2-3,然后再移回关闭。同样,如果选择 1 或更高,请将背景从灰色变为绿色,表示风扇电源已开启。
要让您的自定义视图可供点击,请执行以下操作:
- 将视图的
isClickable
属性设置为true
。这样一来,您的自定义视图即可响应点击。 - 实现
View
类的performClick
()
,以便在用户点击视图时执行操作。 - 调用
invalidate()
方法。这将告知 Android 系统调用onDraw()
方法来重新绘制视图。
通常,对于标准 Android 视图,您可以实现 OnClickListener()
以在用户点击该视图时执行操作。对于自定义视图,您需要实现 View
类的 performClick
()
方法,然后调用 super
。performClick().
默认的 performClick()
方法还会调用 onClickListener()
,因此您可以将操作添加到 performClick()
中,并将 onClickListener()
提供给您或可能使用您的自定义视图的其他开发者进一步自定义。
- 在
DialView.kt
中的FanSpeed
枚举内,添加一个扩展函数next()
,它会将当前风扇速度更改为列表中的下一个速度(从OFF
更改为LOW
、MEDIUM
和HIGH
,然后返回到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
}
}
- 在
DialView
类中的onSizeChanged()
方法之前,添加一个init()
块。将视图的isClickable
属性设为 true 即可让该视图接受用户输入。
init {
isClickable = true
}
- 在
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().
- 运行应用。点按
DialView
元素,将指示标志从关闭切换为 1。刻度盘应变为绿色。每次点按时,指示符都应移至下一个位置。指示标志重新关闭时,刻度盘应再次变为灰色。
此示例介绍了在自定义视图中使用自定义属性的基本机制。您可以为 DialView
类定义每个风扇拨动位置颜色不同的自定义属性。
- 创建并打开
res/values/attrs.xml
。 - 在
<resources>
中,添加<declare-styleable>
资源元素。 - 在
<declare-styleable>
资源元素内,添加三个attr
元素,每个属性各有一个name
和format
。format
就是一种类型,在本例中是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>
- 打开
activity_main.xml
布局文件。 - 在
DialView
中,为fanColor1
、fanColor2
和fanColor3
添加属性,并将其值设为如下所示的颜色。请将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
中的属性,并将属性值分配给局部变量进行缓存。
- 打开
DialView.kt
类文件。 - 在
DialView
内,声明变量以缓存属性值。
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
- 在
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)
}
- 根据
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
- 运行应用,点击刻度盘,每个位置的颜色设置应该不同,如下所示。
如需详细了解自定义视图属性,请参阅创建视图类。
无障碍是指一组设计、实现和测试技术,可让您的应用(包括残障人士)可供所有人使用。
可能会影响人们使用 Android 设备的常见残疾包括失明、弱视、色盲、失聪或听力丧失,以及运动技能受限。如果您能够在开发应用时考虑无障碍功能,那么对于残障用户以及所有其他用户,您都能获得更出色的用户体验。
默认情况下,Android 在标准界面视图中提供多项无障碍功能,例如 TextView
和 Button
。不过,在创建自定义视图时,您需要考虑自定义视图如何提供无障碍功能,例如屏幕上内容的语音描述。
在此任务中,您将了解 TalkBack、Android 屏幕阅读器以及如何修改应用,使其包含针对 DialView
自定义视图的语音提示和说明。
第 1 步:探索 TalkBack
TalkBack 是 Android 的内置屏幕阅读器。启用 TalkBack 后,用户无需查看屏幕即可与其 Android 设备互动,因为 Android 会大声描述屏幕元素。视障用户可能需要使用 TalkBack 才能使用您的应用。
在此任务中,您将启用 TalkBack,以了解屏幕阅读器的工作原理以及如何在应用中导航。
- 在 Android 设备或模拟器上,依次转到设置 > 无障碍 > TalkBack。
- 点按开启/关闭切换按钮以开启 TalkBack。
- 点按确定以确认权限。
- 在系统提示时,确认您的设备密码。如果这是您首次运行 TalkBack,系统会启动教程。(本教程可能不适用于旧版设备。)
- 闭上双眼找到教程或许有所帮助。如果以后想再次打开该教程,请依次转到设置 >;无障碍 > TalkBack >;设置 >;启动 TalkBack 教程。
- 编译并运行
CustomFanController
应用,或使用设备上的概览或最近用过按钮打开该应用。请注意,当 TalkBack 开启时,系统会读出应用的名称以及TextView
标签的文本(“风扇控制”)。然而,如果您点按DialView
视图本身,则不会得知该视图的状态(表盘的当前设置)或点按该视图以将其激活时将执行的操作。
第 2 步:为拨号标签添加内容说明
内容说明说明了应用中视图的含义和用途。这些标签可让屏幕阅读器(例如 Android 的 TalkBack)功能准确说明每个元素的功能。对于静态视图(例如 ImageView
),您可以使用 contentDescription
属性向布局文件中的视图添加内容说明。文本视图(TextView
和 EditText
)会自动使用视图中的文本作为内容说明。
对于自定义风扇控制视图,您需要在每次点击该视图时动态更新内容说明,以指明当前风扇设置。
- 在
DialView
类的底部,声明一个不带参数或返回类型的函数updateContentDescription()
。
fun updateContentDescription() {
}
- 在
updateContentDescription()
内,将自定义视图的contentDescription
属性更改为与当前风扇速度相关联的字符串资源(关闭、1、2 或 3)。在屏幕上绘制转盘时,这些标签与onDraw()
中使用的标签相同。
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- 向上滚动到
init()
代码块,并在该代码块末尾添加对updateContentDescription()
的调用。这将在视图初始化时初始化内容说明。
init {
isClickable = true
// ...
updateContentDescription()
}
- 在
performClick()
方法中,在invalidate()
之前添加对updateContentDescription()
的另一个调用。
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- 编译并运行应用,并确保 TalkBack 已开启。点按可更改拨号视图的设置,然后您会发现 TalkBack 读出当前标签(关闭、1、2、3)以及短语“Double tap to activate.”
第 3 步:添加有关点击操作的更多信息
您可以在这里停止操作,在 TalkBack 中使用视图。不过,如果您的视图不仅可指示该视图可以激活(“点按两次可激活”)外,还会说明它在视图激活时会发生什么情况(“点按两次可更改”或“点按两次可重置”)。
为此,您可以通过无障碍代理将有关视图操作(此处为点击或点按操作)的信息添加到无障碍节点信息对象中。无障碍委托可让您通过组合(而不是继承)自定义与应用相关的无障碍功能。
对于此任务,您将使用 Android Jetpack 库 (androidx.*
) 中的无障碍功能类,以确保向后兼容性。
- 在
DialView.kt
中的init
代码块中,将视图上的无障碍代理设置为新的AccessibilityDelegateCompat
对象。在收到请求时,导入androidx.core.view.ViewCompat
和androidx.core.view.AccessibilityDelegateCompat
。此策略可最大程度地向后兼容您的应用。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
- 在
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 的无障碍服务会在这些节点中导航,以便查找有关视图的信息(例如,可朗读的内容说明或可对该视图执行的可能操作)。在创建自定义视图时,您可能还需要替换节点信息以提供可访问性的自定义信息。在这种情况下,您将替换节点信息,以指明视图操作具有自定义信息。
- 在
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 操作的字符串。
- 将
"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)
)
}
})
- 在
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)
}
})
- 在
res/values/strings.xml
中,为“更改”和“重置”添加字符串资源。
<string name="change">Change</string>
<string name="reset">Reset</string>
- 编译并运行应用,并确保 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 中打开它。
- 如需创建继承
View
子类(例如EditText
)外观和行为的自定义视图,请添加用于扩展该子类的新类,并通过替换子类的某些方法进行调整。 - 要创建任何大小和形状的自定义视图,请添加一个用于扩展
View
的新类。 - 替换
onDraw()
等View
方法,以定义视图的形状和基本外观。 - 使用
invalidate()
强制绘制或重新绘制视图。 - 为了优化性能,请先分配变量并分配绘图和绘制所需的任何值,然后再在
onDraw()
中使用这些变量和值,例如在成员变量的初始化中。 - 将
performClick()
(而非OnClickListener
())替换到自定义视图,以提供视图的交互行为。这样一来,您或其他可能使用您的自定义视图类的 Android 开发者便可以使用onClickListener()
提供进一步的行为。 - 像定义其他界面元素一样,将自定义视图添加到具有属性来定义其 XML 布局文件的 XML 布局文件中。
- 在
values
文件夹中创建attrs.xml
文件来定义自定义属性。然后,您可以在 XML 布局文件中为自定义视图使用自定义属性。
Udacity 课程:
Android 开发者文档:
- 创建自定义视图
@JvmOverloads
- 自定义组件
- Android 如何绘制视图
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- 查看
- 输入事件
- 绘制
- Kotlin 扩展库 android-ktx
withStyledAttributes
- Android KTX 文档
- Android KTX 原始公告博客
- 让自定义视图使用起来更没有障碍
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
视频:
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
问题 1
要在首次为自定义视图分配尺寸时计算位置、尺寸和其他任何值,您会替换哪种方法?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
问题 2
为了表明您希望使用 onDraw()
重新绘制视图,在属性值发生更改后,您应从界面线程调用什么方法?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
问题 3
要为自定义视图添加互动功能,您应该替换哪种 View
方法?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
如需查看本课程中其他 Codelab 的链接,请参阅“使用 Kotlin 进行高级 Android 开发”的 Codelab 着陆页。