Window.Callback 代理方案是在更底层——Window 级别拦截所有触摸事件,从中识别点击行为并完成埋点。相比 OnClickListener 代理,它覆盖了所有 View(包括动态创建和无 Listener 的 View),且不依赖反射操作私有字段。
一、核心原理 每个 Activity/Dialog 都持有一个 Window,Window 内部有一个 Window.Callback 接口,负责分发按键、触摸、焦点等事件。Activity 和 Dialog 都实现了该接口。Android 中的触摸事件链路:
DecorView.dispatchTouchEvent() → Window.Callback.dispatchTouchEvent() → Activity.dispatchTouchEvent() → PhoneWindow.superDispatchTouchEvent() → DecorView.superDispatchTouchEvent() → ViewGroup.dispatchTouchEvent()
二、实现代码 class TrackingWindowCallback ( private val origin: Window.Callback ) : Window.Callback by origin { override fun dispatchTouchEvent (event: MotionEvent ?) : Boolean { event ?: return origin.dispatchTouchEvent(event) if (event.action == MotionEvent.ACTION_UP) { handleClickEvent(event) } return origin.dispatchTouchEvent(event) } private fun handleClickEvent (event: MotionEvent ) { val decorView = (origin as ? Activity)?.window?.decorView ?: return val targetView = findTargetView(decorView, event.rawX.toInt(), event.rawY.toInt()) targetView?.let { track(it) } } private fun findTargetView (parent: ViewGroup , x: Int , y: Int ) : View? { for (i in parent.childCount - 1 downTo 0 ) { val child = parent.getChildAt(i) val loc = IntArray(2 ) child.getLocationOnScreen(loc) if (x in loc[0 ]..(loc[0 ] + child.width) && y in loc[1 ]..(loc[1 ] + child.height)) { if (child is ViewGroup) return findTargetView(child, x, y) ?: child return child } } return parent } private fun track (view: View ) { AnalyticsSDK.track("app_click" , mapOf( "view_id" to view.id, "class" to view.javaClass.simpleName, "text" to ((view as ? TextView)?.text?.toString() ?: "" ), "resource_name" to view.resources.getResourceEntryName(view.id) )) } }
三、在 Application 中全局注入 class TrackingApplication : Application () { override fun onCreate () { super .onCreate() registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated (activity: Activity , bundle: Bundle ?) { val origin = activity.window.callback activity.window.callback = TrackingWindowCallback(origin) } }) } }
四、与 OnClickListener 方案对比
维度
OnClickListener 代理
Window.Callback 代理
覆盖度
仅覆盖有 Listener 的 View
覆盖所有 View 点击
实现复杂度
简单(需反射)
中等(需坐标计算)
性能开销
极低
坐标遍历有开销
扩展性
仅点击
可扩展手势/长按
多窗口支持
需单独处理
Window 级别天然支持
面试常考问题 Q1:Window.Callback 的代理委托模式(by)有什么优势?
Kotlin 的 by 委托让代理类无需手动实现 Callback 的所有方法,未覆盖的方法自动转发给原始 Callback,代码量极简,只重写需要拦截的 dispatchTouchEvent。
Q2:如何区分 click 和 scroll?
通过 event.action 判断:记录 ACTION_DOWN 位置,ACTION_UP 时计算位移距离。若 sqrt(dx^2 + dy^2) < touchSlop(系统默认 8dp),判定为点击;否则为滑动。Android 的 ViewConfiguration.getScaledTouchSlop() 获取系统阈值。
Q3:Dialog/PopupWindow 如何覆盖?
Dialog 有自己的 Window,需要单独 Hook。可通过 Dialog.setContentView() 时替换其 Window.Callback,或在 Application 级别 Hook Dialog 的创建。PopupWindow 使用 WindowManager.addView() 添加窗口,需通过 WindowManagerGlobal 的代理来拦截。AOSP 源码路径:Dialog.java 的 mWindow 属性和 Window.Callback 接口定义于 frameworks/base/core/java/android/view/Window.java。