Android点击、曝光事件的监听分散在代码的各个角落, 不利于开发维护。
其实可以将这2种行为收敛到一起, 运行时监听Activity的生命周期,即ActivityLifeCallBack回调; 在onResume或onStart函数里判断当前acitivity是否添加了自定义ViewGroup(暂且命名为TrackerViewGroup), 如果未添加则添加;
在onDestory函数中移除该自定义View, 即其父View调用removeView。
一、触发曝光的方法
添加一层ViewGroup目的是监听滑动事件和View显示状态变更; 从而将曝光埋点的触发时机收敛到自定义View中。
1、activity切换前台会执行dispatchWindowFocusChanged函数;
2、手指在屏幕上滑动时dispatchToutchEvent函数里监听ACTION_MOVE, 并触发检索当前视图曝光流程, 每次只曝光差异View(从可见到不可见)并刷新缓存为当前可见view;
3、实现GestureDector的目的是监听Fling行为;
4、当子View变化时,例如ListView/RecyclerView滑入滑出item, 当前视图中的TextView执行setText操作也会触发父容器的onLayout。
public class TrackerViewGroup extends FrameLayout implements GestureDetector.OnGestureListener {
....
public TrackerViewGroup(Context context) {
super(context);
this.mGestureDetector = new GestureDetector(context, this); //监听Fling事件
}
public TrackerFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override public boolean dispatchTouchEvent(MotionEvent ev) {
mGestureDetector.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
....
break;
case MotionEvent.ACTION_MOVE:
//判断滑动距离超过阈值时,Activity递归判断当前哪些View可见并跟上次缓存相比较。 每次曝光差异的View
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//触发曝光流程且只曝光与上次的差异view
super.onLayout(changed, left, top, right, bottom);
}
@Override public boolean onDown(MotionEvent motionEvent) {
return false;
}
//GestureDetector无法判断滑动结束即IDLE,只触发Fling开始事件
@Override public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v,
float v1) {
this.postDelayed(new Runnable() {
@Override public void run() {
//触发曝光流程, 即递归判断当前Activity哪些View可见。 只曝光差异部分
}
}, 1000); //经验值
return false;
}
@Override public void dispatchWindowFocusChanged(boolean hasFocus) {
//曝光当前activity所有可见View, 不判断与上次曝光view的差异
super.dispatchWindowFocusChanged(hasFocus);
}
@Override protected void dispatchVisibilityChanged(View changedView, int visibility) {
//曝光当前activity所有可见View, 不判断与上次曝光view的差异
super.dispatchVisibilityChanged(changedView, visibility);
}
Activity包含ViewPager时需要特殊处理, 在递归遍历TrackerViewGroup子View/ViewGroup时,如果是ViewPage要添加滑动监听, 从而能够在ViewPager切换时触发曝光。
View从可见到不可见时触发曝光逻辑。例如ListView/RecyclerView划入item时并不触发曝光, 而是在划出屏幕、按home键、启动/关闭activity时触发item曝光逻辑;
二、监听点击事件
View.java的performClick函数会判断mAccessibilityDelegate非空时执行对应的回调, 在点击事件时发送AccessibilityEvent.TYPE_VIEW_CLICKED。 所以监听View的点击事件只需要实例化一个AccessibilityDelegate即可。
/**
* Delegate for injecting accessibility functionality.
*/
AccessibilityDelegate mAccessibilityDelegate;
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;
}
三、如何绑定数据
当递归判断某个View已显示时需要执行曝光逻辑, 曝光的参数可以关联到View上, 即通过View.setTag(id, object)的方式实现。 定义2个id值, 分别存储View的唯一标识和曝光参数(通常是Map类型)。 view绑定数据时例如:
view.setTag(R.id.trace_name, 进程唯一的值);
view.setTag(R.id.content, map);
曝光或点击需要取数据时view.getTag(R.id.content)。
四、遍历View
类似于递归子View, 以TrackerViewGroup为根view遍历所有子view/viewgroup, 并和缓存的view(所有设置了viewtag的view,在onLayout时)曝光状态做比较, 只能曝光差异部分; 遍历前要比较上次执行时间和当前时间间隔,低于阈值时返回(避免频繁递归导致性能问题)。
参考判断View是否可见, 显示比例可以设置阈值, 例如一个View显示了70%是否执行曝光逻辑等;
参考:
缓存Activity页面级别的参数,推荐使用静态WeakedHashMap, 解决代码层级太深、传值不便的情况。示例代码: static WeakHashMap
参考: WeakHashMap原理