全局监控click事件的四种⽅式
本⽂主要给⼤家分享如何在全局上去监听click点击事件,并做些通⽤处理或是拦
截。使⽤场景可能就是具体的全局防快速重复点击,或是通⽤打点分析上报,⽤户⾏
为监控等。以下将以四种不同的思路和实现⽅式去监控全局的点击操作,由简单到复
杂逐⼀讲解。
⽅式⼀,适配监听接⼝,预留全局处理接⼝并作为所有监听器的基类使⽤
抽象出公共基类监听对象,可预留拦截机制和通⽤点击处理,简要代码如下:
kListener{
@Override
publicvoidonClick(Viewview){
if(!interceptViewClick(view)){
onViewClick(view);
}
}
protectedbooleaninterceptViewClick(Viewview){
//TODO:这⾥可做⼀此通⽤的处理如打点,或拦截等。
returnfal;
}
protectedabstractvoidonViewClick(Viewview);
}
使⽤⽅式之⼀匿名对象作为公共监听器
CustClickListenermClickListener=newCustClickListener(){
@Override
protectedvoidonViewClick(Viewview){
xt(,ng(),_SHORT).show();
}
};
@Override
protectedvoidonCreate(@NullableBundlesavedInstanceState){
te(savedInstanceState);
tContentView(ty_login);
findViewById().tOnClickListener(mClickListener);
}
这种⽅式⽐较简单,⽆兼容问题,但是需要⾃始⾄终都要使⽤基于基类的监听器对象,对开
发者约束⽐较⼤。适⽤于新项⽬之初就有此使⽤约定。对于⽼代码重构⼯作量⽐较⼤,⽽且
如果接⼊第三⽅墨盒模块就⽆能为⼒了。
⽅式⼆,反射代理,适时偷梁换柱开发者⽆感知,在适配包装器⾥做通⽤处
理。
以下是代理接⼝和内置监听适配器,全局的监听接⼝需要实现
IProxyClickListener
并设置到内置适配器
WrapClickListener
⾥
publicinterfaceIProxyClickListener{
booleanonProxyClick(WrapClickListenerwrap,Viewv);
kListener{
IProxyClickListenermProxyListener;
kListenermBaListener;
publicWrapClickListener(kListenerl,IProxyClickListenerproxyListener){
mBaListener=l;
mProxyListener=proxyListener;
}
@Override
publicvoidonClick(Viewv){
booleanhandled=mProxyListener==null?fal:yClick(,v);
if(!handled&&mBaListener!=null){
k(v);
}
}
}
}
我们需要选择⼀个时机对所有设置有监听器的
View
做监听代理的hook.这个时机可以对Activity的根View添加⼀个视图变化监听(当然也可
选择在Activity的DOWN事件的分发时机):
wTreeObrver().addOnGlobalLayoutListener(alLayoutListener(){
@Override
publicvoidonGlobalLayout(){
hookViews(rootView,0)
}
});
注:以上为了⽅便匿名注册了监听,实际使⽤在Activity退出时要反注册掉。
在进⾏代理前先要反射获取View监听器相关的Method和Field对象如下:
publicvoidinit(){
if(sHookMethod==null){
try{
ClassviewClass=e("");
if(viewClass!=null){
sHookMethod=laredMethod("getListenerInfo");
if(sHookMethod!=null){
essible(true);
}
}
}catch(Exceptione){
reportError(e,"init");
}
}
if(sHookField==null){
try{
ClasslistenerInfoClass=e("$ListenerInfo");
if(listenerInfoClass!=null){
sHookField=laredField("mOnClickListener");
if(sHookField!=null){
essible(true);
}
}
}catch(Exceptione){
reportError(e,"init");
}
}
}
只有保证了
sHookMethod
和
sHookField
成功获取才能进⼊下⼀步递归去设置监听代理偷梁换柱。以下为具体实现递归设置代理监听的过程。其
中
mInnerClickProxy
为外部传⼊的的全局处理点击事件的代理接⼝。
privatevoidhookViews(Viewview,intrecycledContainerDeep){
if(ibility()==E){
booleanforceHook=recycledContainerDeep==1;
if(viewinstanceofViewGroup){
booleanexistAncestorRecycle=recycledContainerDeep>0;
ViewGroupp=(ViewGroup)view;
if(!(pinstanceofAbsListView||pinstanceofRecyclerView)||existAncestorRecycle){
hookClickListener(view,recycledContainerDeep,forceHook);
if(existAncestorRecycle){
recycledContainerDeep++;
}
}el{
recycledContainerDeep=1;
}
intchildCount=ldCount();
for(inti=0;i
Viewchild=ldAt(i);
hookViews(child,recycledContainerDeep);
}
}el{
hookClickListener(view,recycledContainerDeep,forceHook);
}
}
}
privatevoidhookClickListener(Viewview,intrecycledContainerDeep,booleanforceHook){
booleanneedHook=forceHook;
if(!needHook){
needHook=kable();
if(needHook&&recycledContainerDeep==0){
needHook=(mPrivateTagKey)==null;
}
}
if(needHook){
try{
ObjectgetListenerInfo=(view);
kListenerbaClickListener=getListenerInfo==null?null:(kListener)(getListenerInfo);//获取已设置过的监听器
if((baClickListener!=null&&!(ickListener))){
(getListenerInfo,ickListener(baClickListener,mInnerClickProxy));
(mPrivateTagKey,recycledContainerDeep);
}
}catch(Exceptione){
reportError(e,"hook");
}
}
}
以上深度优先从Activity的根View进⾏递归设置监听。只会对原来的View本⾝有点击的事件监听器的进⾏设置,成功设置后还会对操作的
View设置⼀个tag标志表明已经设置了代理,避免每次变化重复设置。这个tag具有⼀定的含意,记录该View相对可能存在的可回收容器的
层级数。因为对于像
AbsListView
或
RecyclerView
的直接⼦View是需要强制重新绑定代理的,因为它们的复⽤机制可能被重新设置了监听。
此⽅式实现实现稍微复杂,但是实现效果⽐较好,对开发者⽆感知进⾏监听器的hook代理。
反射效率上也可以接受速度⽐较快⽆影响。对任何设置了监听器的View都有效。然
⽽AbsListView的Item点击⽆效,因为它的点击事件不是通过onClick实现的,除⾮不是⽤
tItemOnClick⽽是⾃⼰绑定click事件。
⽅式三,通过AccessibilityDelegate捕获点击事件。
分析View的源码在处理点击事件的回调时调⽤了mClick⽅法,内部调⽤了
ndAccessibilityEvent
⽽此⽅法有个托管接
⼝
mAccessibilityDelegate
可以由外部处理所有的AccessibilityEvent.正好此托管接⼝的设置也是开放的
tAccessibilityDelegate
,如以下View源
码关键⽚段。
publicbooleanperformClick(){
finalbooleanresult;
finalListenerInfoli=mListenerInfo;
if(li!=null&&ckListener!=null){
playSoundEffect();
k(this);
result=true;
}el{
result=fal;
}
ndAccessibilityEvent(_VIEW_CLICKED);
returnresult;
}
publicvoidndAccessibilityEvent(inteventType){
if(mAccessibilityDelegate!=null){
cessibilityEvent(this,eventType);
}el{
ndAccessibilityEventInternal(eventType);
}
}
publicvoidtAccessibilityDelegate(@NullableAccessibilityDelegatedelegate){
mAccessibilityDelegate=delegate;
}
基于此原理我们可在某个时机给所有的View注册我们⾃⼰的AccessibilityDelegate去监听系统⾏为事件,简要实现代码如下。
ibilityDelegate{
booleanmInstalled=fal;
WeakReference
alLayoutListenermOnGlobalLayoutListener=null;
publicViewClickTracker(ViewrootView){
if(rootView!=null&&wTreeObrver()!=null){
mRootView=newWeakReference(rootView);
mOnGlobalLayoutListener=alLayoutListener(){
@Override
publicvoidonGlobalLayout(){
Viewroot=mRootView==null?null:();
booleaninstall=;
if(root!=null&&wTreeObrver()!=null&&wTreeObrver().isAlive()){
try{
installAccessibilityDelegate(root);
if(!mInstalled){
mInstalled=true;
}
}catch(Exceptione){
tackTrace();
}
}el{
destroyInner(fal);
}
}
};
wTreeObrver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
}
privatevoidinstallAccessibilityDelegate(Viewview){
if(view!=null){
essibilityDelegate();
if(viewinstanceofViewGroup){
ViewGroupparent=(ViewGroup)view;
intcount=ldCount();
for(inti=0;i
Viewchild=ldAt(i);
if(ibility()!=){
installAccessibilityDelegate(child);
}
}
}
}
}
@Override
publicvoidndAccessibilityEvent(Viewhost,inteventType){
cessibilityEvent(host,eventType);
if(_VIEW_CLICKED==eventType&&host!=null){
//TODO这⾥处理通⽤的点击事件,host即为相应被点击的View.
}
}
}
以上实现⽐较巧妙,在监测到window上全局视图树发⽣变化后递归的给所有的View安
装AccessibilityDelegate。经测试⼤多数⼚商的机型和版本都是可以的,然⽽部分机型⽆法成功捕获
监控到点击事件,所以不推荐使⽤。
⽅式四,通过分析Activity的dispatchTouchEvent事件并查找事件接受
的⽬标View。
这个⽅式初看有点匪夷所思,但是⼀系列触屏事件发⽣后总归要有⼀个组件消耗了它,查看
ViewGroup
关键源码如下:
//Firsttouchtargetinthelinkedlistoftouchtargets.
privateTouchTargetmFirstTouchTarget;
publicbooleandispatchTouchEvent(MotionEventev){
......
if(newTouchTarget==null&&childrenCount!=0){
for(inti=childrenCount-1;i>=0;i--){
if(dispatchTransformedTouchEvent(ev,fal,child,idBitsToAssign)){
newTouchTarget=addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget=true;
break;
}
}
}
......
//Dispatchtotouchtargets.
if(mFirstTouchTarget==null){
//Notouchtargetssotreatthisasanordinaryview.
handled=dispatchTransformedTouchEvent(ev,canceled,null,_POINTER_IDS);
}el{
//Dispatchtotouchtargets,excludingthenewtouchtargetifwealready
//touchtargetsifnecessary.
TouchTargetpredecessor=null;
TouchTargettarget=mFirstTouchTarget;
while(target!=null){
finalTouchTargetnext=;
if(alreadyDispatchedToNewTouchTarget&&target==newTouchTarget){
handled=true;
}el{
finalbooleancancelChild=retCancelNextUpFlag()||intercepted;
......
if(cancelChild){
if(predecessor==null){
mFirstTouchTarget=next;
}el{
=next;
}
e();
target=next;
continue;
}
}
predecessor=target;
target=next;
}
}
}
这⾥发现意愿接受touch事件的直接⼦View都会被添加到
mFirstTouchTarget
这个链式对象⾥,且链经过调整后next⼏乎总是null.这就给我
们⼀个突破⼝。可以从得到当前接受事件的直接⼦View,然后按此⽅法递归去查找直⾄
为null。我们就算是找到了最终touch事件的接受者。这个查找最好的时机应该是在
ACTION_UP或ACTION_CANCEL
。
通过以上原理我们可以有法获取⼀系列Touch事件最终接受处理的⽬标View,再根据我们记录的按下位置和松开位置及偏移偏量可判断是否为
可能的点击动作。为了加强判断是否为真正的click事件,可进⼀步分析⽬标View是否安装了点击监听器(原理可参考上⾯讲的⽅式⼆。以下获
取和分析事件时机都是在Activity的dispatchTouchEvent⽅法中进⾏的。
记录down和up事件后,以下为实现判断是否为可能的点击判断
//whetheritcouldbeaclickaction
publicbooleanisClickPossible(floatslop){
if(mCancel||mDownId==-1||mUpId==-1||mDownTime==0||mUpTime==0){
returnfal;
}el{
(mDownX-mUpX)
}
}
在up事件发⽣后⽴即查找⽬标View.⾸先要保证反射mFirstTouchTarge相关的准备⼯作。
privatebooleanensureTargetField(){
if(sTouchTargetField==null){
try{
ClassviewClass=e("oup");
if(viewClass!=null){
sTouchTargetField=laredField("mFirstTouchTarget");
essible(true);
}
}catch(Exceptione){
tackTrace();
}
try{
if(sTouchTargetField!=null){
sTouchTargetChildField=e().getDeclaredField("child");
essible(true);
}
}catch(Exceptione){
tackTrace();
}
}
returnsTouchTargetField!=null&&sTouchTargetChildField!=null;
}
然后从Activity的DecorView去递归查找⽬标View.
//notfind
privateViewfindTargetView(){
ViewnextTarget,target=null;
if(ensureTargetField()&&mRootView!=null){
nextTarget=findTargetView(mRootView);
do{
target=nextTarget;
nextTarget=null;
if(targetinstanceofViewGroup){
nextTarget=findTargetView((ViewGroup)target);
}
}while(nextTarget!=null);
}
returntarget;
}
//reflecttofindtheTouchTargetchildview,nullifnotfound.
privateViewfindTargetView(ViewGroupparent){
try{
Objecttarget=(parent);
if(target!=null){
Objectview=(target);
if(viewinstanceofView){
return(View)view;
}
}
}catch(Exceptione){
tackTrace();
}
returnnull;
}
通过以上⽅式所有具有点击功能的View都能正确监听,然⽽可能存在并没有监听点击事件
的View也被认为是⼀次点击事件。要过滤掉这部分可通过分析⽬标View是否安装了点击
监听器,这⾥就不多贴代码了,原理和代码在⽅式⼆中有讲过。
以上四种⽅式各有优劣,效率上都⽐较快,综合对⽐以⽅式⼆⽐较精准。像⽅式三和
试四只作为参考,具有学习意义,特别是⽅式四可应⽤前景⽐较⼴泛,所有的⼿势的
⽬标View都可查找得到
本⽂讲述的是我最近研究的⽤户⾏为监控的⼀个监控点。具体更多的⾏为
监控请参考项⽬InteractionHook⽬前还在持续开发中。
本文发布于:2023-01-04 16:07:40,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/91571.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |