本文相关代码Github地址autotrace_app_click,有帮助的话Star一波吧。
[TOC]
你在开发中是否遇到过这样的场景,当点击同一个dialog
或者button
的时候,如果暴击多次,该dialog
或button
的被点击行为会被瞬间执行多次,这时候有小伙伴可能要想了,我可以做一个view
时间戳呀,让它延迟生效。
可是你们有木有想过一个问题,这么做?是不是会绑定view
?如果工程里面有10000个点击事件需要处理,那岂不是要改10000行代码,有没有一种优雅的方式能实现如下需求呢?
- 1.1 可以设置全局控制点击行为的开关
- 1.2 可以动态控制点击时间戳
- 1.3 对
butterknife
,kotlin
,lambada
表达式,dialog
,xml
点击事件高度支持 - 1.4 对不需要防重复点击的事件,可以通过注解进行额外逻辑处理
- 1.5 可以借助沪江
aop
思想,也可以自己编写transform plugin
实现 - 1.6 将打点事件带到线上,线上分析用户行为
小朋友,你是否有很多问号?
本文介绍的内容会详细解释以上问题,并在最后给解答。稳住,别慌~
- 在app的
onCreate()
方法初始化SDK
,然后在ActivityLifeCycleCallBack
注册,这样就能监控activity
所有生命周期方法了。
- 我们在该回调事件里面的对应生命周期方法如:
onActivityPaused(Activity activity)
,通过activity.findViewById(android.R.id.content)
方法可以拿到整块区域所对应的rootview
,其实就是framelayout
- 2.0.3.1 自定义
OnClickListener
派生类WrapperOnClickListener
,实现OnClickListener
接口 - 2.0.3.2 逐层递归遍历
rootview
,判断当前view
是否设置了mOnClickListener
对象- 2.0.3.2.1 如果已经设置了
mOnClickListener
并且mOnClickListener
不是我们自定义的WrapperOnClickListener
类型,则通过WrapperOnClickListener
代理当前view
设置的mOnClickListener
- 2.0.3.2.1 如果已经设置了
- 2.0.3.3 在
WrapperOnClickListener
的onClick
方法里会先调用view
的原有mOnClickListener
处理逻辑- 调用埋点代码,实现"插入"埋点,达到自动埋点效果。
public interface ITrackClickEvent {
/**
* 控件的类型
*/
String CANONICAL_NAME = "$element_type";
/**
* 控件的id,即android:id属性指定的值
*/
String VIEW_ID = "$element_id";
/**
* 控件显示的文本信息
*/
String ELEMENT_CONTENT = "$element_content";
/**
* 当前控件所属的 Activity 页面
*/
String ACTIVITY_NAME = "$activity";
/**
* 点击空间行为事件名称
*/
String APP_CLICK = "$AppClick";
String APP_VERSION = "$app_version";
String APP_NAME = "$app_name";
String SCREEN_HEIGHT = "$screen_height";
String SCREEN_WIDTH = "$screen_width";
String ELEMENT_POSITION = "$element_position";
String ELEMENT_ID = "$element_id";
String ELEMENT_ELEMENT = "$element_element";
String MODEL = "$model";
String LIB_VERSION = "$lib_version";
String OS = "$os";
String OS_VERSION = "$os_version";
String MANUFACTURER = "$manufacturer";
String LIB = "$lib";
}
DataBinding
框架给Button
设置OnClickListener
对象动作稍微晚于onActivityResumed
回调方法,DataBinding
还没来得及给我们Button
对象设置mOnClickListener
对象,我们再遍历RootView
的时,当前View
不满足hasObClickListener
的判断条件,因此没有去代理mOnClickListener
对象,给出的解决方案是给DataBinding
框架一点延迟事件处理设置mOnClickListener
对象操作
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
delegateViewsOnClickListener(activity, activity.findViewById(android.R.content));
}
},300);
我们通过android.R.content
获取的RootView
不包含Activity
标题栏,其实也就是不包含MenuItem
所对应的父容器的,自然当我们遍历RootView
是无法获取MenuItem
控件的,因此也无法代理mOnClickListener
对象,间接导致MenuItem
点击事件无法触发。我们可以借助DecorView
来处理,那么什么是DecorView
呢?
官方解释是这样的:
The DecorView is the view that actually holds the window’s background drawable. Calling getWindow().setBackgroundDrawable() from your Activity changes the background of the window by changing the DecorView‘s background drawable. As mentioned before, this setup is very specific to the current implementation of Android and can change in a future version or even on another device.
我的理解是: DecorView
是整个Window
最顶层View
,他有且只有一个字孩子LinearLayout
,其实就包括了通知栏,标题栏,内容显示栏,LinearLayout
里面包括两个FrameLayout
,第一个是标题栏显示的Title,第二个FrameLayout
才是我们所说的android.R.content
。所以针对我么上面提到的无法采集MenuItem
点击事件的问题,我们只需要将activity.findViewById(android.R.content)
换成 activity.getWindow().getDecorView()
就可以采集到MenuItem
的点击事件了
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
delegateViewsOnClickListener(activity, activity.getWindow().getDecorView());
}
},300);
}
但是上报的时候,我们发现却没有Button文本信息,这是为什么呢?element_content
这个字段没问题呀,后面我查了一下,MenuView.ItemView
的派生类是ActionMenuItemView
,而非View
,所以这里需要强转一下
else if (view instanceof ActionMenuItemView) {
text = ((ActionMenuItemView) view).getText().toString();
}
ViewGroup rootView = findViewById(R.id.rootView);
AppCompatButton button = new AppCompatButton(this);
button.setText("动态创建的button");
button.setOnClickListener(v -> {
});
rootView.addView(button);
当点击这个动态创建的 rootView
,当前方案是无法采集点击事件的。为啥会这样呢?这是因为我们在Activity
的onResume
之前去遍历整个rootView
并代理其mOnClickListener
对象的,如果在onResume
动态创建View
当时肯定无法被遍历到的,后来我没没有再次遍历,所以它的mOnClickListener
对就没有被代理过,因此点击控件是没有效果的,那么该怎么办呢?用OnGlobalLayoutListener
来解决,什么是OnGlobalLayoutListener
?官方解释是这样的:
Interface definition for a callback to be invoked when the global layout state or the visibility of views within the view tree changes.
我的理解是: 当一个View
视图树发生改变的时候,我没给当前的View
设置了OnGlobalLayoutListener
监听器,就能回调 onGlobalLayout()
方法,基于这个原理我们可以给我们的Activity
的RootView
注册一个这样的监听器,这样就能实时观察视图树布局的变化呢,我们重新遍历一次RootView
,然后找到那些没有被代理过的OnGlobalLayoutListener
对象View
进行代理即可解决上面的问题
private ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;
@Override
public void onActivityCreated(@NonNull final Activity activity, @Nullable Bundle savedInstanceState) {
onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
delegateViewsOnClickListener(activity,
SensorsDataHelper.getRootViewFromActivity(activity, false));
}
};
}
@Override
public void onActivityResumed(@NonNull final Activity activity) {
SensorsDataHelper
.getRootViewFromActivity(activity, true)
.getViewTreeObserver()
.addOnGlobalLayoutListener(onGlobalLayoutListener);
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
/*
* 移除顶层Activity的监听
*/
SensorsDataHelper.getRootViewFromActivity(activity, false)
.getViewTreeObserver()
.removeOnGlobalLayoutListener(onGlobalLayoutListener);
}
// -----------------省-------------------------
}
((TextView) view).getText().toString();
view.getContentDescription().toString();
public class WrapperOnCheckedChangeListener implements CompoundButton.OnCheckedChangeListener {
private CompoundButton.OnCheckedChangeListener source;
public WrapperOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener source) {
this.source = source;
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
//调用原有的 OnClickListener
try {
if (source != null) {
source.onCheckedChanged(compoundButton, b);
}
} catch (Exception e) {
e.printStackTrace();
}
//插入埋点代码
SensorsDataPrivate.trackViewOnClick(compoundButton);
}
}
/**
* CheckBox 代理事件
*
* @param view compoundButton代理ui
*/
public void compoundButtonItemClick(View view) {
if (view instanceof CheckBox){
final CompoundButton.OnCheckedChangeListener onCheckedChangeListener =
SensorsDataHelper.getOnCheckedChangeListener(view);
if (onCheckedChangeListener != null &&
!(onCheckedChangeListener instanceof MkOnCheckedChangeListener)) {
((CompoundButton) view).setOnCheckedChangeListener(
new MkOnCheckedChangeListener(onCheckedChangeListener));
}
}
}
- 5.0.4.1 确定
RadioGroup
设置的Listener
类型OnGroupClickListener
,然后自定义MkOnGroupClickListener
public class MkOnGroupClickListener implements ExpandableListView.OnGroupClickListener {
private ExpandableListView.OnGroupClickListener source;
public MkOnGroupClickListener(ExpandableListView.OnGroupClickListener source) {
this.source = source;
}
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long id) {
SensorsDataManager.trackAdapterView(expandableListView, view, groupPosition, -1);
if (source != null) {
source.onGroupClick(expandableListView, view, groupPosition, id);
}
return false;
}
}
- 5.0.4.2 判断当前
View
是RadioGroup
if (view instanceof RadioGroup) {
try {
RadioGroup radioGroup = (RadioGroup) view;
Activity activity = SensorsDataHelper.getActivityFromView(view);
if (activity != null) {
int checkedRadioButtonId = radioGroup.getCheckedRadioButtonId();
RadioButton radioButton = activity.findViewById(checkedRadioButtonId);
if (radioButton != null) {
text = radioButton.getText().toString();
}
}
} catch (Exception e) {
Log.getStackTraceString(e);
}
}
- 5.0.4.3 如果反射已经设置了
mOnClickListener
,且mOnClickListener
不为空,比去不是我们自定义的MkOnGroupClickListener
- 通过
MkOnGroupClickListener
代理
- 通过
if (onGroupClickListener != null && !(onGroupClickListener instanceof MkOnGroupClickListener)) {
((ExpandableListView) view).setOnGroupClickListener(new MkOnGroupClickListener(onGroupClickListener));
}
public void seekBarItemClick(View view) {
final SeekBar.OnSeekBarChangeListener onSeekBarChangeListener =
SensorsDataHelper.getOnSeekBarChangeListener(view);
if (onSeekBarChangeListener != null &&
!(onSeekBarChangeListener instanceof MkOnSeekBarChangeListener)) {
((SeekBar) view).setOnSeekBarChangeListener(
new MkOnSeekBarChangeListener(onSeekBarChangeListener));
}
}
ListView
,GridView
是AdapterView
子类,设置代理逻辑判断都大同小异
- 5.0.8.1 设置
ListView/ GridView
的代理MkAdapterViewOnItemClick
/**
* 列表代理事件
*
* @param view gridView代理ui
*/
public void gridViewItemClick(View view) {
if (view instanceof ListView || view instanceof GridView){
AdapterView.OnItemClickListener onItemClickListener = ((AdapterView) view).getOnItemClickListener();
if (onItemClickListener != null && !(onItemClickListener instanceof MkAdapterViewOnItemClick)) {
((AdapterView) view).setOnItemClickListener(new MkAdapterViewOnItemClick(onItemClickListener));
}
}
}
- 5.0.8.1 自定义
MkAdapterViewOnItemClick
源码如下:
public class MkAdapterViewOnItemClick implements AdapterView.OnItemClickListener {
private AdapterView.OnItemClickListener source;
public MkAdapterViewOnItemClick(AdapterView.OnItemClickListener source) {
this.source = source;
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
if (source != null) {
source.onItemClick(adapterView, view, position, id);
}
SensorsDataManager.trackAdapterView(adapterView, view, position);
}
}
ExpandableListView
是AdapterView
的子类,同时也是ListView
的子类,ListView
的点击事件分为GroupClick
和ChildClick
,它设置的监听器也有两种,这里我们就需要增加额外的方式去做处理啦,我们来看看 ExpandableListView
的点击事件是如何处理的吧
public static void trackAdapterView(AdapterView<?> adapterView, View view, int groupPosition, int childPosition) {
try {
final JSONObject jsonObject = new JSONObject();
jsonObject.put(ITrackClickEvent.CANONICAL_NAME, adapterView.getClass().getCanonicalName());
jsonObject.put(ITrackClickEvent.ELEMENT_ID, SensorsDataHelper.getViewId(adapterView));
if (childPosition > -1) {
jsonObject.put(ITrackClickEvent.ELEMENT_POSITION, String.format(Locale.CHINA, "%d:%d", groupPosition, childPosition));
} else {
jsonObject.put(ITrackClickEvent.ELEMENT_POSITION, String.format(Locale.CHINA, "%d", groupPosition));
}
StringBuilder stringBuilder = new StringBuilder();
String viewText = SensorsDataHelper.traverseViewContent(stringBuilder, view);
if (!TextUtils.isEmpty(viewText)) {
jsonObject.put(ITrackClickEvent.ELEMENT_ELEMENT, viewText);
}
final Activity activity = SensorsDataHelper.getActivityFromView(adapterView);
if (activity != null) {
jsonObject.put(ITrackClickEvent.ACTIVITY_NAME, activity.getClass().getCanonicalName());
}
SensorsReporter.getSensorsDataApiInstance().track(ITrackClickEvent.APP_CLICK, jsonObject);
} catch (Exception e) {
Log.getStackTraceString(e);
}
}
那么我们是如何获取ExpandableListView
的呢?这里主要体现在 $element_position
这个参数上,我们需要根据是group
或者child
做不同的逻辑处理
/**
* ExpandableListView代理事件
*
* @param view ExpandableListView代理view
*/
public void expandableItemClick(View view) {
if (view instanceof ExpandableListView){
try {
final Class viewClazz = Class.forName("android.widget.ExpandableListView");
// ---------------------------------------Child---------------------------------------
Field mOnChildClickListenerField = viewClazz.getDeclaredField("mOnChildClickListener");
if (!mOnChildClickListenerField.isAccessible()) {
mOnChildClickListenerField.setAccessible(true);
}
ExpandableListView.OnChildClickListener onChildClickListener =
(ExpandableListView.OnChildClickListener) mOnChildClickListenerField.get(view);
if (onChildClickListener != null && !(onChildClickListener instanceof MkOnChildClickListener)) {
((ExpandableListView) view).setOnChildClickListener(new MkOnChildClickListener(onChildClickListener));
}
// ---------------------------------------Group---------------------------------------
Field mOnGroupClickListenerField = viewClazz.getDeclaredField("mOnGroupClickListener");
if (!mOnGroupClickListenerField.isAccessible()) {
mOnGroupClickListenerField.setAccessible(true);
}
ExpandableListView.OnGroupClickListener onGroupClickListener =
(ExpandableListView.OnGroupClickListener) mOnGroupClickListenerField.get(view);
if (onGroupClickListener != null && !(onGroupClickListener instanceof MkOnGroupClickListener)) {
((ExpandableListView) view).setOnGroupClickListener(new MkOnGroupClickListener(onGroupClickListener));
}
} catch (Exception e) {
Log.getStackTraceString(e);
}
}
}
先通过反射获取mOnGroupClickListener
对象和mOnChildClickListener
,如果listener
不为空,并且不是我们需要的Listener
类型,那么分别通过自定义的的MkOnGroupClickListener
和MkOnChildClickListener
区代理完成。
/**
* @author 杨正友(小木箱)于 2020/10/4 15 22 创建
* @Email: [email protected]
* @Tel: 18390833563
* @function description:
*/
public class MkOnGroupClickListener implements ExpandableListView.OnGroupClickListener {
private ExpandableListView.OnGroupClickListener source;
public MkOnGroupClickListener(ExpandableListView.OnGroupClickListener source) {
this.source = source;
}
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long id) {
SensorsDataManager.trackAdapterView(expandableListView, view, groupPosition, -1);
if (source != null) {
source.onGroupClick(expandableListView, view, groupPosition, id);
}
return false;
}
}
在其onGroupClick
方法内部,我们首先调用埋点代码,然后调用原有的listener
的OnGroupClickListener
方法,这样即可实现插入
埋点代码的效果
/**
* @author 杨正友(小木箱)于 2020/10/4 15 21 创建
* @Email: [email protected]
* @Tel: 18390833563
* @function description:
*/
public class MkOnChildClickListener implements ExpandableListView.OnChildClickListener {
private ExpandableListView.OnChildClickListener source;
public MkOnChildClickListener(ExpandableListView.OnChildClickListener source) {
this.source = source;
}
@Override
public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long id) {
SensorsDataManager.trackAdapterView(expandableListView, view, groupPosition, childPosition);
if (source != null) {
return source.onChildClick(expandableListView, view, groupPosition, childPosition, id);
}
return false;
}
}
目前这种全埋点的方案是无法采集activity
上游离的view
,如: Dialog
,因为无法遍历到被点击的view
。对于这样的dialog
,我们可以通过如下方式解决
/**
* Track Dialog 的点击
* @param activity Activity
* @param dialog Dialog
*/
public void trackDialog(@NonNull final Activity activity, @NonNull final Dialog dialog) {
if (dialog.getWindow() != null) {
dialog.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
SensorsDataManager.delegateViewsOnClickListener(activity, dialog.getWindow().getDecorView());
}
});
}
}
然后在Dialog
show之前调用即可
AlertDialog alertDialog = new AlertDialog.Builder(this)
.setNegativeButton("取消", (dialog, which) -> {})
.setPositiveButton("确定", (dialog, which) -> {})
.setTitle("小木箱").setMessage("一定要加油努力哦~").create();
SensorsReporter.getSensorsDataApiInstance().trackDialog(this,alertDialog);
你的 点赞、评论、收藏、转发,是对我的巨大鼓励!