👉 腾小云导读
现我国现有4471w视障/听障人士,60岁及以上人群达2.6亿规模。微信作为国民级应用,实现无障碍迫在眉睫。为了帮助他们更好地使用微信 App,Android微信完成了适老化及无障碍改造。本文主要介绍Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。希望能给广大开发爱好者带来帮助和启发!
👉 看目录,点收藏
1 无障碍需求框架背景
1.1 无障碍需求
1.2 框架简介
2 无障碍开发基础知识
2.1 读屏软件识别View原理
2.2 读屏软件后的事件分发原理
3 框架实现的整体流程和执行原理
3.1 整体流程
3.2 执行原理
4 核心说明:全局热区补足机制
4.1 背景说明
4.2 具体实现
4.3 额外说明
5 走查工具
6 总结
01、无障碍需求框架
目前,业界已经有共识性的无障碍开发守则。例如 Web Content Accessibility Guidelines (WCAG) 2.0,它是由互联网的主要国际标准组织万维网联盟 (W3C) 的Web可访问性倡议 (WAI) 发布的一系列 Web 可访问性指南的一部分。
此外,WAI-ARIA(可访问的富Internet应用程序套件)是由万维网联盟(W3C)发布的一项关于 A11 Y技术应用规范。该规范定义了一种使残障人士更易于访问 Web 内容和 Web 应用程序的方法,增加 HTML、JavaScript 和相关技术开发的网站动态内容以及用户界面组件的可访问性。
目前,Android没有官方统一、方便的框架,官方提供的原生api并不是特别好用,所以微信团队对其进行参考,开发了一个无障碍框架,基于原生的api进行了再封装,将繁琐的无障碍适配逻辑封装在底层,以声明式接口的形式,让上层业务能以更简便更解耦的代码,完成无障碍的适配。接下来我们进行分享:
1.1无障碍需求
本框架主要具备以下特性:
- 可感知性 :包括大字体适配,颜色对比度等 。
- 可操作性 :主要是过小热区的放大,提高老年人/残疾人的交互体验 。
- 可理解性 :微信应提供读屏文案等信息,帮助盲人在开启 Talkback 等读屏软件的情况下,正常使用微信。
下面给出一些较为典型的需求:
- 需求1:过小热区的放大
需求是要求微信内的所有可交互控件,可点击范围不得低于 44dp * 44dp。
大小不合规的控件,如果一个个进行排查、布局修改。工程量庞大。
- 需求2:响应区域会随无障碍开关发生变化
该 Item 由一个 SwitchButton + TextView 组成。
开启 Talkback 时,整个 Item 识别为一个焦点,选中双击是触发点击 switch的逻辑。在无障碍模式下,选中双击是直接触发相应控件的 Click 事件。但是在不开 Talkback 的情况下点击 Item 又无需响应,只响应 SwitchButton 。也就是点击区域会随 Talkback 开关发生变化。
实现可能是:在 ItemClick 中进行 if 判断。但这样写侵入性高,难维护。
- 需求3:读屏文案由其他的控件的值组合
选中头像,读屏文案:腾讯行政的头像,有 2 条未读消息。需要读出列表中其他关联内容,这种只能把适配代码侵入到 Adapter中。
1.2 框架简介
框架将不同的无障碍需求的实现进行封装,抽象成不同的规则。
业务侧可以将一个页面/业务的无障碍需求,在一个配置类里使用规则表达出来,再由框架进行处理。实现相应的效果。
class ChatAccessibility(activity: AppCompatActivity) :
BaseAccessibilityConfig(activity) {
override fun initConfig() {
// 设置 contentDesc
view(rootId,viewId).desc(R.string.send_smiley)
// ...
}
}
框架基类 BaseAccessibilityConfig 提供了一系列用于表达规则的 api,包括但不限于如下功能:
-
通过配置统一设置 contentDescription
-
支持把多个 View 组合成一体进行读屏
-
通过配置禁用某个View被 Talkback 聚焦的能力
-
支持按指定顺序进行读屏,支持局部控制 Talkback 聚焦顺序
-
支持设定在 Activity 启动后的第一个读屏控件
-
支持对某个父 View 的 disableChildren 功能
-
在某个 View 满足条件时,对其进行读屏,但不聚焦
-
在某个 View 满足条件时,读出提前设定的 string,但不聚焦
-
全局热区宽高补齐至 44dp,并提供自定义热区放大/禁用热区放大的功能 ...
02、无障碍开发基础知识
在深入了解框架的设计前,先来介绍一些无障碍功能开发的基础知识。
2.1 基础知识1:读屏软件识别 View 原理
读屏软件无法直接识别到View,只能识别到View提供的虚拟节点「Node」,View 和虚拟节点一般是一一对应的。当页面内容发生变化,比如 View 被设值,或者发生滚动等情况,View 会向无障碍系统发送一个事件,通知系统。
然后系统就回头向 View 索取节点,组成页面更新后新的节点树,而 「节点树 和 ViewTree 是一一对应的」。此时读屏软件拿到的就是新的内容了。
2.2 基础知识2:读屏软件后的事件分发流程
分为上下两部分:读屏软件拦截处理行为、读屏软件接受事件。
流程如下:
- 读屏软件拦截用户 Touch 事件,根据事件的坐标去定位到目标节点。
- 将 Touch 事件解释为节点行为,这里以触摸选中为例,那么就是聚焦行为。
- 读屏软件通过该节点向无障碍系统发送,无障碍系统又转发给View(聚焦产生的绿框就是在View的内部处理里去绘制的)。
- 生成新的虚拟节点并提供给读屏软件后,读屏软件组合信息,通过 TTS 语音引擎的 api 读出。
读屏软件展示给用户的所有信息,全部来自虚拟节点。可以在节点生成的过程中,修改节点的信息,所以这里是一个绝佳的**「信息自定义」**的地方。
采用将所有的 View 都 「Wrap 一层 AccessibilityDelegate」 的方式,「在 onInitializeAccessibilityNodeInfo 方法中修改节点信息」。
03、框架实现整体流程与执行原理
3.1 整体流程
-
业务侧实现规则配置类,编写的规则会进入配置池。
-
框架在View生成节点给系统的时候进行拦截 「(onInitializeAccessibilityNodeInfo)」。
-
在配置池中寻找匹配的规则。
-
根据匹配的规则对节点进行修改。
-
最后生成的节点就会由系统交由给读屏软件进行读屏。
3.2 执行原理
核心原理:采用基于责任链的流水线来处理。整体流程主要分为两部分:
-
View 预处理责任链(图示左边):执行预出来操作,如异步生成缓存、View标记等;
-
节点处理责任链(图示右边):节点处理的同时会同步查找规则进行设置。
接下来主要简单介绍下框架的一个核心功能的实现:「全局热区补足机制」 (位于框架流程中的预处理责任链中的一环)。
04、核心说明:全局热区补足机制
4.1 背景说明
- 需求说明
过小热区放大,即微信内的所有可交互控件可点击范围不得低于 44dp * 44dp,像一些大小不合规的控件,如果一个个进行排查、布局修改,工程量太庞大。还有热区其他一些需求 etc。
- 问题难点
一般会选择直接修改 padding,有些甚至需要改动相应布局,但这样的改动工作量太大且容易影响原来视图布局。
- 解决方案
需要一个全局的热区补足机制,将过小热区补足至规范。
4.2 具体实现
在 「创建 View 的统一入口」 去设置 TouchDelegate 代理,由父 View 作为TouchDelegate 的承载 View 去代理 Touch 事件,这里有几个问题需要解决:
- 如何找到合适的承载View
- 热区及时更新
- 性能优化
- 读屏模式下的热区扩大
下面我们分别展开讲。
- 重点问题1:如何找到合适的承载 View
从目标 View 向上冒泡,找到一个合适的父 View。那么需要 「冒泡终止条件」。 首先条件一肯定是 「足够大」。当前 View 够大了就没必要再往上冒了。
但是这样会存在问题:子 View 的 Click优先级高于父View的TouchDelegate。事件派发机制:
从父 View 往子 View 派发,从子 View 向上处理。View 的事件处理顺序是先 OnTouchListener,然后是 TouchDelegate,再是Click、LongClick。
所以会导致下图的情况:
目前进行了折中处理,相比上图,显然是下图的放大后的体验更佳:
同时加入了条件二:「该承载 View 是 Clickable、LongClickable」。最终方案流程确定如下:
- 重点问题2:热区及时更新
背景: 承载 View 的 TouchDelegate 需要的参数包含一个 Rect,也就是对扩大的热区进行响应。
问题: 这个矩阵是提前传入,且和 小 View 没有直接的关系。如果小 View 的布局发生变动,会导致扩大后热区没有及时跟上变化。导致热区错位。
解决方案: 在 小 View 的 onLayoutChange 中重新进行一遍 ·View 扩大方案· 的处理。同时为了防止 onLayoutChange 执行过于频繁,将 onLayoutChange 包装成 View 的一个事件。如果短时间内多次 onLayoutChange ,则只在最后一次 onLayoutChange 的时候进行 「View扩大方案」处理。
- 重点问题3:性能优化
背景 :最初的 View 扩大方案执行时机是在创建 View 的统一入口,也就是在 LayoutInflate 的 onCreateView 中同步执行,每个 View 都得执行。
问题:由于 View 数量较为庞大,所以存在较大的性能隐患。
解决方案:采用了异步方案并同时对 View 处理任务进行收拢。将执行时机提前到 LayoutInflate.inflate 并异步处理,在异步任务中去遍历该 inflate 的根 View的所有子 View。尽量不去阻塞主线程的运行。
- 重点问题4:读屏模式下的热区扩大
通过上面的实现,点击热区确实是扩大了。但是在读屏模式下选中的时候,选中的框并没有扩大。那么首先需要知道,选中时的框是以什么作为 Bound。
绿框的绘制核心逻辑位于 ViewRootImpl 中的一个 drawAccessibilityFocusedDrawableIfNeeded(),该方法的调用时机是用户触摸选中某个View后,传递到 ViewRootImpl 时进行调用,也就是读屏选中的绿框是由系统绘制的,而不是由读屏软件绘制的。从源码中能够得知的是,绿框的Bound 根据是否有虚拟节点,分为两种情况:
private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) {
final Rect bounds = mAttachInfo.mTmpInvalRect;
if (getAccessibilityFocusedRect(bounds)) {
final Drawable drawable = getAccessibilityFocusedDrawable();
if (drawable != null) {
drawable.setBounds(bounds);
drawable.draw(canvas);
}
} else if (mAttachInfo.mAccessibilityFocusDrawable != null) {
mAttachInfo.mAccessibilityFocusDrawable.setBounds(0, 0, 0, 0);
}
}
private boolean getAccessibilityFocusedRect(Rect bounds) {
...
final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
if (provider == null) {
host.getBoundsOnScreen(bounds, true);
} else if (mAccessibilityFocusedVirtualView != null) {
mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds);
} else {
return false;
}
...
return !bounds.isEmpty();
}
经过跟踪源码发现,这是因为 「绿框的绘制」 是根据 View.getBoundInScreen 获取的矩阵来做到的。而 TouchDelegate 的设置无法改变 View.getBoundInScreen 获取到的矩阵。在使用虚拟节点的情况下,才会使用虚拟节点的Bound进行绘制。
对于这个问题,我们的解决思路是:
- 对每个 View 设置自定义的 AccessibilityDelegate, 并实现其中的 getAccessibilityNodeProvider 方法。
- 如果判断 View 需要扩大,在 getAccessibilityNodeProvider 中返回自定义的 Provider。
- 在自定义的 Provider 中,计算 View 的扩大后的矩阵在屏幕上的位置。
- 将矩阵设置给虚拟节点,并返回给系统。
4.3 额外说明
- 如何匹配规则与View?
框架将配置池按 Activity 划分,极大减少冲突概率,同时减少配置池大小,加快查找规则的速度,提供 layoutId + viewId,rootId + viewId 两种形式的 View 定位机制。由两个 Id 确定一个 View,减少冲突。
- 查找规则时间长可能导致的主线程卡顿?
由于查找规则的时机是在生成节点,是由系统触发且无法异步。在查找规则的过程中,使用预处理的时候提前生成的缓存进行查找,尽可能减少耗时。
05、走查工具
5.1 背景
当完成无障碍需求的开发后,需进行验证。在验证过程中发现开启验证效率低下,需开启读屏软件后,逐个元素验证。
5.1.1 解决方案与原理
基于无障碍服务(AccessibilityService)开发、集成了在不开启 Talkback 的情况下能展现读屏区域一个无障碍功能走查工具,无需开启 Talkback 逐个手动触摸,就能高效检查无障碍适配情况。
实现原理如下:
-
自定义实现一个 AccessibilityService 用于获取到当前活跃窗口的根节点。
-
每隔 0.5s 进行一次节点的获取:从当前活跃窗口的根节点遍历所有的节点,逐个进行判断是否会被聚焦。
-
对通过允许聚焦的节点进行信息收集,在一次遍历完成后通知到 DrawService。
-
提前在window中添加一个 View 用于绘制信息,由 DrawService 进行绘制。
5.2 具体实现
关键实现:如何判断一个节点能否被聚焦,即需理解 Talkback 是如何聚焦,流程如下:
1、如果是支持 WebView 中 Html 无障碍,特殊判断。
2、如果不可见,则不聚焦。
3、判断是否是画中画,像下图的红框这种就是画中画,如果是画中画,这个就是焦点。
4、该节点是否和 window 边界重合等大。对于这种和 window 等大的节点,Talkback 选择不做聚焦。
5、检查该节点是否 clickable/longClickable/focusable 或者是列表的“会说话的” 顶层视图(满足->6 不满足->7)列表(ListView/RecycleView)的顶层视图例子如下:
但是聚焦的前提是“会说话的”。“会说话的”包括以下几个条件:
- HasText:包括 contentDescription、text、hintText(包括 Button 的 Text)。
- hasStateDescription:包括 CheckBox 的已选未选状态、进度条的进度状态等。
- hasNonActionableSpeakingChildren:含有无法聚焦、点击但是 HasText 的子 View(如上图通讯录中的 “新的朋友” TextView,就是无法聚焦、点击但是 HasText 的子 View)。
6、基本上满足了步骤5就可以视为可聚焦了,但是有一些View仅仅是 Focusable,但是却 ”什么话都没得说“ ,对于这种 View 应该是要排除的。故按如下步骤做判断:只要是没有子节点的 focusable/clickable/longclickable 的 View,全部聚焦 、“会说话的” 全部聚焦 6.3 剩下的就不聚焦了(“不会说话”、“有子节点”)。
7、能到这一步,说明步骤 5 不满足,即该节点是普通的不可聚焦的 View。但是防止错过一些没有点击事件的 TextView 之类的需要聚焦,需要再最后做一步判断(这一步也是啥为了保证所有的信息都可以不遗漏);如果没有可聚焦父节点,但仍然 hasText 或 hasStateDescription,聚集该节点。
8、一路闯关到这的 View,就终于逃离 TalkBack 的聚焦了。
06、总结
为了帮助老年人、视障/听障人群等更好地使用微信 App,Android微信完成了适老化及无障碍改造如上。本文主要介绍 Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。我们在介绍了无障碍开发所涉及的2大重点基础知识(读屏识别View原理和读屏软件后的事件分发原理)之后,为各位展开回顾了我们框架具体细节和方法。
以上是本次分享全部内容,欢迎大家在评论区分享交流。如果觉得内容有用,欢迎转发~
-End-
原创作者|许怀鑫
技术责编|许怀鑫
现我国现有4471w视障/听障人士,60岁及以上人群达到2.6亿规模。信息无障碍(Web Accessibility)的概念在近几年受到关注。 信息无障碍是指通过信息化手段弥补身体机能、所处环境等存在的差异,使任何人(无论是健全人还是残疾人、无论是年轻人还是老年人)都能平等、方便、安全地获取、交互、使用信息。微信、QQ、腾讯新闻和腾讯地图等应用加适老化元素,配备为老人而设的“关怀模式”;搜狗输入法推出为视障群体量身打造的“保益盲人输入法”......
当说到无障碍,大家第一反应是弱势群体。实际上,无障碍是适用于全民的。每个人都可能有遇障时刻。当你手提重物或受伤时,你可能会选择乘坐无障碍电梯;当你处在嘈杂的环境下看视频时,你可能需要通过字幕获取信息……每个人都是无障碍环境的受益者,视障、听障人群、含残疾人、老年人是信息无障碍的重点受益群体。
事件分享:你还见到过哪些让你眼前一亮的信息无障碍案例?
脑洞时刻:程序员还可以为信息无障碍做些什么?
欢迎在公众号评论区聊一聊你的看法。在4月10日前将你的评论记录截图,发送给腾讯云开发者公众号后台,可领取腾讯云「开发者春季限定红包封面」一个,数量有限先到先得😄。我们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月10日中午12点开奖。快邀请你的开发者朋友们一起来参与吧!
回复「微信」,领取更多微信的技术case和论文资源