Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to capture all the clicks on an Activity page

2025-01-19 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article focuses on "how to capture all the clicks on an Activity page". Interested friends may wish to take a look. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn how to capture all the clicks on an Activity page.

Preparatory work

First, Delo lists some of the click behaviors on the page, which are commonly used:

Ordinary View click dynamic add View click button on Dialog click

So you have the following code:

Class MainActivity: AppCompatActivity () {

Override fun onCreate (savedInstanceState: Bundle?) {

Super.onCreate (savedInstanceState)

SetContentView (R.layout.activity_main)

Btn1.setOnClickListener {

ShowToast ("Button 1 clicked")

}

Btn2.setOnClickListener {

Val builder =

AlertDialog.Builder (this)

.setTitle ("I am a dialog")

Val view: View = layoutInflater.inflate (R.layout.dialog_btn, null)

Val btn4 =

View.findViewById (R.id.btn4)

Btn4.setOnClickListener {

ShowToast ("clicked the Dialog button")

}

Builder.setView (view)

Builder.create () .show ()

}

Btn3.setOnClickListener {

Var button = Button (this)

Button.text = "I am the new button"

Var param = LinearLayout.LayoutParams (

ViewGroup.LayoutParams.WRAP_CONTENT

ViewGroup.LayoutParams.WRAP_CONTENT

)

Mainlayout.addView (button, param)

Button.setOnClickListener {

ShowToast ("clicked the newly added button")

}

}

}

}

Since I want to capture click events, the first thing I think of is through the event distribution mechanism, that is, to get all the touch events at the source, and then count the click events, let's do it.

Event distribution

Override the dispatchTouchEvent method of Activity, because there are only click events, so you only need to count ACTION_UP events, and if there are long press events, you need to determine the time to press.

Override fun dispatchTouchEvent (ev: MotionEvent?): Boolean {

Ev?.let {

When (ev.action) {

MotionEvent.ACTION_UP-> {

Log.e (Companion.TAG, "ACTION_UP--CLICK")

}

Else-> {}

}

}

Return super.dispatchTouchEvent (ev)

}

Ok, run it.

Click button 1, log print normally Click the dialog button in button 2, log. Did not click the button in button 3, the log prints normally

As a result, as you can see, click events in Dialog cannot be responded to. Why?

This starts with the event distribution mechanism. Clicking on the screen first responds to the top-level View of the current screen, that is, DecorView, which in Activity is the root layout of Window. DecorView then calls the dispatchTouchEvent method of Activity, acts as a control intercept for developer event distribution, and finally returns to the super.dispatchTouchEvent (event) method of DecorView to start ViewGroup event delivery. Take a look at the relevant source code:

/ / DecorView.java

@ Override

Public boolean dispatchTouchEvent (MotionEvent ev) {

/ / cb is actually the corresponding Activity

Final Window.Callback cb = mWindow.getCallback ()

Return cb! = null & &! mWindow.isDestroyed () & mFeatureId

< 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } //Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } //PhoneWindow.java @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } //DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } 可以看到事件的开始经历了DecorView-->

Activity-- > PhoneWindow-- > DecorView-- > ViewGroup.

And we can't get the click event of Dialog in the second step Acitivity. It's obvious that DecorView didn't send the event. Isn't Dialog's DecorView and Activity's DecorView the same?

Let's continue to study the species Dialog, which has an unclear relationship with Activity.

Dialog,Activity has a constant relationship.

Here we only look at two methods, one is the constructor of Dialog, the other is the show method, to see how this love triangle is formed:

/ / Constructor

Dialog (Context context, int theme, boolean createContextThemeWrapper) {

/ /.

/ / the WindowManager object is obtained. MContext is usually an Activity, and system services are usually obtained through Binder.

MWindowManager = (WindowManager) context.getSystemService (Context.WINDOW_SERVICE)

/ / create a new Window

Window w = PolicyManager.makeNewWindow (mContext)

MWindow = w

/ / this is also the reason why mWindow.getCallback () above is Activity. When you create a new Window, you will set callback to yourself.

W.setCallback (this)

W.setOnWindowDismissedCallback (this)

/ / the associated WindowManager and the new Window,token are null

W.setWindowManager (mWindowManager, null, null)

W.setGravity (Gravity.CENTER)

MListenersHandler = new ListenersHandler (this)

}

/ / show method

Public void show () {

/ /.

If (! mCreated) {

/ / call back the onCreate method of Dialog

DispatchOnCreate (null)

}

/ / call back the onStart method of Dialog

OnStart ()

/ / get the DecorView object of the current new Window

MDecor = mWindow.getDecorView ()

WindowManager.LayoutParams l = mWindow.getAttributes ()

Try {

/ / add a View to the windowManager shared by Activity

MWindowManager.addView (mDecor, l)

/ /.

} finally {

}

}

You can see that a Dialog has gone through the following steps from scratch:

First, you create a new Window of type PhoneWindow, similar to the Activity process for creating Window, and set the setCallback callback. Associate this new Window with the WindowManager object you got from Activity, that is, dialog and Activity share the same WindowManager object. The show method shows Dialog, and the onCreate,onStart method of Dialog is called back first. Then take Dialog's own DecorView object, add it to the WindowManager object through the addView method, and the Dialog appears on the screen.

By analyzing this process, we can also learn about some minor problems we usually encounter, such as why Dialog must be attached to Activity display. Because the Dialog creation process requires the use of Activity's Context, that is, you need to use Activity's token to create the window. So the Content passed into Application will get an error-- "Unable to add window-- token null is not for an application".

Back to the point, the process is summed up in one sentence: Dialog uses Activity's WindowManager object and adds a new Window's DecorView on top of it.

Therefore, we know that Dialog and Activity are located in a different Window, that is, the parent View--DecorView is also different, so after the Dialog appears, click the button on the screen to start the response from Dialog's own DecorView, and then review the code of DecorView:

/ / DecorView.java

@ Override

Public boolean dispatchTouchEvent (MotionEvent ev) {

/ / cb becomes Dialog here

Final Window.Callback cb = mWindow.getCallback ()

Return cb! = null & &! mWindow.isDestroyed () & mFeatureId

< 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } 这时候getCallback的对象变成了Dialog,所以不会回调Activity的dispatchTouchEvent方法,而是走到Dialog的dispatchTouchEvent方法。 这个问题终于搞清楚了,但是我们自己的问题该怎么解决呢?继续探索~ 替换OnClickListener 既然点击事件都是通过setOnClickListener完成的,那么我们替换这个OnClickListener不就能获取所有的点击事件了? ok,先看看setOnClickListener方法,看看该怎么替换: //View.java ListenerInfo mListenerInfo; public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; } 代码很简单,所以我们只需要替换View的getListenerInfo()获取到的mListenerInfo对象中的mOnClickListener即可。 1)思路有了,先生成我们自己需要替换的OnClickListener: class MyOnClickListenerer(var onClickListener: View.OnClickListener?) : View.OnClickListener { override fun onClick(v: View?) { Log.e("lz", "点击了一个按钮--$v") onClickListener!!.onClick(v) } } 2)然后选择hook点,我们之前在《线程与更新UI》文章中说过,Activity的DecorView被完整绘制出来是在onResume之后,所以我们就在这里进行hook我们的MyOnClickListenerer: override fun onResume() { super.onResume() var rootView = window.decorView as ViewGroup hookAllChildView(rootView) } private fun hookAllChildView(viewGroup: ViewGroup) { val count = viewGroup.childCount for (i in 0 until count) { if (viewGroup.getChildAt(i) is ViewGroup) { hookAllChildView(viewGroup.getChildAt(i) as ViewGroup) } else { hook(viewGroup.getChildAt(i)) } } } @SuppressLint("DiscouragedPrivateApi", "PrivateApi") private fun hook(view: View) { try { val getListenerInfo: Method = View::class.java.getDeclaredMethod("getListenerInfo") getListenerInfo.isAccessible = true //获取当前View的ListenerInfo对象 val mListenerInfo: Any = getListenerInfo.invoke(view) try { val listenerInfoClazz = Class.forName("android.view.View\$ListenerInfo") try { //获取mOnClickListener参数 val mOnClickListener: Field = listenerInfoClazz.getDeclaredField("mOnClickListener") mOnClickListener.isAccessible = true var oldListener: View.OnClickListener? = mOnClickListener.get(mListenerInfo) as? View.OnClickListener if (oldListener != null && oldListener !is MyOnClickListenerer) { //替换OnClickListenerer val proxyOnClick = MyOnClickListenerer(oldListener) mOnClickListener.set(mListenerInfo, proxyOnClick) } } catch (e: NoSuchFieldException) { e.printStackTrace() } } catch (e: ClassNotFoundException) { e.printStackTrace() } } catch (e: NoSuchMethodException) { e.printStackTrace() } } 等我满意的去运行项目的时候,又被无情的现实扇了一巴掌: 点击按钮1,日志打印正常点击按钮2中的dialog按钮,日志。。。没有点击按钮3中的button,日志。。。没有 好家伙,结果只有一个按钮是正常捕获的。分析下原因吧,为啥Dialog和新加的View都无法捕获呢? 好好想想我们hook的时机,是在界面上的布局绘制出来之后,但是Dialog和新加的View都是在界面绘制之后再出现的,自然也就没有hook到。怎么解决呢? 新加的View其实还比较好解决,给rootView 添加 ViewTreeObserver.OnGlobalLayoutListener监听即可,当视图树的布局发生改变时,就可以被ViewTreeObserver监听到,然后再hook一次就行了。但是 Dialog又不好处理了,还是同样的问题,不是同一个rootView ,所以需要在Dialog的rootView也要进行一次hook。 4)再次改动 //Dialog增加hook var rootView = dialog.window?.decorView as ViewGroup hookAllChildView(rootView) //增加监听view树 rootView.viewTreeObserver.addOnGlobalLayoutListener { hookAllChildView(rootView) } 这下运行确实都能打印出日志了,但是,这也太蠢了点吧。。特别是Dialog,不可能每个Dialog都去加一遍hook代码呀。所以,还需要想想其他的方案 AspectJ 经过上述问题,我们又想到了一个办法,同样是进行代码埋点,使用AspectJ来解决我们的问题。 AspectJ是一个面向切面编程(AOP)的框架,可以在编译期将代码插入到目标切入点中,达到AOP目的。 //AspectJ的配置代码就不贴了,需要的小伙伴可以看看文末的源代码链接 @Aspect class ClickAspect { @Pointcut("execution(* android.view.View.OnClickListener.onClick(..))") fun pointcut() { } @Around("pointcut()") @Throws(Throwable::class) fun onClickMethodAround(joinPoint: ProceedingJoinPoint) { val args: Array = joinPoint.args var view: View? = null for (arg in args) { if (arg is View) { view = arg } } joinPoint.proceed() Log.d("lz", "点击了一个按钮: $view") } } 通过找到切点,也就是View中的onClick方法,*表示任意返回值,..表示任意参数,然后在这个切点中获取view信息,得到点击事件的反馈。 运行,三种情况都能正常打印日志。所以这个方法是可行的。 AccessibilityService 到这里,问题也是有解决的办法了。但是还有没有其他的方案呢?既然是关于界面反馈类的问题,这里又想到一个方案--无障碍服务AccessibilityService,来试试看。 class ClickAccessibilityService: AccessibilityService() { override fun onInterrupt() { } override fun onAccessibilityEvent(event: AccessibilityEvent?) { val eventType = event?.eventType val className = event?.className.toString() when (eventType) { AccessibilityEvent.TYPE_VIEW_CLICKED ->

Log.e (TAG, "[accessibility] clicked a button = $className")

}

}

Companion object {

Private const val TAG = "AccessibilityService"

}

}

/ / in addition, you need to configure service and the corresponding config file in AndroidManifest.xml. For more information, please see the source code at the end of the document, so we will not post it here.

That's all the key code. In the onAccessibilityEvent callback, just get the AccessibilityEvent.TYPE_VIEW_CLICKED event, run it, and turn on our accessibility service.

All three clicks can print the log normally and get it done.

Summary

We tried four ways:

Event distribution scheme. Intercept click events on the page by overriding the dispatchTouchEvent method of Activity. However, the click events in Dialog cannot be intercepted, because the event distribution is initiated by DecorView, but the DecorView where Dialog is located and the DecorView of Activity are not the same, so it is impossible to intercept click events in Dialog in the dispatchTouchEvent method of Activitiy. Hook replaces the OnClickListener scheme. This solution is mainly by replacing mOnClickListener in View for our own OnClickListener, and then intercepting click events. But this solution needs to get the replacement View, so the new View and Dialog need to be handled separately. The new View needs to listen to the View tree of the current page, and the Dialog must hook the View in the Dialog again. AspectJ section programming scheme. This scenario inserts the code into the target method at compile time, so just find the pointcut-- that is, the onClick method in View. It can solve our problems perfectly, and there is no need for users to operate separately. Barrier-free services. This solution is to intercept all click events in APP through the barrier-free service in Android, and the corresponding event is AccessibilityEvent.TYPE_VIEW_CLICKED. This solution can also solve our problem perfectly, but there is a big disadvantage, that is, users need to set up the page separately to open the auxiliary service.

Although in our actual project there is almost no need to get all the click events on the page, the analysis of this problem can let us understand the relevant knowledge, such as today's event distribution mechanism, Hook method, aspect programming, barrier-free services, with this knowledge, we will be able to have our own solutions when we really encounter some problems or requirements about page events.

At this point, I believe you have a deeper understanding of "how to capture all the click behavior on an Activity page". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report