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

抖音无障碍背景

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

无障碍模式下的使用方法

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

常用的操作手势:

浏览某个View:单击

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

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

本文的目的

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

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

无障碍功能实现原理系统结构

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

被辅助app-SystemServer()

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

SystemServer-辅助app()

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

辅助app-SystemServer()

SystemServer-被辅助app()

当需要被辅助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负责将普通触摸事件转换为触摸浏览手势,例如将_DOWN事件转换为_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()方法:

publicfinalbooleandispatchPointerEvent(MotionEventevent){if(()){returndispatchTouchEvent(event);}else{returndispatchGenericMotionEvent(event);}}

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

boolMotionEvent::isTouchEvent(int32_tsource,int32_taction){if(sourceAINPUT_SOURCE_CLASS_POINTER){//SpecificallyexcludesHOVER_(actionAMOTION_EVENT_ACTION_MASK){caseAMOTION_EVENT_ACTION_DOWN:caseAMOTION_EVENT_ACTION_MOVE:caseAMOTION_EVENT_ACTION_UP:caseAMOTION_EVENT_ACTION_POINTER_DOWN:caseAMOTION_EVENT_ACTION_POINTER_UP:caseAMOTION_EVENT_ACTION_CANCEL:caseAMOTION_EVENT_ACTION_OUTSIDE:returntrue;}}returnfalse;}

符合以上case的event即为TouchEvent。

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

@OverridepublicbooleandispatchTouchEvent(MotionEventev){=();returncb!=null!()mFeatureId0?(ev):(ev);}

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

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

@OverridepublicbooleandispatchGenericMotionEvent(MotionEventev){=();returncb!=null!()mFeatureId0?(ev):(ev);}

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

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

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

3.1普通模式的事件分发

3.1.1普通模式下事件分发KeyMethod

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

publicbooleandispatchTouchEvent(MotionEventev)

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

publicbooleanonInterceptTouchEvent(MotionEventev)

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

publicbooleanonTouchEvent(MotionEventevent)

3.1.2普通模式下的事件分发

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

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

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

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

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

protectedbooleandispatchHoverEvent(MotionEventevent)

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

publicbooleanonInterceptHoverEvent(MotionEventevent)

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

publicbooleanonHoverEvent(MotionEventevent)

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

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

publicfinalbooleandispatchPointerEvent(MotionEventevent){if(()){returndispatchTouchEvent(event);}else{returndispatchGenericMotionEvent(event);}}

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

publicbooleandispatchGenericMotionEvent(MotionEventevent){···finalintsource=();if((_CLASS_POINTER)!=0){finalintaction=();//判断事件类型属于Hover,调用dispatch方法开始进行分发if(action==_HOVER_ENTER||action==_HOVER_MOVE||action==_HOVER_EXIT){if(dispatchHoverEvent(event)){returntrue;}}returnfalse;}

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

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

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

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

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

sAccessibilityEvent

sAccessibilityEventUnchecked

onInitializeAccessibilityEvent

dispatchPopulateAccessibilityEvent

onPopulateAccessibilityEvent

onRequestSAccessibilityEvent(仅在ViewGroup中有默认实现)

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

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

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

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

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

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

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

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

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

无障碍功能实现实例

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

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

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

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

Case4:使用的自定义Toast不播报内容

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

(newRunnable(){@Overridepublicvoidrun(){(_VIEW_HOVER_ENTER);}},1);

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

Case5:设置自定义View的播报内容

解决方法:overrideView的onPopulateAccessibilityEvent()方法。

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

@OverridepublicvoidonPopulateAccessibilityEvent(AccessibilityEventevent){(event);finalCharSequencetext=isChecked()?"已开启":"已关闭";if(text!=null){().add(text);}}

Case6:设置自定义View播报的控件类型及选中状态

解决方法:使用AccessibilityDelegate

(targetView,newAccessibilityDelegateCompat(){@OverridepublicvoidonInitializeAccessibilityNodeInfo(Viewhost,AccessibilityNodeInfoCompatinfo){(host,info);("标签类型");//设置播报的标签类型(true);(checked);//设置播报的被选中状态}});
加入我们

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

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

联系邮箱:@

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

版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。

相关推荐