IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 抖音Android无障碍开发知识总结 -> 正文阅读

[移动开发]抖音Android无障碍开发知识总结

抖音无障碍背景

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

无障碍模式下的使用方法

抖音的无障碍功能实现主要是通过开启 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() 方法中:

@Override

public?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() 方法中,也有类似的代码逻辑:

@Override

public?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 开/关状态(已开启/已关闭)的播报内容。

@Override

public?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()?{

????@Override

????public?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 ?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-15 15:43:16  更:2021-08-15 15:43:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 11:05:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码