Edit the Blog Post Header component above this, then place your content here, then fill out the Related Articles section below, if possible. You should have at least one related article and ideally, all three.
Feel free to add supplementary content to the sidebar at right, but please retain the Twitter component (it can live at the bottom of your added content).
This text component can be edited or deleted as necessary.
Related articles only have an image (the squarer version of the banner) and a title. This text can be deleted. Place 1-3 related articles.
Fire OS通过对VoiceView的特定增强扩展了Android的无障碍功能框架。这样,您可以为屏幕阅读器的用户提供更丰富的上下文信息。这里的一个重要技术是使用AccessibilityNodeInfo中的extrasBundle来添加键,例如:
例如:
public void onInitializeAccessibilityNodeInfo(
View host,
AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info
.getExtras()
.putString(
"com.amazon.accessibility.describedBy",
"此行包含热门电影。"
);
info
.getExtras()
.putString(
"com.amazon.accessibility.usageHint.touch", // 适用于Fire平板电脑
"点击并按住可获取选项。双击以选择。"
);
info
.getExtras()
.putString(
"com.amazon.accessibility.usageHint.remote", // 适用于Fire TV
"按左右键以查找项目"
)
});
对于使用React和Jetpack Compose的开发者来说,重要的是要注意,这些框架抽取出了直接操纵AccessibilityNodeInfo的能力。在本例中,您需要遵循这些框架的指导,以实现类似的结果。例如,使用Jetpack Compose时,您可能会在组件上定义语义属性,类似于您在AccessibilityNodeInfo中使用extras的方式。
如果创建自定义用户界面组件,则需要手动实现无障碍功能。例如,假设有一个名为CircularSeekBar的自定义组件。这是一个圆形进度选择器,允许用户在圆形轨道上拖动拇指,以设置0%到100%的值。
视觉设计对于视力正常的用户而言是直观的。但是,依赖VoiceView的视障用户需要其他方式来理解此组件并与之交互。因此,您需要实现几个关键的无障碍功能。
首先,确保可以通过键盘和屏幕阅读器导航到视图。这意味着在类定义中设置isFocusable=true。
class CircularSeekBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var progress = 0
private val maxProgress = 100
init {
isFocusable = true
}
接下来,描述无障碍功能服务的视图。覆盖onInitializeAccessibilityNodeInfo,让VoiceView知道这是一个范围在0-100之间的搜索栏,用户可以向前/向后滚动以调整该值。
override fun onInitializeAccessibilityNodeInfo(
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(info)
// 使用兼容性包装器以获得更好的向后兼容性。
val infoCompat = AccessibilityNodeInfoCompat.wrap(info)
// 将视图标识为无障碍功能服务的SeekBar。
infoCompat.className = "android.widget.SeekBar"
infoCompat.roleDescription = "搜索栏"
infoCompat.contentDescription = "圆形进度选择器"
// 描述视图的范围(最小值、最大值和当前值)。
infoCompat.rangeInfo =
AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_INT,
0f,
maxProgress.toFloat(),
progress.toFloat()
)
// 声明可以在此视图上执行的操作。
// 这使用户能够通过屏幕阅读器手势调整值。
infoCompat.addAction(
AccessibilityNodeInfoCompat
.AccessibilityActionCompat
.ACTION_SCROLL_FORWARD
)
infoCompat.addAction(
AccessibilityNodeInfoCompat
.AccessibilityActionCompat
.ACTION_SCROLL_BACKWARD
)
infoCompat.isScrollable = true
}
通过覆盖performAccessibilityAction为VoiceView用户提供实际功能。例如,允许用户向上或向下滑动以将值加减5,同时提供即时的音频反馈。
override fun performAccessibilityAction(
action: Int,
arguments: Bundle?
): Boolean {
val currentProgress = getProgress()
// 根据操作确定新的进度。
val newProgress = when (action) {
AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> {
(currentProgress + 5).coerceAtMost(getMaxProgress())
}
AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> {
(currentProgress - 5).coerceAtLeast(0)
}
else -> return super.performAccessibilityAction(action, arguments)
}
// 如果进度发生变化,则更新并宣布新值。
if (newProgress != currentProgress
setProgress(newProgress
val accessibilityManager =
context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
if (accessibilityManager.isEnabled) {
val event = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
event.text.add("Progress: $newProgress%")
accessibilityManager.sendAccessibilityEvent(event)
}
return true
}
// 如果未处理该操作,则返回false。
return super.performAccessibilityAction(action, arguments)
}
如果上述方法需要应用于使用遥控器通过方向键进行导航的FireTV应用,那么您可以通过明确触发performAccessibilityAction来包含处理方向键按键按下的代码。例如:
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
performAccessibilityAction(
AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null
)
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
performAccessibilityAction(
AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null
)
}
else -> super.onKeyDown(keyCode, event)
}
}
您还可以添加出现在无障碍功能菜单中的自定义操作。在您的MainActivity中,将AccessibilityDelegateCompat附加到包含自定义组件的容器。
ViewCompat.setAccessibilityDelegate(
seekBarContainer,
object : AccessibilityDelegateCompat() {
/**
* 当无障碍功能服务为视图构建节点信息时,
* 会调用此方法。使用它来添加自定义操作。
*/
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
val actionLabel = "重置进度"
val customAction =
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.action_reset_progress, // 在res/values/ids.xml中定义的ID
actionLabel
)
info.addAction(customAction)
}
/**
* 当用户触发自定义无障碍功能操作时,
* 会调用此方法。
*/
override fun performAccessibilityAction(
host: View,
action: Int,
args: Bundle?
): Boolean {
// 检查执行的操作是否是我们的自定义操作。
if (action == R.id.action_reset_progress) {
// 执行以下操作:将搜索栏的进度重置为0。
circularSeekBar.setProgress(0)
val accessibilityManager =
host.context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
if (accessibilityManager.isEnabled) {
val event = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
event.text.add("进度重置为0%")
accessibilityManager.sendAccessibilityEvent(event)
}
return true // 返回true以指示操作已处理。
}
// 如果这不是我们的操作,则让默认实现处理它。
return super.performAccessibilityAction(host, action, args)
}
}
)
即使视力正常的用户通过触摸进行交互时,也通过宣布最终值来提供反馈。这可以在自定义组件的onTouchEvent定义中完成:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
…
MotionEvent.ACTION_UP -> {
// 当用户松开手指不再触摸时,
// 发生无障碍功能的宣布。
// 这将向屏幕阅读器用户通知最终选择的值。
val accessibilityManager =
context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
if (accessibilityManager.isEnabled) {
val accessibilityEvent = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
accessibilityEvent.text.add("进度设置为$progress%")
accessibilityManager.sendAccessibilityEvent(accessibilityEvent)
}
// 调用performClick了解是否符合无障碍功能标准。
performClick()
return true
}
}
return super.onTouchEvent(event)
}
如果您的用户依赖键盘导航或屏幕阅读器,直观的导航体验至关重要。假设有一个按钮以非线性模式排列的布局。或者,会在界面中有添加不增大功能值的装饰性图像。比如:
如果您不提供适当的焦点管理,您的VoiceView用户将遇到令人困惑的导航模式或不必要的装饰性内容宣布。相反,应当通过实现焦点控制和内容筛选来创建符合逻辑和可预测的导航体验。
为此,您需要在布局中使用android:importantForAccessibility="no"。此操作会要求VoiceView在导航过程中完全跳过装饰性图像。这可以防止用户收听到不利于其理解界面的“Image”(图像)宣布。在您的XML中,进行以下设置:
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@android:drawable/ic_menu_gallery"
android:importantForAccessibility="no"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@null" />
这是另一种布局,其中元素呈非线性排列:
在这种情况下,用逻辑序列覆盖默认的几何导航。在这个实现中,nextFocus*属性是关键。
<!-- Center Button -->
<Button
android:id="@+id/button_center"
android:text="中心r"
android:nextFocusForward="@id/button_right"
android:nextFocusUp="@id/button_left"
android:nextFocusDown="@id/button_right"
android:nextFocusLeft="@id/button_left"
android:nextFocusRight="@id/button_right" />
<!-- Right Button -->
<Button
android:id="@+id/button_right"
android:text="向右"
android:nextFocusForward="@id/button_left"
android:nextFocusUp="@id/button_center"
android:nextFocusDown="@id/button_left"
android:nextFocusLeft="@id/button_center"
android:nextFocusRight="@id/button_left" />
<!-- Left Button -->
<Button
android:id="@+id/button_left"
android:text="向左"
android:nextFocusForward="@id/button_center"
android:nextFocusUp="@id/button_right"
android:nextFocusDown="@id/button_center"
android:nextFocusLeft="@id/button_right"
android:nextFocusRight="@id/button_center" />
应用这种做法可以创建可预测的导航路径,特别是在Fire TV上,用户通过定向导航而非直接触摸进行交互。
视觉设计通常依赖于图标和图像来有效地传达含义。但是,对于视力正常的用户而言显而易见的事情,对于屏幕阅读器用户而言可能并非如此。当VoiceView遇到未标记的按钮或图像时,它可能会宣布“Unlabeled button”(未标记按钮)或直接宣布为“Image”。 这对用户没有帮助,而且会让感到困惑。
或者,假设有一个带“设置”按钮的界面,该图标是一个视力正常的用户普遍认可的齿轮符号。如果没有适当的标签,VoiceView用户在聚焦此元素时可能会听到“Unlabeled button”,不可能理解按钮的功能。该如何弥补这一不足? 为所有传达信息或功能的非文本元素提供文本替代项。
使用android:contentDescription属性来提供明确、描述性的标签。
<ImageButton
android:id="@+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_preferences"
android:contentDescription="设置" />
内容描述应添加到通过视觉设计传达信息的任何元素中:
请牢记,编写有效描述意味着应关注功能而非外观。描述应该回答的问题应当是“这是用来做什么的?”而非“这看起来像什么?”
现代应用经常动态更新内容 - 可能会出现状态消息,或者向列表中添加新项目,或者完成一些后台操作。视力正常的用户可以立即看到这些更改,但屏幕阅读器用户可能会错过这些重要的更新…除非明确宣布了它们。如果没有这些宣布,您的应用就会出现信息缺口,这会让某些用户对应用中发生的事情感到困惑。
假设有一个应用,其中内容可以在不刷新整页的情况下更改,例如在后台完成配置文件更新时。对于AccessibilityEvent.TYPE_ANNOUNCEMENT的使用在这里很关键。
updateButton.setOnClickListener {
// 这里会发生一些后台操作(如保存数据)。
// 使用AccessibilityEvent.TYPE_ANNOUNCEMENT来将结果告知用户,
// 无需更改屏幕上可见的文本。
val accessibilityManager =
context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
if (accessibilityManager.isEnabled) {
val event = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
event.text.add("您的配置文件已更新。")
accessibilityManager.sendAccessibilityEvent(event)
}
如何处理您的应用显示中可能出现新内容的区域? 在这种情况下,将容器指定为实时区域以自动宣布新内容。例如:
<LinearLayout
android:id="@+id/live_region_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:accessibilityLiveRegion="polite" />
当新内容添加到此容器中时,VoiceView会自动宣布它:
addContentItemButton.setOnClickListener {
itemCount++
val newTextView = TextView(this)
newTextView.text = "已添加新项目#$itemCount。"
// 因为对于容器设置为android:accessibilityLiveRegion="polite",
// 一旦添加此新TextView,屏幕阅读器就会自动
// 读取其文本。
container.addView(newTextView)
}
android:accessibilityLiveRegion属性支持不同的宣布行为:
列表是许多应用界面的基础,例如歌曲库、产品目录、联系人列表等等。然而,屏幕阅读器的默认行为可能会导致浏览这些列表时的效率低下。如果VoiceView将列表项中的每个文本元素视为单独的可设定焦点元素,那么用户将需要多次滑动才能浏览单个项目。这很快就会令用户感到沮丧。
假设有一个带有歌曲列表的音乐应用。每个列表项目都包含一首歌曲标题和艺术家姓名,显示为单独的文本元素。
如果没有无障碍设计,VoiceView将要求用户每首歌滑动两次 - 一次针对标题,一次针对艺术家。这种导航体验很糟糕。用户需要记住多个焦点的信息。
通过将相关信息划分在一组,可创建更高效、更合乎逻辑的体验。将每个列表项的根布局设置为可设定焦点,从而将子元素划分在一组。
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:focusable="true"
android:background="?android:attr/selectableItemBackground">
<!--
通过在根布局上设置android:focusable="true",
我们要求VoiceView将整个列表项视为单个
交互元素。这样会将子TextView划分在一组,
以获得更连贯的屏幕阅读器体验。将在适配器中以编程方式设置
contentDescription。
-->
<TextView
android:id="@+id/song_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="歌曲标题"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/song_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="艺术家姓名"
android:textSize="14sp"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/song_title"
app:layout_constraintEnd_toEndOf="parent"
</androidx.constraintlayout.widget.ConstraintLayout>
关键属性android:focusable="true"要求VoiceView将整个列表项目视为单个可设定焦点单位,而非单个子元素。
然后,在列表对象的RecyclerView适配器中,创建一个包含所有相关信息的全面内容描述:
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val song = songs[position]
holder.titleTextView.text = song.title
holder.artistTextView.text = song.artist
// 默认情况下,屏幕阅读器会将标题和艺术家视为
// 两个独立的、可设定焦点的元素。通过将它们组合成父视图上的单个
// contentDescription(在XML中标记为可设定焦点),
// 我们让用户一次听到所有项目的信息。
holder.view.contentDescription =
"歌曲: ${song.title}, 艺术家: ${song.artist}"
}
这种方法将两个单独的宣布(“Bohemian Rhapsody”及其后的“Queen”)转换为一个单一的综合公告(“歌曲: Bohemian Rhapsody,艺术家: Queen”)。
请记住: 在制作contentDescription时使用清晰、可预测的模式。例如:
当您以这种方式进行开发时,列表导航会明显变得更加高效和用户友好。屏幕阅读器用户可以轻松快速地浏览内容。
在应用中实现VoiceView是确保应用可供尽可能多的人使用的最佳方法之一。这对您很重要,对您的用户也很重要。