Accessibility

Accessibility,即辅助功能。许多 Android 用户有不同的能力(限制),这要求他们可能会以不同的方式使用他们的 Android 设备 。这些限制包括视力、肢体、年龄等,这些 限制阻碍了他们看到或充分使用触摸屏,而用户的听力丧失,让他们可能无法感知声音 信息和警报。

Android 提供了辅助功能的特性和服务,帮助这些用户更容易的使用他们的设备,这些 功能包括语音合成、触觉反馈、手势导航、轨迹球和方向键导航。Android 应用程序开 发人员可以利用这些服务,使他们的应用程序更贴近用户。该辅助服务工作在后台,由 系统调用,用户界面的一些状态(比如Button 被点击了)的改变可以通过回调 Accessibilityservice 方法来通知。

比如下面的这个 Button:

< android.support.v7.widget.AppCompatButton
android:layoutwidth="wrapcontent"
android:layoutheight="wrapcontent" android:text=" 神策数据 "
android:contentDescription="SensorsData"/>

由于添加了 android:contentDescription 属性,当用户移动焦点到这个按钮或将鼠标悬停在它上面时,提供口头反馈的辅助功能服务就会发出“SensorsData”的声音。

View.AccessibilityDelegate

我们先看一下 View.java 的 performClick() 源码:

/**

*Call this view's OnClickListener, if it is defined.

Performs all normal

*actions associated with clicking: reporting accessibility event, playing

*a sound, etc.

*

*@return True there was an assigned OnClick- Listener that was called, false

*otherwise is returned.

*/

public boolean performClick() {

final boolean result;

final ListenerInfo li = mListenerInfo;

if (li != null && li.mOnClickListener != null) {

playSoundEffect(SoundEffectConstants.- CLICK);

li.mOnClickListener.onClick(this);

result = true;
} else {

result = false;

}

sendAccessibilityEvent(AccessibilityEvent.- TYPEVIEWCLICKED);

notifyEnterOrExitForAutoFillIfNeeded(true);

return result;

}

......

public void sendAccessibilityEvent(int eventType){
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilit-
yEvent(this, eventType);
} else {

sendAccessibilityEventInternal(eventType);
}

}

......

public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate){

mAccessibilityDelegate = delegate;

}

从上面的源码可以很容易的看出来,当一个 View 被点击的时候,系 统会先调用当前 View 已设置的 mOnClickListener 的 onClick(view)方法,然后再调用 sendAccessibilityEvent(AccessibilityEvent.- TYPEVIEWCLICKED) 的内部方法。在 sendAccessibili- tyEvent(int eventType) 方法的内部实现里,其实是调用 mAccessibilityDelegate 的 sendAccessibilityEvent 方 法,并传入当前 View 对象和 AccessibilityEvent.- TYPEVIEWCLICKED。因此,我们只需要代理 View 的 mAccessibilityDelegate,当一个 View 被点击时,在原有 mOnClickListener 执行之后,我们就能收到“消息”。 代理 mAccessibilityDelegate 之后,我们就能拿到当前被点击的 View 对象,从而可以加入自动埋点的逻辑,达到 自动埋点的效果。

原理概述

在应用程序自定义的 Application 的 onCreate() 方法中初始化埋点 SDK,并传入当前的 Application 对象。SDK 就可以拿到这个 Application 对象,然后我们就可以通过 application.registerActivityLifecycleCallback 这个方法来注 册 Application.ActivityLifecycleCallbacks。这样 SDK 就可以对 App 中所有的 Activity 的生命周期事件进行集中处理(监 控)了。在 ActivityLifecycleCallbacks 的 onActivityResumed(Activity activity, Bundle bundle) 方法中,我们可以拿到当前正在显示的 Activity 对象,然后再通过 activity.getWindow().getDecorView() 方法或者 activity.findViewById(android.R.id.content) 方法拿到当前 Activity 的 RootView,通过 rootView.get- ViewTreeObserver() 方法可以拿到 RootView 的 ViewTreeObserver 对象,然后再通过 addOnGlobalLay- outListener() 方法给 RootView 注册 ViewTreeObserv- er.OnGlobalLayoutListener 监听器,这样我们就可以在收到当前 Activity 的视图状态发生改变时去主动遍历一次 RootView,并用我们自定义的 SensorsDataAccessibili- tyDelegate 代理当前 View 的 mAccessibilityDelegate 属性。在我们自定义的 SensorsDataAccessibilityDelegate 文件中的 public void sendAccessibilityEvent(View host, int eventType) 方法中,我们先调用原有的 mAccessibili- tyDelegate 的 sendAccessibilityEvent 方法,然后再插入埋点代码,其中 host 即是当前被点击的 View 对象,从而 可以做到自动埋点的效果。

实现步骤

完整的项目源码后续会 release 给大家。

缺点

• Application.ActivityLifecycleCallbacks 要求 API 14+ • view.hasOnClickListeners() 要求 API 15+ • 无法采集 Dialog、PopupWindow 的点击事件 • 每次点击都需要遍历一次 RootView,效率比较低

知识点

• Application.ActivityLifecycleCallbacks • Android 系统事件处理机制 • View Elevation

参考资料

[1]https://github.com/foolchen/AndroidTracker

注:该内容来自神策数据用户行为洞察研究院出品的《Android 全埋点解决方案》白皮书,查看完整白皮书可点击《Android 全埋点解决方案》

更多白皮书、报告、干货和案例,可以关注“神策数据”和“用户行为洞察研究院”公众号了解~