百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

抖音Android无障碍开发知识总结

ztj100 2024-12-07 18:45 65 浏览 0 评论

抖音无障碍背景

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

无障碍模式下的使用方法

抖音的无障碍功能实现主要是通过开启 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
  • 邮件标题:简历-姓名-工作年限-期望工作地点

相关推荐

离谱!写了5年Vue,还不会自动化测试?

前言大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。Playwright是一个功能强大的端到...

package.json 与 package-lock.json 的关系

模块化开发在前端越来越流行,使用node和npm可以很方便的下载管理项目所需的依赖模块。package.json用来描述项目及项目所依赖的模块信息。那package-lock.json和...

Github 标星35k 的 SpringBoot整合acvtiviti开源分享,看完献上膝盖

前言activiti是目前比较流行的工作流框架,但是activiti学起来还是费劲,还是有点难度的,如何整合在线编辑器,如何和业务表单绑定,如何和系统权限绑定,这些问题都是要考虑到的,不是说纯粹的把a...

Vue3 + TypeScript 前端研发模板仓库

我们把这个Vue3+TypeScript前端研发模板仓库的初始化脚本一次性补全到可直接运行的状态,包括:完整的目录结构所有配置文件研发规范文档示例功能模块(ExampleFeature)...

Vue 2迁移Vue 3:从响应式到性能优化

小伙伴们注意啦!Vue2已经在2023年底正式停止维护,再不升级就要面临安全漏洞没人管的风险啦!而且Vue3带来的性能提升可不是一点点——渲染速度快40%,内存占用少一半,更新速度直接翻倍!还在...

VUE学习笔记:声明式渲染详解,对比WEB与VUE

声明式渲染是指使用简洁的模板语法,声明式的方式将数据渲染进DOM系统。声明式是相对于编程式而言,声明式是面向对象的,告诉框架做什么,具体操作由框架完成。编程式是面向过程思想,需要手动编写代码完成具...

苏州web前端培训班, 苏州哪里有web前端工程师培训

前端+HTML5德学习内容:第一阶段:前端页面重构:PC端网站布局、HTML5+CSS3基础项目、WebAPP页面布局;第二阶段:高级程序设计:原生交互功能开发、面向对象开发与ES5/ES6、工具库...

跟我一起开发微信小程序——扩展组件的代码提示补全

用户自定义代码块步骤:1.HBuilderX中工具栏:工具-代码块设置-vue代码块2.通过“1”步骤打开设置文件...

JimuReport 积木报表 v1.9.3发布,免费可视化报表

项目介绍积木报表JimuReport,是一款免费的数据可视化报表,含报表、大屏和仪表盘,像搭建积木一样完全在线设计!功能涵盖:数据报表、打印设计、图表报表、门户设计、大屏设计等!...

软开企服开源的无忧企业文档(V2.1.3)产品说明书

目录1....

一款面向 AI 的下一代富文本编辑器,已开源

简介AiEditor是一个面向AI的下一代富文本编辑器。开箱即用、支持所有前端框架、支持Markdown书写模式什么是AiEditor?AiEditor是一个面向AI的下一代富文本编辑...

玩转Markdown(2)——抽象语法树的提取与操纵

上一篇玩转Markdown——数据的分离存储与组件的原生渲染发布,转眼已经鸽了大半年了。最近在操纵mdast生成md文件的时候,心血来潮,把玩转Markdown(2)给补上了。...

DeepseekR1+ollama+dify1.0.0搭建企业/个人知识库(入门避坑版)

找了网上的视频和相关文档看了之后,可能由于版本不对或文档格式不对,很容易走弯路,看完这一章,可以让你少踩三天的坑。步骤和注意事项我一一列出来:1,前提条件是在你的电脑上已配置好ollama,dify1...

升级JDK17的理由,核心是降低GC时间

升级前后对比升级方法...

一个vsCode格式化插件_vscode格式化插件缩进量

ESlint...

取消回复欢迎 发表评论: