点击事件的埋点,需要处理两个问题:
- 如何将业务模块跟埋点模块隔离,减少对业务模块的侵入;
- 基于第一点,怎么才能获取获取业务模块的属性呢,利用activity 的pageId。
1. 业务模块跟埋点模块如何隔离
基本思路:利用ActivityLifecycleCallbacks对Activity的生命周期进行监听,在其创建时获取设置了点击事件的View,当触发了点击事件,则上报相关信息。 只要遍历decorView就可以找出符合条件的view,那么如何监听点击事件呢?
方式1:代理原有的点击事件监听;
方式2:设置setAccessibilityDelegate监听,这是在点击事件后会触发的事件,我采取的是这种方式;
View源码如下:
ini
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.TYPE_VIEW_CLICKED);//这是我们的切入点
return result;
}
2. 业务模块如何暴露相关属性
方式1:业务模块实现自定义接口,对外暴露相关属性;
方式2:业务模块利用view的setTag函数配置相关属性,我采取的是这种方式;
3.代码实现
ClickEventHelper 是点击事件处理的工具类,负责对符合条件的view绑定监听事件以及对上报前进行业务处理;
- 注释1:业务属性实体类,,可根据实际情况进行定义,pageId为view所属的页面id,viewId为view表示,有别于xml 的id;
- 注释2:负责查找设置了绑定事件的view,以及设置accessibilityDelegate监听事件;
- 注释3:适配fragment的场景;
- 注释4:查找设置了绑定事件的view,内部用到了递归,注意ViewGroup也可能设置了点击事件;
- 注释5:给view设置accessibilityDelegate监听;
- 注释6:处理监听回调,通过tag提取业务属性,然后进行业务处理;
kotlin
class ClickEventHelper {
data class ViewParams(val pageId: Int, val viewId: Int)//1
companion object {
const val TAG = "ClickEventHelper"
}
fun bindAccessibilityDelegateToViews(activity: Activity) {//2
val hasOnClickListenersViewList = mutableListOf<View>()
val rootView = activity.window.decorView as ViewGroup
rootView.forEach {
findHasClickListenerView(it, hasOnClickListenersViewList)
}
hasOnClickListenersViewList.forEach {
bindToView(it)
}
}
fun bindAccessibilityDelegateToViews(fragment: Fragment) {//3
val hasOnClickListenersViewList = mutableListOf<View>()
val rootView = fragment.requireView() as ViewGroup
rootView.forEach {
findHasClickListenerView(it, hasOnClickListenersViewList)
}
hasOnClickListenersViewList.forEach {
bindToView(it)
}
}
private fun findHasClickListenerView(parentView: View, list: MutableList<View>) {//4
when (parentView) {
is ViewGroup -> {
if (parentView.hasOnClickListeners()) {//layout 也有可能有点击事件
list.add(parentView)
}
parentView.forEach {
findHasClickListenerView(it, list)
}
}
else -> {
if (parentView.hasOnClickListeners()) {
list.add(parentView)
}
}
}
}
private fun bindToView(view: View) {//5
view.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(view: View, eventType: Int) {//6
val p = view.tag as? ViewParams
p?.apply {
val pageId = p.pageId
val viewId = p.viewId
//TODO 做相应的业务处理
//TODO 埋点上报
}
}
}
}
}
对ActivityLifecycleCallbacks的实现就比较简单了,监听Activity的创建,调用ClickEventHelper对应的函数;因为无法监听Fragment的创建,所以需要在其生命周期函数中调用bindAccessibilityDelegateToViews函数。 大家是否注意到注释2,用到了协程,为什么呢?其实跟onActivityCreated的回调时机有关。onActivityCreated函数比Activity的onCreate先执行,使用协程是保证bindAccessibilityDelegateToViews函数在onCreate执行完再触发,因为一般都是在onCreate函数中对view setTag。
kotlin
class AppActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
private val clickEventHelper = ClickEventHelper()
override fun onActivityCreated(p0: Activity, p1: Bundle?) {//1
val lifecycleScope = ((p0 as? LifecycleOwner)?.lifecycleScope) ?: return
clickEventHelper.bindAccessibilityDelegateToViews(p0)
lifecycleScope.launchWhenCreated {//2
clickEventHelper.bindAccessibilityDelegateToViews(p0)
}
}
override fun onActivityStarted(p0: Activity) {}
override fun onActivityResumed(p0: Activity) {}
override fun onActivityPaused(p0: Activity) {}
override fun onActivityStopped(p0: Activity) {}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
override fun onActivityDestroyed(p0: Activity) {}
}
使用方式:在Activity或Fragment对view设置tag即可;
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.nameTv.setOnClickListener {
Log.d("MainActivity","点击了")
}
binding.nameTv.setTag(ClickEventHelper.ViewParams(10, 1010))
}
}
4.优化空间
- 上述代码只对设置了OnClickListener监听的view进行了处理,如果需要处理其他点击事件,例如CheckBox、RadioButton,对findHasClickListenerView函数进行扩展即可;
- 如果页面中view太多,可考虑异步处理查找的逻辑;
- 可以使用HashMap缓存符合条件的view,key为activity、fragment全类名,value为view集合,这样可以防止同一个页面重复查找。
有需要的同学,可以直接拿到走哈。