文章目录
-
- 一、无障碍事件核心概念
-
- [1.1 无障碍服务工作原理](#1.1 无障碍服务工作原理)
- [1.2 三大无障碍事件 API 总览](#1.2 三大无障碍事件 API 总览)
- [二、onAccessibilityFocus 焦点监听](#二、onAccessibilityFocus 焦点监听)
-
- [2.1 API 定义](#2.1 API 定义)
- [2.2 基础用法](#2.2 基础用法)
- [2.3 焦点高亮反馈](#2.3 焦点高亮反馈)
- [三、onAccessibilityActionIntercept 动作拦截](#三、onAccessibilityActionIntercept 动作拦截)
-
- [3.1 API 定义](#3.1 API 定义)
- [3.2 AccessibilityAction 枚举](#3.2 AccessibilityAction 枚举)
- [3.3 AccessibilityActionInterceptResult 枚举](#3.3 AccessibilityActionInterceptResult 枚举)
- [3.4 基础拦截示例](#3.4 基础拦截示例)
- 四、无障碍属性速查
-
- [4.1 核心无障碍属性汇总](#4.1 核心无障碍属性汇总)
- [4.2 accessibilityLevel 枚举说明](#4.2 accessibilityLevel 枚举说明)
- 五、可运行完整示例:无障碍焦点与动作拦截演示
- 六、实战场景:无障碍属性完整配置
-
- [6.1 accessibilityVirtualNode ------ Canvas 虚拟节点](#6.1 accessibilityVirtualNode —— Canvas 虚拟节点)
- [6.2 accessibilityGroup ------ 分组聚焦](#6.2 accessibilityGroup —— 分组聚焦)
- 七、综合实战:无障碍友好的登录表单
-
- [7.1 功能说明](#7.1 功能说明)
- [7.2 完整可运行代码](#7.2 完整可运行代码)
- 总结
一、无障碍事件核心概念
1.1 无障碍服务工作原理
用户(视障/辅助需求)
↓
辅助工具(屏幕朗读器 TalkBack/无障碍扫描)
↓
遍历 UI 树,读取节点的无障碍属性
↓(accessibilityText / accessibilityDescription / accessibilityLevel)
组件节点向辅助工具暴露语义信息
↓
用户通过辅助手势触发无障碍操作(点击/焦点移动等)
← 此处 onAccessibilityFocus 触发(焦点变化)
← 此处 onAccessibilityActionIntercept 触发(动作拦截)
↓
决策:继续执行 / 拦截 / 向父传递
1.2 三大无障碍事件 API 总览
| API | 触发时机 | API 版本 | 核心用途 |
|---|---|---|---|
onAccessibilityFocus |
组件获焦或失焦时 | 18+ | 监听焦点变化,更新 UI 状态、触发朗读提示 |
onAccessibilityActionIntercept |
无障碍操作触发前 | 20+ | 拦截点击等动作,实现二次确认或自定义逻辑 |
无障碍属性(accessibilityText 等) |
渲染/树构建时 | 10+ | 为组件提供语义化描述,供辅助工具朗读 |
二、onAccessibilityFocus 焦点监听
2.1 API 定义
typescript
// 接口签名(API 18+)
onAccessibilityFocus(callback: AccessibilityFocusCallback): T
// 回调类型定义
type AccessibilityFocusCallback = (isFocus: boolean) => void
| 参数 | 类型 | 说明 |
|---|---|---|
callback |
AccessibilityFocusCallback |
无障碍焦点变化回调 |
isFocus |
boolean |
true = 获得焦点;false = 失去焦点 |
| 返回值 | 当前组件实例 T |
支持链式调用 |
2.2 基础用法
typescript
// 监听 Button 的无障碍焦点变化
Button('提交表单')
.height(48)
.borderRadius(10)
.backgroundColor('#1B5E20')
.fontColor(Color.White)
.accessibilityText('提交表单按钮')
.accessibilityDescription('点击后将提交当前填写的表单数据')
.onAccessibilityFocus((isFocus: boolean) => {
if (isFocus) {
console.info('[无障碍] 提交按钮获得焦点,屏幕朗读器将朗读按钮信息')
} else {
console.info('[无障碍] 提交按钮失去焦点')
}
})
.onClick(() => {
console.info('表单提交')
})
2.3 焦点高亮反馈
实际开发中,常借助 onAccessibilityFocus 为获焦组件增加视觉高亮,帮助低视力用户感知当前焦点位置:
typescript
@Component
struct AccessibleCard {
@State isFocused: boolean = false
label: string = '操作项'
description: string = ''
build() {
Row({ space: 12 }) {
Text('🔷').fontSize(18)
Column({ space: 4 }) {
Text(this.label).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
if (this.description) {
Text(this.description).fontSize(11).fontColor('#888888')
}
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('›').fontSize(18).fontColor('#BDBDBD')
}
.width('100%').height(64).padding({ left: 16, right: 16 })
.backgroundColor(this.isFocused ? '#E8F5E9' : Color.White)
.border({
width: this.isFocused ? 2 : 1,
color: this.isFocused ? '#2E7D32' : '#F0F0F0',
style: BorderStyle.Solid
})
.borderRadius(12)
.shadow(this.isFocused ? { radius: 8, color: '#402E7D32', offsetX: 0, offsetY: 2 } : { radius: 0, color: Color.Transparent, offsetX: 0, offsetY: 0 })
.animation({ duration: 200, curve: Curve.EaseOut })
.accessibilityText(this.label)
.accessibilityDescription(this.description || `${this.label},点击以执行操作`)
.accessibilityLevel('yes')
.onAccessibilityFocus((isFocus: boolean) => {
this.isFocused = isFocus
console.info(`[无障碍焦点] ${this.label}: isFocus=${isFocus}`)
})
}
}
三、onAccessibilityActionIntercept 动作拦截
3.1 API 定义
typescript
// 接口签名(API 20+)
onAccessibilityActionIntercept(callback: AccessibilityActionInterceptCallback): T
// 回调类型定义
type AccessibilityActionInterceptCallback =
(action: AccessibilityAction) => AccessibilityActionInterceptResult
| 参数/返回 | 类型 | 说明 |
|---|---|---|
action |
AccessibilityAction |
当前触发的无障碍动作枚举 |
| 返回值(回调) | AccessibilityActionInterceptResult |
告知框架如何处理此动作 |
3.2 AccessibilityAction 枚举
| 枚举值 | 数值 | 说明 |
|---|---|---|
AccessibilityAction.UNDEFINED_ACTION |
0 | 未定义动作 |
AccessibilityAction.ACCESSIBILITY_CLICK |
1 | 无障碍点击(辅助工具触发的点击) |
3.3 AccessibilityActionInterceptResult 枚举
| 枚举值 | 数值 | 说明 | 是否执行组件自身逻辑 | 是否向父传递 |
|---|---|---|---|---|
ACTION_INTERCEPT |
0 | 完全拦截:不执行组件自身逻辑,不向父传递 | ❌ | ❌ |
ACTION_CONTINUE |
1 | 拦截后继续:执行自定义逻辑后,仍执行组件自身逻辑 | ✅ | ❌ |
ACTION_RISE |
2 | 向父传递:不执行自身逻辑,将事件向父组件传递 | ❌ | ✅ |
三种结果选型建议 :需要弹出确认框再决定是否执行 →
ACTION_INTERCEPT;需要额外埋点但仍正常执行 →ACTION_CONTINUE;需要父组件统一处理 →ACTION_RISE。
3.4 基础拦截示例
typescript
// 危险操作二次确认:无障碍点击触发确认弹窗
@Component
struct DangerButton {
@State isOn: boolean = false
build() {
Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.selectedColor('#D32F2F')
.accessibilityText(this.isOn ? '开关已开启' : '开关已关闭')
.accessibilityDescription('该操作将影响系统设置,开启前需要确认')
.onAccessibilityActionIntercept((action: AccessibilityAction) => {
if (action === AccessibilityAction.ACCESSIBILITY_CLICK) {
// 弹出确认对话框,拦截系统默认点击
AlertDialog.show({
title: '无障碍操作确认',
message: `当前开关为【${this.isOn ? '开启' : '关闭'}】状态,确认要切换吗?`,
primaryButton: {
value: '确认切换',
fontColor: '#D32F2F',
action: () => {
this.isOn = !this.isOn
console.info(`[无障碍] 开关已切换为 ${this.isOn}`)
}
},
secondaryButton: {
value: '取消',
action: () => {
console.info('[无障碍] 用户取消切换')
}
}
})
// 拦截系统默认点击,由弹窗决定后续逻辑
return AccessibilityActionInterceptResult.ACTION_INTERCEPT
}
return AccessibilityActionInterceptResult.ACTION_CONTINUE
})
}
}
四、无障碍属性速查
4.1 核心无障碍属性汇总
| 属性 | 类型 | API 版本 | 说明 |
|---|---|---|---|
accessibilityText(value) |
`string | Resource` | 10+ |
accessibilityDescription(value) |
`string | Resource` | 10+ |
accessibilityLevel(value) |
`"auto" | "yes" | "no" |
accessibilityGroup(value) |
boolean |
10+ | 将子组件合并为一个可聚焦单元 |
accessibilityVirtualNode(builder) |
CustomBuilder |
11+ | 为自绘组件(Canvas)提供虚拟无障碍节点 |
4.2 accessibilityLevel 枚举说明
| 值 | 说明 | 典型场景 |
|---|---|---|
"auto" |
系统综合判断(默认) | 大多数普通组件 |
"yes" |
明确可被无障碍识别 | 重要按钮、表单控件 |
"no" |
不被识别(自身),子组件不受影响 | 装饰性组件(背景色块等) |
"no-hide-descendants" |
自身及所有后代均不可识别 | 广告区、装饰性图形区 |
typescript
// 装饰性分割线:对无障碍服务隐藏,不占用朗读焦点
Divider()
.color('#F0F0F0')
.accessibilityLevel('no')
// 重要按钮:明确标记可识别,并提供完整语义信息
Button('立即购买')
.backgroundColor('#E53935')
.fontColor(Color.White)
.accessibilityLevel('yes')
.accessibilityText('立即购买按钮')
.accessibilityDescription('点击后跳转到支付页面,请确认已选择商品规格')
五、可运行完整示例:无障碍焦点与动作拦截演示
以下示例演示 onAccessibilityFocus 与 onAccessibilityActionIntercept 的实际效果,并实时记录事件日志:
typescript
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct AccessibilityEventDemo {
@State focusLog: string[] = ['等待无障碍焦点事件...']
@State actionLog: string[] = ['等待无障碍动作事件...']
@State switchState: boolean = false
@State focusedItem: string = '无'
@State interceptCount: number = 0
@State continueCount: number = 0
private appendFocusLog(msg: string): void {
this.focusLog = [msg, ...this.focusLog.slice(0, 7)]
}
private appendActionLog(msg: string): void {
this.actionLog = [msg, ...this.actionLog.slice(0, 7)]
}
build() {
Column({ space: 0 }) {
// 标题栏
Row({ space: 10 }) {
Text('♿').fontSize(22)
Text('无障碍事件演示').fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White)
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 14 })
.backgroundColor('#1B5E20').justifyContent(FlexAlign.Start)
Scroll() {
Column({ space: 16 }) {
// ── 区域1:焦点监听 ──────────────────────────────
Column({ space: 10 }) {
Text('① onAccessibilityFocus 焦点监听').fontSize(13)
.fontWeight(FontWeight.Bold).fontColor('#1B5E20').width('100%')
Text('当前焦点组件:').fontSize(11).fontColor('#9E9E9E').width('100%')
Text(this.focusedItem).fontSize(13).fontColor('#1B5E20').fontWeight(FontWeight.Bold)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('#E8F5E9').borderRadius(8).width('100%')
// 三个可聚焦的卡片
ForEach(['搜索框', '筛选按钮', '排序选项'], (label: string) => {
Row({ space: 12 }) {
Text(label === '搜索框' ? '🔍' : label === '筛选按钮' ? '⚙️' : '↕️').fontSize(18)
Text(label).fontSize(14).fontColor('#333333').layoutWeight(1)
Text(this.focusedItem === label ? '▶ 焦点在此' : '')
.fontSize(10).fontColor('#2E7D32').fontWeight(FontWeight.Bold)
}
.width('100%').height(52).padding({ left: 14, right: 14 })
.backgroundColor(this.focusedItem === label ? '#E8F5E9' : Color.White)
.borderRadius(10)
.border({
width: this.focusedItem === label ? 2 : 1,
color: this.focusedItem === label ? '#2E7D32' : '#EEEEEE',
style: BorderStyle.Solid
})
.animation({ duration: 180, curve: Curve.EaseOut })
.accessibilityLevel('yes')
.accessibilityText(label)
.accessibilityDescription(`${label},点击以执行${label}操作`)
.onAccessibilityFocus((isFocus: boolean) => {
if (isFocus) {
this.focusedItem = label
this.appendFocusLog(`✅ [获焦] ${label}`)
} else {
if (this.focusedItem === label) this.focusedItem = '无'
this.appendFocusLog(`○ [失焦] ${label}`)
}
})
.onClick(() => {
this.appendFocusLog(`点击 ${label}`)
})
})
// 焦点日志
Column({ space: 3 }) {
Text('焦点事件日志').fontSize(11).fontColor('#9E9E9E').width('100%')
ForEach(this.focusLog, (item: string, idx: number) => {
Text(item).fontSize(10).fontColor(idx === 0 ? '#1B5E20' : '#BDBDBD').width('100%')
})
}
.padding(10).backgroundColor('#F9F9F9').borderRadius(10)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start).width('100%')
}
.width('90%').padding(16).backgroundColor(Color.White).borderRadius(16)
.border({ width: 1, color: '#E8F5E9', style: BorderStyle.Solid })
// ── 区域2:动作拦截 ──────────────────────────────
Column({ space: 10 }) {
Text('② onAccessibilityActionIntercept 动作拦截').fontSize(13)
.fontWeight(FontWeight.Bold).fontColor('#B71C1C').width('100%')
Row({ space: 16 }) {
Column({ space: 4 }) {
Text(`${this.interceptCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#D32F2F')
Text('拦截次数').fontSize(10).fontColor('#9E9E9E')
}
Column().width(1).height(36).backgroundColor('#EEEEEE')
Column({ space: 4 }) {
Text(`${this.continueCount}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
Text('放行次数').fontSize(10).fontColor('#9E9E9E')
}
Column().width(1).height(36).backgroundColor('#EEEEEE')
Column({ space: 4 }) {
Text(this.switchState ? '开' : '关').fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(this.switchState ? '#1B5E20' : '#9E9E9E')
Text('开关状态').fontSize(10).fontColor('#9E9E9E')
}
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly).padding({ top: 8, bottom: 8 })
.backgroundColor('#FFF5F5').borderRadius(10)
// Switch:无障碍点击拦截,弹确认框
Row({ space: 14 }) {
Column({ space: 4 }) {
Text('高风险开关').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333333')
Text('无障碍点击将弹出确认框').fontSize(10).fontColor('#888888')
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Toggle({ type: ToggleType.Switch, isOn: this.switchState })
.selectedColor('#D32F2F')
.width(52).height(28)
.accessibilityText(this.switchState ? '高风险开关:已开启' : '高风险开关:已关闭')
.accessibilityDescription('开启此开关将影响重要系统配置,请谨慎操作')
.onAccessibilityActionIntercept((action: AccessibilityAction) => {
if (action === AccessibilityAction.ACCESSIBILITY_CLICK) {
this.interceptCount++
this.appendActionLog(`🛑 [拦截] ACCESSIBILITY_CLICK → 弹确认框`)
AlertDialog.show({
title: '⚠️ 无障碍操作确认',
message: `开关当前为【${this.switchState ? '开启' : '关闭'}】\n确认要切换状态吗?此操作影响系统配置。`,
primaryButton: {
value: '确认切换',
fontColor: '#D32F2F',
action: () => {
this.switchState = !this.switchState
this.appendActionLog(`✅ 确认切换 → 开关=${this.switchState}`)
}
},
secondaryButton: {
value: '取消',
action: () => {
this.appendActionLog('○ 取消操作')
}
}
})
return AccessibilityActionInterceptResult.ACTION_INTERCEPT
}
return AccessibilityActionInterceptResult.ACTION_CONTINUE
})
.onChange((isOn: boolean) => {
// 普通手动点击不经过拦截,直接触发 onChange
this.switchState = isOn
this.continueCount++
this.appendActionLog(`🟢 [放行] onChange → 开关=${isOn}`)
})
}
.width('100%').padding(14).backgroundColor(Color.White).borderRadius(12)
.border({ width: 1, color: '#FFCDD2', style: BorderStyle.Solid })
// 动作日志
Column({ space: 3 }) {
Text('动作拦截日志').fontSize(11).fontColor('#9E9E9E').width('100%')
ForEach(this.actionLog, (item: string, idx: number) => {
Text(item).fontSize(10).fontColor(idx === 0 ? '#D32F2F' : '#BDBDBD').width('100%')
})
}
.padding(10).backgroundColor('#F9F9F9').borderRadius(10)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start).width('100%')
}
.width('90%').padding(16).backgroundColor(Color.White).borderRadius(16)
.border({ width: 1, color: '#FFEBEE', style: BorderStyle.Solid })
// ── 区域3:拦截结果说明 ──────────────────────────
Column({ space: 8 }) {
Text('💡 三种拦截结果说明').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
ForEach([
['ACTION_INTERCEPT', '完全拦截', '❌ 不执行自身 ❌ 不向父传递', '危险操作二次确认'],
['ACTION_CONTINUE', '拦截后继续', '✅ 仍执行自身 ❌ 不向父传递', '埋点/日志+正常执行'],
['ACTION_RISE', '向父传递', '❌ 不执行自身 ✅ 向父传递', '父组件统一处理'],
], (row: string[]) => {
Column({ space: 3 }) {
Row({ space: 8 }) {
Text(row[0]).fontSize(10).fontWeight(FontWeight.Bold).fontColor('#B71C1C').width(145)
Text(row[1]).fontSize(10).fontColor('#555555').layoutWeight(1)
}.width('100%')
Row({ space: 8 }) {
Text('').width(145)
Text(row[2]).fontSize(9).fontColor('#888888').layoutWeight(1)
}.width('100%')
Row({ space: 8 }) {
Text('').width(145)
Text(`场景:${row[3]}`).fontSize(9).fontColor('#AAAAAA').layoutWeight(1)
}.width('100%')
}
.width('100%').padding({ top: 5, bottom: 5 })
.border({ width: { bottom: 1 }, color: '#F5F5F5', style: BorderStyle.Solid })
})
}
.width('90%').padding(14).backgroundColor('#F1F8E9').borderRadius(12)
.border({ width: 1, color: '#C5E1A5', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
}
.width('100%').alignItems(HorizontalAlign.Center).padding({ top: 16, bottom: 24 })
}
.layoutWeight(1).width('100%')
}
.width('100%').height('100%').backgroundColor('#FAFAFA')
}
}
运行效果如图:

六、实战场景:无障碍属性完整配置
6.1 accessibilityVirtualNode ------ Canvas 虚拟节点
Canvas 是自绘制组件,无障碍服务无法感知其内容。通过 accessibilityVirtualNode 提供虚拟节点,让屏幕朗读器也能理解图表内容:
typescript
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct CanvasAccessibilityDemo {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
build() {
Column({ space: 16 }) {
Text('Canvas 无障碍虚拟节点').fontSize(18).fontWeight(FontWeight.Bold).margin({ top: 24 })
Text('屏幕朗读器将读取虚拟节点描述,而非空白Canvas').fontSize(12).fontColor('#888888')
// Canvas 组件添加虚拟无障碍节点
Canvas(this.ctx)
.width(320).height(180)
.borderRadius(16)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
.onReady(() => {
// 绘制简单柱状图
const data = [60, 85, 45, 92, 70]
const colors = ['#EF5350', '#42A5F5', '#66BB6A', '#FFA726', '#AB47BC']
data.forEach((val, i) => {
this.ctx.fillStyle = colors[i]
this.ctx.fillRect(20 + i * 60, 180 - val * 1.5, 40, val * 1.5)
})
// 绘制标签
this.ctx.fillStyle = '#555555'
this.ctx.font = '12px sans-serif'
;['一月', '二月', '三月', '四月', '五月'].forEach((label, i) => {
this.ctx.fillText(label, 20 + i * 60, 175)
})
})
// 关键:提供虚拟无障碍节点
.accessibilityVirtualNode(() => {
Column({ space: 4 }) {
Text('月度销售额柱状图').accessibilityText('月度销售额柱状图,共五个月份数据')
Text('一月:60万').accessibilityText('一月销售额 60万元')
Text('二月:85万').accessibilityText('二月销售额 85万元,环比增长 41.7%')
Text('三月:45万').accessibilityText('三月销售额 45万元,环比下降 47.1%')
Text('四月:92万').accessibilityText('四月销售额 92万元,环比增长 104.4%')
Text('五月:70万').accessibilityText('五月销售额 70万元,环比下降 23.9%')
}
.accessibilityGroup(true)
.accessibilityLevel('no') // 虚拟节点本身不需要被单独识别
})
}
.width('100%').height('100%').alignItems(HorizontalAlign.Center).backgroundColor(Color.White)
}
}
6.2 accessibilityGroup ------ 分组聚焦
将多个相关子组件合并为一个无障碍聚焦单元,减少屏幕朗读器的焦点跳转次数:
typescript
// 商品卡片:价格+标题+评分合并为一个无障碍节点
Column({ space: 6 }) {
Image($r('app.media.product'))
.width('100%').height(120).borderRadius(8)
.accessibilityLevel('no') // 图片隐藏,由分组文本描述
Text('HarmonyOS6 开发者手册').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
Row({ space: 6 }) {
Text('⭐ 4.8').fontSize(12).fontColor('#FF9800')
Text('1,234 人评价').fontSize(11).fontColor('#9E9E9E')
}
Text('¥ 99.00').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#D32F2F')
}
.width(160).padding(12).backgroundColor(Color.White).borderRadius(12)
// 分组:四个子组件合并为一个可聚焦单元
.accessibilityGroup(true)
.accessibilityText('HarmonyOS6 开发者手册,售价 99 元,评分 4.8 分,1234 人评价')
.accessibilityDescription('点击查看商品详情页')
.accessibilityLevel('yes')
七、综合实战:无障碍友好的登录表单
7.1 功能说明
以下综合示例演示一个完整的无障碍友好登录表单,整合本文所有核心知识点:
- 输入框焦点监听 :
onAccessibilityFocus驱动高亮边框和辅助提示文字 - 登录按钮动作拦截 :
onAccessibilityActionIntercept在无障碍模式下增加确认步骤 - 无障碍属性完整配置 :每个控件均设置
accessibilityText+accessibilityDescription+accessibilityLevel - 分组 :表单控件用
accessibilityGroup合理分组
7.2 完整可运行代码
typescript
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct AccessibilityLoginDemo {
@State username: string = ''
@State password: string = ''
@State usernameFocused: boolean = false
@State passwordFocused: boolean = false
@State loginBtnFocused: boolean = false
@State log: string[] = ['等待操作...']
@State loginCount: number = 0
@State interceptCount: number = 0
@State rememberMe: boolean = false
private appendLog(msg: string): void {
this.log = [msg, ...this.log.slice(0, 9)]
}
build() {
Column({ space: 0 }) {
// 顶部标题
Column({ space: 6 }) {
Text('♿ 无障碍友好登录').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
Text('所有控件均配置了完整无障碍语义').fontSize(12).fontColor('#888888')
}
.width('100%').padding({ top: 40, bottom: 24 })
.alignItems(HorizontalAlign.Center).backgroundColor(Color.White)
Scroll() {
Column({ space: 20 }) {
// 用户名输入框
Column({ space: 6 }) {
Row({ space: 6 }) {
Text('用户名').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333333')
if (this.usernameFocused) {
Text('▶ 焦点在此').fontSize(10).fontColor('#1B5E20').fontWeight(FontWeight.Bold)
}
}.width('100%')
TextInput({ placeholder: '请输入用户名', text: this.username })
.height(48).borderRadius(10).fontSize(14)
.backgroundColor(this.usernameFocused ? '#F1F8E9' : '#F5F5F5')
.border({
width: this.usernameFocused ? 2 : 1,
color: this.usernameFocused ? '#2E7D32' : '#E0E0E0',
style: BorderStyle.Solid
})
.animation({ duration: 180, curve: Curve.EaseOut })
.onChange((val: string) => { this.username = val })
.accessibilityText('用户名输入框')
.accessibilityDescription('请输入您的账号用户名,仅支持字母和数字,长度 6 到 20 位')
.accessibilityLevel('yes')
.onAccessibilityFocus((isFocus: boolean) => {
this.usernameFocused = isFocus
this.appendLog(`[焦点] 用户名输入框 isFocus=${isFocus}`)
})
if (this.usernameFocused) {
Text('💡 仅支持字母和数字,长度 6-20 位').fontSize(11).fontColor('#2E7D32').width('100%')
}
}.width('90%').alignItems(HorizontalAlign.Start)
// 密码输入框
Column({ space: 6 }) {
Row({ space: 6 }) {
Text('密码').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#333333')
if (this.passwordFocused) {
Text('▶ 焦点在此').fontSize(10).fontColor('#1B5E20').fontWeight(FontWeight.Bold)
}
}.width('100%')
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.height(48).borderRadius(10).fontSize(14)
.backgroundColor(this.passwordFocused ? '#F1F8E9' : '#F5F5F5')
.border({
width: this.passwordFocused ? 2 : 1,
color: this.passwordFocused ? '#2E7D32' : '#E0E0E0',
style: BorderStyle.Solid
})
.animation({ duration: 180, curve: Curve.EaseOut })
.onChange((val: string) => { this.password = val })
.accessibilityText('密码输入框')
.accessibilityDescription('请输入您的登录密码,密码区分大小写,至少 8 位')
.accessibilityLevel('yes')
.onAccessibilityFocus((isFocus: boolean) => {
this.passwordFocused = isFocus
this.appendLog(`[焦点] 密码输入框 isFocus=${isFocus}`)
})
if (this.passwordFocused) {
Text('💡 密码区分大小写,至少 8 位').fontSize(11).fontColor('#2E7D32').width('100%')
}
}.width('90%').alignItems(HorizontalAlign.Start)
// 记住我(Toggle)
Row({ space: 12 }) {
Toggle({ type: ToggleType.Checkbox, isOn: this.rememberMe })
.width(22).height(22).selectedColor('#1B5E20')
.accessibilityText(this.rememberMe ? '记住我:已勾选' : '记住我:未勾选')
.accessibilityDescription('勾选后,下次打开应用将自动填充账号信息')
.accessibilityLevel('yes')
.onChange((isOn: boolean) => {
this.rememberMe = isOn
this.appendLog(`[切换] 记住我 → ${isOn}`)
})
Text('记住登录状态').fontSize(13).fontColor('#555555')
Text(this.rememberMe ? '(已勾选)' : '').fontSize(11).fontColor('#2E7D32')
}
.width('90%').justifyContent(FlexAlign.Start)
.accessibilityGroup(true)
.accessibilityText(this.rememberMe ? '记住我,已勾选' : '记住我,未勾选')
.accessibilityDescription('勾选以在下次启动时自动填充登录信息')
// 登录按钮
Button('立即登录')
.width('90%').height(50).borderRadius(12).fontSize(16).fontWeight(FontWeight.Bold)
.backgroundColor(this.loginBtnFocused ? '#2E7D32' : '#1B5E20')
.fontColor(Color.White)
.shadow(this.loginBtnFocused
? { radius: 12, color: '#601B5E20', offsetX: 0, offsetY: 4 }
: { radius: 0, color: Color.Transparent, offsetX: 0, offsetY: 0 })
.animation({ duration: 200, curve: Curve.EaseOut })
.accessibilityText('立即登录按钮')
.accessibilityDescription('点击后使用当前填写的用户名和密码进行登录,请确认信息填写正确')
.accessibilityLevel('yes')
.onAccessibilityFocus((isFocus: boolean) => {
this.loginBtnFocused = isFocus
this.appendLog(`[焦点] 登录按钮 isFocus=${isFocus}`)
})
.onAccessibilityActionIntercept((action: AccessibilityAction) => {
if (action === AccessibilityAction.ACCESSIBILITY_CLICK) {
this.interceptCount++
this.appendLog(`🛑 [拦截] 登录按钮无障碍点击 ×${this.interceptCount}`)
// 无障碍模式下弹出确认框
AlertDialog.show({
title: '♿ 确认登录',
message: `账号:${this.username || '(未填写)'}\n是否确认登录?`,
primaryButton: {
value: '确认登录',
fontColor: '#1B5E20',
action: () => {
this.loginCount++
this.appendLog(`✅ 无障碍确认登录 ×${this.loginCount}`)
}
},
secondaryButton: {
value: '返回检查',
action: () => {
this.appendLog('○ 返回检查表单')
}
}
})
return AccessibilityActionInterceptResult.ACTION_INTERCEPT
}
return AccessibilityActionInterceptResult.ACTION_CONTINUE
})
.onClick(() => {
this.loginCount++
this.appendLog(`🟢 [普通点击] 登录 ×${this.loginCount},账号=${this.username || '空'}`)
})
// 统计信息
Row({ space: 24 }) {
Column({ space: 3 }) {
Text(`${this.loginCount}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
Text('登录次数').fontSize(10).fontColor('#9E9E9E')
}
Column().width(1).height(36).backgroundColor('#E0E0E0')
Column({ space: 3 }) {
Text(`${this.interceptCount}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#D32F2F')
Text('无障碍拦截').fontSize(10).fontColor('#9E9E9E')
}
}
.width('90%').justifyContent(FlexAlign.SpaceEvenly).padding({ top: 12, bottom: 12 })
.backgroundColor('#F9F9F9').borderRadius(12)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
// 事件日志
Column({ space: 4 }) {
Text('📋 事件日志(最新在顶)').fontSize(11).fontColor('#9E9E9E').width('100%')
ForEach(this.log, (item: string, idx: number) => {
Text(item).fontSize(10).fontColor(idx === 0 ? '#1B5E20' : '#BDBDBD').width('100%')
})
}
.width('90%').padding(12).backgroundColor('#F9F9F9').borderRadius(12)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
// 无障碍配置说明卡片
Column({ space: 8 }) {
Text('♿ 本示例无障碍配置清单').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#1B5E20')
ForEach([
['用户名/密码输入框', 'accessibilityText + Description + onAccessibilityFocus'],
['记住我 Toggle', 'accessibilityGroup 分组 + 动态 accessibilityText'],
['登录按钮', 'onAccessibilityFocus 高亮 + onAccessibilityActionIntercept 确认'],
['装饰性元素', 'accessibilityLevel("no") 对无障碍服务隐藏'],
], (row: string[]) => {
Row({ space: 8 }) {
Text('•').fontSize(12).fontColor('#2E7D32').width(12)
Column({ space: 2 }) {
Text(row[0]).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#333333').width('100%')
Text(row[1]).fontSize(10).fontColor('#888888').width('100%')
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%')
})
}
.width('90%').padding(14).backgroundColor('#F1F8E9').borderRadius(12)
.border({ width: 1, color: '#C5E1A5', style: BorderStyle.Solid })
.alignItems(HorizontalAlign.Start)
}
.width('100%').alignItems(HorizontalAlign.Center).padding({ bottom: 40 })
}
.layoutWeight(1).width('100%')
}
.width('100%').height('100%').backgroundColor(Color.White)
}
}
总结
- 焦点监听 :
onAccessibilityFocus(isFocus)在组件获焦/失焦时触发,常用于高亮边框、显示辅助提示、朗读状态更新,让低视力用户清楚感知当前焦点位置 - 动作拦截 :
onAccessibilityActionIntercept在无障碍点击前触发,三种返回值各司其职:ACTION_INTERCEPT(二次确认)、ACTION_CONTINUE(埋点+正常执行)、ACTION_RISE(向父传递) - 语义属性完整配置 :
accessibilityText>accessibilityDescription>accessibilityLevel三属性搭配,让每个交互组件都能被屏幕朗读器准确识别和播报 - 分组减负 :
accessibilityGroup将相关子组件合并为一个聚焦单元,减少朗读器焦点跳转次数,提升使用流畅度 - Canvas 虚拟节点 :
accessibilityVirtualNode为自绘图表提供语义化节点,弥补自绘组件无障碍信息缺失 - 装饰性元素隐藏 :
accessibilityLevel('no')隐藏纯装饰性图标/分割线,避免占用朗读器焦点,降低用户认知负担
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!