藏在微信里的温度,无障碍开发框架分享
👉 腾小云导读 现我国现有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 Ac cessibility Guidelines (WCAG) 2.0,它是由互联网的主要国际标准组织万维网联盟 (W3C) 的Web可访问性倡议 (WAI) 发布的一系列 Web 可访问性指南的一部分。
此 外,WAI-ARIA (可访问的富Internet应用程序套件) 是由万 维网联盟(W3C)发布的一项关于 A11 Y技术应用规范。该规范定义了一种使残障人士更易于访问 Web 内容和 Web 应用程序的方法,增加 HTML、JavaScript 和相关技术开发的网站动态内容以及用户界面组件的可访问性。
目前,Android没有官方统一、方便的框架,官方提供的原生api并不是特别好用,所以微信团队对其进行参考,开发了一个无障碍框架,基于原生的api进行了再封装,将繁琐的无障碍适配逻辑封装在底层,以声明式接口的形式,让上层业务能以更简便更解耦的代码,完成无障碍的适配。接下来我们进行分享:
1.1 无障碍需求
本框架主要具备以下特性:
|
下面给出一些较为典型的需求:
-
需 求1:过小热区的放大
-
需 求2:响应区域会随无障碍开关发生变化
开启 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,包括但不限于如下功能:
|
02
无障碍开发基础知识
在深入了解框架的设计前,先来介绍一些无障碍功能开发的基础知识。
2 .1 基础知识1:读屏软件识别 View 原理
读屏软件无法直接识别到View,只能识别到View提供的虚拟节点 「Node」 ,View 和虚拟节点一般是一一对应的。当页面内容发生变化,比如 View 被设值,或者发生滚动等情况,View 会向无障碍系统发送一个事件,通知系统。
然后系统就回头向 View 索取节点,组成页面更新后新的节点树,而 「节点树 和 ViewTree 是一一对应的」 。此时读屏软件拿到的就是新的内容了。
2 .2 基础 知识2:读屏软件后的事件分发流程
分为上下两部分:读屏软件拦截处理行为、读屏软件接受事件。
流程如下:
|
读屏软件展示给用户的所有信息,全部来自虚拟节点。可以在节点生成的过程中,修改节点的信息,所以这里是一个绝佳的 「信息自定义」 的地方。
采用将所有的 View 都 「Wrap 一层 AccessibilityDelegate」 的方式,「在 onInitializeAccessibilityNodeInfo 方法中修改节点信息」。
03
框架实现整体流程与执行原理
3.1 整体流程
- 业务侧实现规则配置类,编写的规则会进入配置池。
- 框架在View生成节点给系统的时候进行拦截 「(onInitializeAccessibilityNodeInfo)」 。
- 在配置池中寻找匹配的规则。
- 根据匹配的规则对节点进行修改。
- 最后生成的节点就会由系统交由给读屏软件进行读屏。
3.2 执行原理
核心原理:采用基于责任链的流水线来处理。整体流程主要分为两部分:
- View 预处理责任链 (图示左边) :执行预出来操作,如异步生成缓存、View标记等;
- 节点处理责任链 (图示右边): 节点处理的同时会同步查找规则进行设置。
接下来主要简单介绍下框架的一个核心功能的实现: 「全局热区补足机制」 (位于框架流程中的预处理责任链中的一环) 。
04
核心说明:全局热区补足机制 4 .1 背景说明
- 需求说明
- 问题难点
- 解决方案
在 「创建 View 的统一入口」 去设置 TouchDelegate 代理,由父 View 作为TouchDelegate 的承载 View 去代理 Touch 事件,这里有几个问题需要解决:
|
下面我们分别展开讲。
- 重点问题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.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进行绘制。
对于这个问题,我们的解决思路是:
|
4.3 额外说明
-
如何匹配规则与View?
-
查找规则时间长可能导致的主线程卡顿?
05
走查工具
5 .1 背景
当完成无障碍需求的开发后,需进行验证。在验证过程中发现开启验证效率低下,需开启读屏软件后,逐个元素验证。
5 .1.1 解决方案与原理
基于无障碍服务 (AccessibilityService) 开发、集成了在不开启 Talkback 的情况下能展现读屏区域一个无障碍功能走查工具,无需开启 Talkback 逐个手动触摸,就能高效检查无障碍适配情况。
实现原理如下:
|
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和论文资源