抖音无障碍背景

国家近期开展了无障碍建设活动。为了积极响应国家号召,为抖音视障用户能够得到更好的交互体验,对抖音无障碍功能进行了专项治理和改造。

无障碍模式下的使用方法

抖音的无障碍功能实现主要是通过开启 Google TalkBack(或第三方屏幕阅读)功能,将用户在屏幕上触摸选中区域的内容朗读出来,使得视障人士可以根据朗读的内容获取自己当前操作区域的信息,从而提升视障人士的使用和交互体验。

常用的操作手势:

  • 浏览某个 View:单击

  • 点击某个 View:双击

  • 沿某个方向滑动:双指沿所需方向滑动

  • 顺序浏览页面:单指左右滑动

本文的目的

使研发同学对无障碍功能有一个更加全面的认识和了解,方便研发同学进行无障碍功能的开发。

本文将分为无障碍功能实现原理无障碍功能实现实例两部分进行介绍。

无障碍功能实现原理

系统结构

无障碍功能的实现需要以下三个部分的支持:辅助 App(例如 TalkBack)、被辅助 app(用户使用的 app,例如抖音头条等)以及系统服务 AccessibilityManagerService,这三者之间的关系如下图所示:

从上图中可以看出,以上的流程主要涉及到三个进程的通信。辅助 app 和被辅助 app 不需要直接跟被辅助的 app 通信,而是通过 SystemServer 进行中转通信,这个过程主要涉及到了四个 aidl 接口:

  • 被辅助 app->SystemServer(IAccessibilityManager.aidl)

当被辅助 app 产生触摸事件后,会通过该接口发送无障碍事件给 SystemServer 进程的 AccessibilityManagerService。

  • SystemServer->辅助 app(IAccessibilityServiceClient.aidl)

当 SystemServer 接收到被辅助 app 发送的无障碍事件时,会将事件通过该接口传递给辅助 app(例如 TalkBack)进行处理。

  • 辅助 app->SystemServer(IAccessibilityServiceConnection.aidl)

  • SystemServer->被辅助 app(IAccessibilityInteractionConnection.aidl)

当需要被辅助 app 的某个 View 的信息时,可以通过这两个接口的 findAccessibilityNodeInfosByViewId 方法实现。

无障碍事件传递流程

当用户触摸屏幕时,会经过以下的流程将触摸事件传递给被触摸的 View:

下面本文将主要分析以上流程中四个重点部分的内容:无障碍模式下的事件转换、触摸事件到 Activity 的传递过程、事件传递给具体的 View 的分发过程以及最终无障碍事件的执行流程。

1.无障碍模式下的事件转换

在 TalkBack 开启的状态下,由于 TalkBack 的无障碍服务中声明了 android:canRequestTouchExplorationMode=''true'' ,因此开启 TalkBack 后 AccessibilityManagerService 会更新 AccessibilityInputFilter 的FLAG_FEATURE_TOUCH_EXPLORATION(触摸浏览)属性置为 true。

在 FLAG_FEATURE_TOUCH_EXPLORATION 模式下会创建一个 TouchExplorer 对象。AccessibilityInputFilter 继承了 InputFilter,对输入事件进行过滤,通过和 TouchExplorer 共同实现 TalkBack 模式下的触摸浏览手势。TouchExplorer 负责将普通触摸事件转换为触摸浏览手势,例如将 MotionEvent.ACTION_DOWN 事件转换为 MotionEvent.ACTION_HOVER_ENTER(悬停事件)。因此在 TalkBack 开启的情况下,用户单击 View 时,App 执行的是 ACTION_HOVER_ENTER 事件,双击 View 时才会执行 ACTION_DOWN 事件。

2.触摸事件到 Activity 的传递过程

在 Android 中,消息机制是 handler 机制,通过将消息封装到 Message 中,并将该消息发送到 handler 所在的 MessageQueue 中,通过 Looper 不断调用 MessageQueue 的 next 方法进行消息的处理。

当用户触摸屏幕上的某个 View 时,handler 会对收到的消息进行以下的处理:

这里需要重点看一下 View 的 dispatchPointerEvent() 方法:

public final boolean dispatchPointerEvent(MotionEvent event) {if (event.isTouchEvent()) {return dispatchTouchEvent(event);} else {return dispatchGenericMotionEvent(event);}}

在该方法中对 event 进行判断,如果是 touchEvent 就调用 dispatchTouchEvent() 方法,否则调用 dispatchGenericMotionEvent() 方法。判断是否为 touch 事件的逻辑如下:

bool MotionEvent::isTouchEvent(int32_t source, int32_t action) {if (source & AINPUT_SOURCE_CLASS_POINTER) {// Specifically excludes HOVER_MOVE and SCROLL.switch (action & AMOTION_EVENT_ACTION_MASK) {case AMOTION_EVENT_ACTION_DOWN:case AMOTION_EVENT_ACTION_MOVE:case AMOTION_EVENT_ACTION_UP:case AMOTION_EVENT_ACTION_POINTER_DOWN:case AMOTION_EVENT_ACTION_POINTER_UP:case AMOTION_EVENT_ACTION_CANCEL:case AMOTION_EVENT_ACTION_OUTSIDE:return true;}}return false;}

符合以上 case 的 event 即为 TouchEvent。

首先来看一下 dispatchPointerEvent 方法中对 TouchEvent 事件的处理,进入 DecorView 的 dispatchTouchEvent() 方法中:

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);}

在该方法中,mWindow 是与 Activity 关联的 PhoneWindow 对象,由于 DecorView 是由 PhoneWindow 创建的,并且通过 setWindow() 方法,DecoView 对象持有 PhoneWindow 对象的引用。通过 getCallback() 方法,获得了实现了 Window.Callback 的对象,而 Activity 实现了这个接口,因此当调用cb.dispatchTouchEvent(ev) 时,实际上调用的是 Activity 中的 dispatchTouchEvent() 方法。

同样的在 dispatchGenericMotionEvent() 方法中,也有类似的代码逻辑:

@Overridepublic boolean dispatchGenericMotionEvent(MotionEvent ev) {final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchGenericMotionEvent(ev) : super.dispatchGenericMotionEvent(ev);}

此方法中实际上也是调用了 Activity 的 dispatchGenericMotionEvent() 方法对事件进行后续的分发和处理。此时事件就已经传递到了 Activity,由 Activity 进一步进行事件分发。

3.触摸事件传递到具体 View 的过程

在研究无障碍模式下的事件传递过程之前,首先来回顾一下普通模式下的事件传递机制:

3.1 普通模式的事件分发

3.1.1 普通模式下事件分发 Key Method

当一个 MotionEvent 产生之后,系统需要将该事件传递给一个具体的 view,这个传递过程就是事件的分发过程。分发过程依赖于以下三个重要方法:

  • public boolean dispatchTouchEvent(MotionEvent ev)

该方法用来进行事件的分发,方法的返回值取决于当前 View 的 onTouchEvent() 方法和子 View 的 dispatchTouchEvent() 方法的影响。

  • public boolean onInterceptTouchEvent(MotionEvent ev)

仅 ViewGroup 拥有的方法,用来判断是否拦截某个事件。

  • public boolean onTouchEvent(MotionEvent event)

在 dispatchTouchEvent() 方法中进行调用,用来处理点击事件。

3.1.2 普通模式下的事件分发

整个分发过程可以用以下的流程图来表示:

3.2 无障碍模式下的事件分发

无障碍模式下的事件分发与普通模式下的事件分发有很多相似之处:

3.2.1 无障碍模式下的事件分发 Key Method:

与普通事件触摸事件的分发类似,无障碍事件触发事件分发也有类似的三个重要方法:

  • protected boolean dispatchHoverEvent(MotionEvent event)

该方法用来进行事件的分发,方法的返回值取决于当前 View 的 onHoverEvent() 方法和子 View 的 dispatchHoverEvent() 方法的影响。

  • public boolean onInterceptHoverEvent(MotionEvent event)

仅 ViewGroup 拥有的方法,用来判断是否拦截某个事件。

  • public boolean onHoverEvent(MotionEvent event)

在 dispatchHoverEvent() 方法中进行调用,用来处理 hover 事件。

3.2.2 无障碍模式下的事件分发

当用户处于无障碍模式下,用户进行点击屏幕时,会调用 dispatchPointerEvent 方法中的 dispatchGenericMotionEvent 方法:

public final boolean dispatchPointerEvent(MotionEvent event) {if (event.isTouchEvent()) {return dispatchTouchEvent(event);} else {return dispatchGenericMotionEvent(event);}}

实际上调用的是 Activity 的 dispatchGenericMotionEvent() 方法,Activity 接收到事件后,会传递给 PhoneWindow 再传递给 DecorView。DecorView 会调用 View 的 dispatchGenericMotionEvent() 方法:

public boolean dispatchGenericMotionEvent(MotionEvent event) {···final int source = event.getSource();if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {final int action = event.getAction();//判断事件类型属于Hover,调用dispatch方法开始进行分发if (action == MotionEvent.ACTION_HOVER_ENTER|| action == MotionEvent.ACTION_HOVER_MOVE|| action == MotionEvent.ACTION_HOVER_EXIT) {if (dispatchHoverEvent(event)) {return true;}}...return false;}

在该方法中,如果判断事件为 HoverEvent,就调用 ViewGroup 的 dispatchHoverEvent() 方法开始进行事件分发。

如果某个 ViewGroup 的 onInterceptHoverEvent() 方法返回 true,表示它要拦截当前事件,并交给自己处理,反之返回 false 表示不拦截当前事件,并将当前事件继续传递给子 View,子 View 会调用自己的 dispatchHoverEvent() 方法,如此循环往复直到事件最终被处理。

在事件处理阶段,View/ViewGroup 首先会判断是否设置了 OnHoverListener,并判断它的 onHover 方法的返回值是否为 true,如果返回值为 true,则不会调用 onHoverEvent() ,反之会调用 onHoverEvent() 方法对事件进行处理。

整个处理过程可以用下面的流程图进行表示:

在 onHoverEvent() 方法中,会调用到 sendAccessibilityHoverEvent()方法,该方法后续会调用以下方法:

  • sendAccessibilityEvent

  • sendAccessibilityEventUnchecked

  • onInitializeAccessibilityEvent

  • dispatchPopulateAccessibilityEvent

  • onPopulateAccessibilityEvent

  • onRequestSendAccessibilityEvent(仅在 ViewGroup 中有默认实现)

以上 6 种方法为当自定义 View 时适配无障碍模式可以覆盖实现的方法,可以重写 View 的这些方法或者实现 View.AccessibilityDelegate 来解决一些特殊场景下 TalkBack 播报的问题。

其中的 sendAccessibilityEventUnchecked 方法会向上传递到 ViewRootImpl 的 requestSendAccessibilityEvent 方法中,从堆栈信息中就可以证实这一点:

接着无障碍事件会通过 AccessibilityManager 的 sendAccessibilityEvent 方法跨进程调用 system_process 进程的 AccessibilityManagerService,将 AccessibilityEvent 事件传递到 TalkBack 的 TalkBackService 中。

4.无障碍事件的执行流程

这一节主要分析从 TalkBack 发出无障碍事件,到被辅助 app 在屏幕上绘制出绿框的过程。

TalkBack 将无障碍事件发送给被辅助 APP 时,需要 system_process 进程作为中转,对应的接口为 IAccessibilityServiceConnection.aidl 和 IAccessibilityInteractionConnection.aidl。经过中转后,最终会调用到被触摸 View 的 performAccessibilityAction 方法中,在没有 delegate 的情况下,会执行 performAccessibilityActionInternal 方法。在该方法中,如果是 ACTION_ACCESSIBILITY_FOCUS 事件,会执行 requestAccessibilityFocus 方法:

这个方法会执行两个关键操作:

  1. 调用 ViewRootImpl 的 setAccessibilityFocus 方法将自身设置为 focus,然后调用 invalidate() 触发重绘操作,ViewRootImpl 会在 onPostDraw 方法中执行 drawAccessibilityFocusedDrawableIfNeeded 来绘制绿框。

  1. 调用 sendAccessibilityEvent 方法,将 TYPE_VIEW_ACCESSIBILITY_FOCUSED 事件发送出去,这个事件被 talkback 接收后,会调用朗读引擎 TTS 读出 View 的内容,实现了无障碍模式下对触摸区域内容的播报。

无障碍功能实现实例

  • Case 1:无障碍模式下点击 View 播报“未加标签”

解决方案:在该 View 的 android:contentDescription 属性上设置需要播报的 String。

  • Case 2:焦点过多,需要删除多余焦点或需要某个 View 能够进行播报

解决方案:将不需要播报的 View 的 android:importantForAccessibility 属性设置为 no,将需要播报的 View 的该属性设置为 yes。

  • Case 3:无障碍模式下在上层页面点击仍能选中下层 View

解决方案:将下层的根 View 的 android:importantForAccessibility 属性设置为"noHideDescendants"

  • Case 4:使用的自定义 Toast 不播报内容

解决方案:在自定义 Toast 展示的时候,主动发送一个 AccessibilityEvent 事件

mText.postDelayed(new Runnable() {@Override public void run() {mText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);}}, 1);

设置延时是为了避免不生效的问题。

  • Case 5:设置自定义 View 的播报内容

解决方法:override View 的 onPopulateAccessibilityEvent()方法。

举例:设置自定义 View 开/关状态(已开启/已关闭)的播报内容。

@Overridepublic void onPopulateAccessibilityEvent(AccessibilityEvent event) {super.onPopulateAccessibilityEvent(event);final CharSequence text = isChecked() ? "已开启" : "已关闭";if (text != null) {event.getText().add(text);}}
  • Case 6:设置自定义 View 播报的控件类型及选中状态

解决方法:使用 AccessibilityDelegate

ViewCompat.setAccessibilityDelegate(targetView, new AccessibilityDelegateCompat() {@Overridepublic void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {super.onInitializeAccessibilityNodeInfo(host, info);info.setRoleDescription("标签类型");//设置播报的标签类型info.setCheckable(true);info.setChecked(checked);//设置播报的被选中状态}});

加入我们

欢迎加入抖音-关系与服务团队,我们专注于抖音多个核心业务场景的落地与迭代,在业务、架构、技术等方面都有投入,期待你的加入!

抖音-关系与服务团队正在热招 Android & iOS 研发,在北京,成都均有职位,欢迎投递简历!

  • 联系邮箱:liutianxiang.kid@bytedance.com

  • 邮件标题:简历-姓名-工作年限-期望工作地点

点个在看杀个 Bug ❤

抖音Android无障碍开发知识总结相关推荐

  1. 抖音Android包体积优化探索:从Class字节码入手精简DEX体积

    前言 众所周知,应用安装包的体积会十分影响用户的应用下载速度和安装速度.据 GoolgePlay 平台对外发布相关的包大小对转化率影响的数据,我们可以看到随着包大小的增加,安装转化率总体呈下降的趋势. ...

  2. 抖音Android岗面试性能优化篇之Rhea(新一代全能型性能分析工具)【速看】

    写在前面的话 性能优化这个知识点是很多大厂面试中都会问到的问题,尤其是想要面试抖音的Android岗的朋友. 用户交互响应的耗时,作为 Android 用户日常感知最深的一项性能指标,在日常开发中有着 ...

  3. Android 驱动(3)---Android驱动开发知识储备

    Android驱动开发知识储备 Android软件层次结构 (1)操作系统层 显示驱动(Frame Buffer),Flash内存驱动,照相机驱动,音频驱动,WiFi驱动,键盘驱动,蓝牙驱动,Bind ...

  4. 抖音 Android 基础技术大揭秘!

    从2016年9月的1.0版本上线至今,抖音在6年间实现了从零开始的快速增长.这短短6年伴随的是移动开发技术与云计算.机器学习等技术融合带来的技术落地新形式,也遇到了直播.连麦等新的用户需求所带来的产品 ...

  5. 抖音 Android 性能优化:新一代全能型性能分析工具 Rhea!

    本文选自「抖音 Android 性能优化」系列文章. 「抖音 Android 性能优化」系列文章是由抖音 Android 基础技术部门技术专家倾力打造的技术干货内容,和大家分享基础技术团队在打造极致用 ...

  6. 抖音 Android 性能优化系列:新一代全能型性能分析工具 Rhea

    本文选自「抖音 Android 性能优化」系列文章. 「抖音 Android 性能优化」系列文章是由抖音 Android 基础技术部门技术专家倾力打造的技术干货内容,和大家分享基础技术团队在打造极致用 ...

  7. 抖音 Android 基础技术大揭秘!

    从2016年9月的1.0版本上线至今,抖音在6年间实现了从零开始的快速增长.这短短6年伴随的是移动开发技术与云计算.机器学习等技术融合带来的技术落地新形式,也遇到了直播.连麦等新的用户需求所带来的产品 ...

  8. 抖音小程序开发:CEO们涌进直播间带货

    抖音小程序开发:CEO们涌进直播间带货 一场大型实验正在百万CEO中展开. 原本在后方排兵布阵的CEO们,齐刷刷地把战场迁移到直播间,他们不讲段子,没有强大的粉丝基数,很多人第一次开播还有些不适应. ...

  9. 2021年2月8日 抖音直播后端开发实习面经

    title: 2021年2月8日 抖音直播后端开发实习面经 tags: 面经 2021年2月8日 抖音直播后端开发实习面经 2021-2-8 抖音直播后端开发实习面经 岗位:后端开发实习岗 自我介绍~ ...

最新文章

  1. b-blkid查看磁盘设备文件系统类型
  2. ireport各个版本的下载地址分享
  3. 学习笔记(十二)——虚拟机安装和pycharm远程连接Ubuntu
  4. Android 检查设备是否存在 导航栏 NavigationBar
  5. (第五篇)Linux操作系统基本结构介绍
  6. C# webBrowser禁止在新窗口打开,强制在本窗口打开
  7. Oceanus:基于Apache Flink的一站式实时计算平台
  8. [css] 行内css和important哪个优先级高?
  9. LeetCode LCP 06. 拿硬币
  10. 管理与决策这属于计算机在什么方面的应用,提供参考11级<计算机应用基础>...
  11. 语言兔子繁衍问题讲解_颍湄脞録兔子不搁那窝里
  12. CSS和CSS3中的伪元素和伪类(总结)
  13. win7如何设置wifi热点_win7电脑本地连接连不上怎么办?详细教您如何设置本地连接...
  14. tab点击一个按钮切换排序_Axure8原型教程:实现Tab选项切换
  15. vue把数据导出为Excel表格的方法
  16. 在线JSON转Excel
  17. go 对象json转map
  18. [ERP/鼎捷E10][销售分销]发出商品余额表取数逻辑及SQL
  19. 手机麦克风声音太大_让手机麦克风声音变大的软件
  20. 在Openwrt 上使用迅雷远程下载功能

热门文章

  1. FFMPEG录屏(3)----捕获系统声音和麦克风
  2. 科三考试邢台市交安考试路线
  3. 如何实现一篇数据新闻报道
  4. 读《主角》笔记,摘记秦腔皇后忆秦娥的半生浮沉
  5. 龙墟界域 鸿蒙界域,妖神记妖神记这个等级划分全面 看漫画
  6. word从任意页开始加页码
  7. 解决微信公众账号申请认证方面的问题
  8. python 判断excel单元格为空_用python检测空白Excel单元格
  9. 机器自动翻译古文拼音 - 宋词 - 桂枝香 金陵怀古 王安石
  10. javaweb_会话管理(sessionCookie)