android中如何全局监控click事件
android中如何全局监控click事件
今天小编给大家分享一下android中如何全局监控click事件的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
方式一,适配监听接口,预留全局处理接口并作为所有监听器的基类使用
抽象出公共基类监听对象,可预留拦截机制和通用点击处理,简要代码如下:
publicabstractclassCustClickListenerimplementsView.OnClickListener{@OverridepublicvoidonClick(Viewview){if(!interceptViewClick(view)){onViewClick(view);}}protectedbooleaninterceptViewClick(Viewview){//TODO:这里可做一此通用的处理如打点,或拦截等。returnfalse;}protectedabstractvoidonViewClick(Viewview);}
使用方式之一匿名对象作为公共监听器
CustClickListenermClickListener=newCustClickListener(){@OverrideprotectedvoidonViewClick(Viewview){Toast.makeText(CustActvity.this,view.toString(),Toast.LENGTH_SHORT).show();}};@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);findViewById(R.id.button).setOnClickListener(mClickListener);}
这种方式比较简单,无兼容问题,但是需要自始至终都要使用基于基类的监听器对象,对开发者约束比较大。适用于新项目之初就有此使用约定。对于老代码重构工作量比较大,而且如果接入第三方墨盒模块就无能为力了。
方式二,反射代理,适时偷梁换柱开发者无感知,在适配包装器里做通用处理。
以下是代理接口和内置监听适配器,全局的监听接口需要实现IProxyClickListener并设置到内置适配器WrapClickListener里
publicinterfaceIProxyClickListener{booleanonProxyClick(WrapClickListenerwrap,Viewv);classWrapClickListenerimplementsView.OnClickListener{IProxyClickListenermProxyListener;View.OnClickListenermBaseListener;publicWrapClickListener(View.OnClickListenerl,IProxyClickListenerproxyListener){mBaseListener=l;mProxyListener=proxyListener;}@OverridepublicvoidonClick(Viewv){booleanhandled=mProxyListener==null?false:mProxyListener.onProxyClick(WrapClickListener.this,v);if(!handled&&mBaseListener!=null){mBaseListener.onClick(v);}}}}
我们需要选择一个时机对所有设置有监听器的 View做监听代理的 hook .这个时机可以对 Activity 的根View添加一个视图变化监听(当然也可选择在 Activity 的 DOWN 事件的分发时机):
rootView.getViewTreeObserver().addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener(){@OverridepublicvoidonGlobalLayout(){hookViews(rootView,0)}});
注:以上为了方便匿名注册了监听,实际使用在 Activity 退出时要反注册掉。
在进行代理前先要反射获取View监听器相关的 Method 和 Field 对象如下:
publicvoidinit(){if(sHookMethod==null){try{ClassviewClass=Class.forName("android.view.View");if(viewClass!=null){sHookMethod=viewClass.getDeclaredMethod("getListenerInfo");if(sHookMethod!=null){sHookMethod.setAccessible(true);}}}catch(Exceptione){reportError(e,"init");}}if(sHookField==null){try{ClasslistenerInfoClass=Class.forName("android.view.View$ListenerInfo");if(listenerInfoClass!=null){sHookField=listenerInfoClass.getDeclaredField("mOnClickListener");if(sHookField!=null){sHookField.setAccessible(true);}}}catch(Exceptione){reportError(e,"init");}}}
只有保证了sHookMethod和sHookField成功获取才能进入下一步递归去设置监听代理偷梁换柱。以下为具体实现递归设置代理监听的过程。其中mInnerClickProxy为外部传入的的全局处理点击事件的代理接口。
privatevoidhookViews(Viewview,intrecycledContainerDeep){if(view.getVisibility()==View.VISIBLE){booleanforceHook=recycledContainerDeep==1;if(viewinstanceofViewGroup){booleanexistAncestorRecycle=recycledContainerDeep>0;ViewGroupp=(ViewGroup)view;if(!(pinstanceofAbsListView||pinstanceofRecyclerView)||existAncestorRecycle){hookClickListener(view,recycledContainerDeep,forceHook);if(existAncestorRecycle){recycledContainerDeep++;}}else{recycledContainerDeep=1;}intchildCount=p.getChildCount();for(inti=0;i<childCount;i++){Viewchild=p.getChildAt(i);hookViews(child,recycledContainerDeep);}}else{hookClickListener(view,recycledContainerDeep,forceHook);}}}privatevoidhookClickListener(Viewview,intrecycledContainerDeep,booleanforceHook){booleanneedHook=forceHook;if(!needHook){needHook=view.isClickable();if(needHook&&recycledContainerDeep==0){needHook=view.getTag(mPrivateTagKey)==null;}}if(needHook){try{ObjectgetListenerInfo=sHookMethod.invoke(view);View.OnClickListenerbaseClickListener=getListenerInfo==null?null:(View.OnClickListener)sHookField.get(getListenerInfo);//获取已设置过的监听器if((baseClickListener!=null&&!(baseClickListenerinstanceofIProxyClickListener.WrapClickListener))){sHookField.set(getListenerInfo,newIProxyClickListener.WrapClickListener(baseClickListener,mInnerClickProxy));view.setTag(mPrivateTagKey,recycledContainerDeep);}}catch(Exceptione){reportError(e,"hook");}}}
以上深度优先从 Activity 的根 View 进行递归设置监听。只会对原来的 View 本身有点击的事件监听器的进行设置,成功设置后还会对操作的 View 设置一个 tag 标志表明已经设置了代理,避免每次变化重复设置。这个 tag 具有一定的含意,记录该 View 相对可能存在的可回收容器的层级数。因为对于像AbsListView或RecyclerView的直接子 View 是需要强制重新绑定代理的,因为它们的复用机制可能被重新设置了监听。
此方式实现实现稍微复杂,但是实现效果比较好,对开发者无感知进行监听器的hook代理。反射效率上也可以接受速度比较快无影响。对任何设置了监听器的 View都有效。 然而AbsListView的Item点击无效,因为它的点击事件不是通过 onClick 实现的,除非不是用 setItemOnClick 而是自己绑定 click 事件。
方式三,通过AccessibilityDelegate捕获点击事件。
分析View的源码在处理点击事件的回调时调用了 View.performClick 方法,内部调用了sendAccessibilityEvent而此方法有个托管接口mAccessibilityDelegate可以由外部处理所有的 AccessibilityEvent. 正好此托管接口的设置也是开放的setAccessibilityDelegate,如以下 View 源码关键片段。
publicbooleanperformClick(){finalbooleanresult;finalListenerInfoli=mListenerInfo;if(li!=null&&li.mOnClickListener!=null){playSoundEffect(SoundEffectConstants.CLICK);li.mOnClickListener.onClick(this);result=true;}else{result=false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);returnresult;}publicvoidsendAccessibilityEvent(inteventType){if(mAccessibilityDelegate!=null){mAccessibilityDelegate.sendAccessibilityEvent(this,eventType);}else{sendAccessibilityEventInternal(eventType);}}publicvoidsetAccessibilityDelegate(@NullableAccessibilityDelegatedelegate){mAccessibilityDelegate=delegate;}
基于此原理我们可在某个时机给所有的 View 注册我们自己的AccessibilityDelegate去监听系统行为事件,简要实现代码如下。
publicclassViewClickTrackerextendsView.AccessibilityDelegate{booleanmInstalled=false;WeakReference<View>mRootView=null;ViewTreeObserver.OnGlobalLayoutListenermOnGlobalLayoutListener=null;publicViewClickTracker(ViewrootView){if(rootView!=null&&rootView.getViewTreeObserver()!=null){mRootView=newWeakReference(rootView);mOnGlobalLayoutListener=newViewTreeObserver.OnGlobalLayoutListener(){@OverridepublicvoidonGlobalLayout(){Viewroot=mRootView==null?null:mRootView.get();booleaninstall=;if(root!=null&&root.getViewTreeObserver()!=null&&root.getViewTreeObserver().isAlive()){try{installAccessibilityDelegate(root);if(!mInstalled){mInstalled=true;}}catch(Exceptione){e.printStackTrace();}}else{destroyInner(false);}}};rootView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);}}privatevoidinstallAccessibilityDelegate(Viewview){if(view!=null){view.setAccessibilityDelegate(ViewClickTracker.this);if(viewinstanceofViewGroup){ViewGroupparent=(ViewGroup)view;intcount=parent.getChildCount();for(inti=0;i<count;i++){Viewchild=parent.getChildAt(i);if(child.getVisibility()!=View.GONE){installAccessibilityDelegate(child);}}}}}@OverridepublicvoidsendAccessibilityEvent(Viewhost,inteventType){super.sendAccessibilityEvent(host,eventType);if(AccessibilityEvent.TYPE_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,false,child,idBitsToAssign)){newTouchTarget=addTouchTarget(child,idBitsToAssign);alreadyDispatchedToNewTouchTarget=true;break;}}}......//Dispatchtotouchtargets.if(mFirstTouchTarget==null){//Notouchtargetssotreatthisasanordinaryview.handled=dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);}else{//Dispatchtotouchtargets,excludingthenewtouchtargetifwealready//dispatchedtoit.Canceltouchtargetsifnecessary.TouchTargetpredecessor=null;TouchTargettarget=mFirstTouchTarget;while(target!=null){finalTouchTargetnext=target.next;if(alreadyDispatchedToNewTouchTarget&&target==newTouchTarget){handled=true;}else{finalbooleancancelChild=resetCancelNextUpFlag(target.child)||intercepted;......if(cancelChild){if(predecessor==null){mFirstTouchTarget=next;}else{predecessor.next=next;}target.recycle();target=next;continue;}}predecessor=target;target=next;}}}
这里发现意愿接受 touch 事件的 直接子View 都会被添加到mFirstTouchTarget这个链式对象里,且链经过调整后 next 几乎总是 null. 这就给我们一个突破口。可以从mFirstTouchTarget.child 得到当前接受事件的直接子 View , 然后按此方法递归去查找直至mFirstTouchTarget.child 为 null。我们就算是找到了最终 touch 事件的接受者。这个查找最好的时机应该是在ACTION_UP 或 ACTION_CANCEL 。
通过以上原理我们可以有法获取一系列 Touch 事件最终接受处理的目标 View,再根据我们记录的按下位置和松开位置及偏移偏量可判断是否为可能的点击动作。为了加强判断是否为真正的 click 事件,可进一步分析目标 View 是否安装了点击监听器(原理可参考上面讲的方式二。以下获取和分析事件时机都是在 Activity 的 dispatchTouchEvent 方法中进行的。
记录 down 和 up 事件后,以下为实现判断是否为可能的点击判断
//whetheritcouldbeaclickactionpublicbooleanisClickPossible(floatslop){if(mCancel||mDownId==-1||mUpId==-1||mDownTime==0||mUpTime==0){returnfalse;}else{returnMath.abs(mDownX-mUpX)<slop&&Math.abs(mDownY-mUpY)<slop;}}
在 up 事件发生后立即查找目标 View.首先要保证反射 mFirstTouchTarge 相关的准备工作。
privatebooleanensureTargetField(){if(sTouchTargetField==null){try{ClassviewClass=Class.forName("android.view.ViewGroup");if(viewClass!=null){sTouchTargetField=viewClass.getDeclaredField("mFirstTouchTarget");sTouchTargetField.setAccessible(true);}}catch(Exceptione){e.printStackTrace();}try{if(sTouchTargetField!=null){sTouchTargetChildField=sTouchTargetField.getType().getDeclaredField("child");sTouchTargetChildField.setAccessible(true);}}catch(Exceptione){e.printStackTrace();}}returnsTouchTargetField!=null&&sTouchTargetChildField!=null;}
然后从 Activity 的 DecorView 去递归查找目标 View .
//findthetargetviewwhoisinterestinthetouchevent.nullifnotfindprivateViewfindTargetView(){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=sTouchTargetField.get(parent);if(target!=null){Objectview=sTouchTargetChildField.get(target);if(viewinstanceofView){return(View)view;}}}catch(Exceptione){e.printStackTrace();}returnnull;}
以上就是“android中如何全局监控click事件”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注恰卡编程网行业资讯频道。
推荐阅读
-
android(如何快速开发框架 小米note开发版MIUI,安卓6.0,怎么安装Xposed框架)
稳定版,你必须先根除。你上网搜索安卓可以叫别人s框架,对方可以把框架做成jar包,把这个jar包加载到项目目录的libs文件中使...
-
android(studio 虚拟机启动不了 android studio可以当模拟器用吗)
androidstudio可以当模拟器用吗?AmdCUP引导模拟器有点复杂。雷电模拟器上的抖音怎么登录不上?不是,闪电模拟调用...
-
从实践中学习手机抓包与数据分析(android 手机抓包app)
android手机抓包app?netcapture抓包精灵app(手机抓包工具)又名sslcapture,是什么专业的安卓手机抓...
-
android(studio全局搜索 android studio怎么看app界面)
androidstudio怎么看app界面?在设备桌面点击运用直接进入到App界面,就也可以参与其他你的操作了。android-...
-
怎么把android框架源代码拉到本地(android studio如何运行别人的源代码)
androidstudio如何运行别人的源代码?androidstudio点击刚建在列表中你选择导入module,导入即可在用...
-
android(studio2022年使用教程 怎么安装Android studio详细教程)
怎么安装Androidstudio详细教程?androidstudio中haxm直接安装的方法追加:1、简单的方法打开Andr...
-
怎么使用Android基准配置文件Baseline Profile方案提升启动速度
-
HTML5如何实现禁止android视频另存为
-
学java好还是学php好?
-
Android如何实现多点触控功能