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

Example Analysis of event Distribution Mechanism and processing in Custom view in Android

2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

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

This article will explain in detail the example analysis of event distribution mechanism and handling in custom view in Android. The editor thinks it is very practical, so I share it with you for reference. I hope you can get something after reading this article.

Title citation

There is only one event, multiple people want to deal with, and the object we want to deal with is either the object we want to give or the event conflict.

As shown in the figure above, the parent layout of RecyclerView is ViewPager, which is fine when sliding left and right, and RecyclerView cannot achieve the desired effect as if it did not receive a sliding event when sliding up and down. Our touch is encapsulated as MotionEvent event delivery. How does it pass at multiple levels? What is the basis for determining which view handles this incident? let's unveil her step by step!

Activity's process of distributing events

Trace back to the source and look for the beginning of event distribution.

When a click occurs, the event is first passed to the current Activity and distributed by the dispatchTouchEvent of Activity

Public boolean dispatchTouchEvent (MotionEvent ev) {if (ev.getAction () = = MotionEvent.ACTION_DOWN) {onUserInteraction ();} if (getWindow (). SuperDispatchTouchEvent (ev)) {return true;} return onTouchEvent (ev);}

The window class returned by getWindow here has only one implementation, PhoneWindow

Private DecorView mDecor public boolean superDispatchTouchEvent (MotionEvent event) {return mDecor.superDispatchTouchEvent (event);}

Let's move on to the superDispatchTouchEvent method implementation of DecorView

Public boolean superDispatchTouchEvent (MotionEvent event) {return super.dispatchTouchEvent (event);}

DecorView inherits from ViewGroup. It should be understood at this point that the event distribution of Activity is handed over to DecorView to handle, and what is DecorView?

DecorView is the root view of the activity window, which is divided into two parts inside a FrameLayout,DecorView, one is ActionBar, the other is ContentParent, that is, the layout of activity in setContentView. In this way, event distribution distributes events to the layout we write from the system level!

Event distribution is a recursive process, which mainly involves three functions

DispatchTouchEvent

OnInterceptTouchEvent

OnTouchEvent

Relationship among the three

Public boolean dispatchTouchEvent (MotionEvent ev) {boolean result = false; if (onInterceptTouchEvent (ev)) {/ / if intercepted, give it to your own onTouchEvent to handle the event result = onTouchEvent (ev);} else {/ / if it is not intercepted, give it to the sub-layout for distribution, which is a hierarchical recursive procedure result = chlid.dispatchTouchEvent (ev) } return result;}

Direct access to the source code is a very painful thing, a variety of possible occurrence makes the source code very poor readability. Below we will analyze it from a particular logic, which will be much clearer. Analyze only one situation at a time!

Distribution process intercepted by parent layout

The parent layout intercepts us in two steps, ACTION_DOWN and ACTION_MOVE

ACTION_DOWN event

Enter the dispatchTouchEvent method of ViewGroup

If (actionMasked = = MotionEvent.ACTION_DOWN) {/ / Throw away all previous state when starting a new touch gesture. / / The framework may have dropped the up or cancel event for the previous gesture / / due to an app switch, ANR, or some other state change. CancelAndClearTouchTargets (ev); resetTouchState ();}

Because it is an ACTION_DOWN event, clear the state first, one is the state of TouchTarget, and the other is mGroupFlags. There's no need to keep going.

/ / Check for interception.final boolean intercepted;// walks into the if if (actionMasked = = MotionEvent.ACTION_DOWN | | mFirstTouchTarget! = null) {/ / We go to the parent layout intercept event, and the child layout uses the imperial sword, disallowIntercept = false final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0 because this is the first time that mFirstTouchTarget = MotionEvent.ACTION_DOWN and it is an ACTION_DOWN event. If (! disallowIntercept) {/ / We intercept here, intercepted = true intercepted = onInterceptTouchEvent (ev); ev.setAction (action); / / restore action in case it was changed} else {intercepted = false;}} else {/ / There are no touch targets and this action is not an initial down / / so this view group continues to intercept touches. Intercepted = true;}

The code comments are relatively complete. The main purpose here is to determine whether this view is intercepted, if intercepting intercepted = true. So I can't get into the sub-view distribution after the traversal.

/ / intercepted = true, can't get into if (! canceled & &! intercepted) {/ / this is a story for (int I = childrenCount-1; I > = 0; imuri -) {} that goes through the whole view.

Go straight down.

/ / Dispatch to touch targets.if (mFirstTouchTarget = = null) {/ / No touch targets so treat this as an ordinary view. Handled = dispatchTransformedTouchEvent (ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}

If you meet this condition, go deep into the dispatchTransformedTouchEvent function, and the third argument is null

Private boolean dispatchTransformedTouchEvent (MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {final boolean handled; if (child = = null) {/ / = execution location = handled = super.dispatchTouchEvent (transformedEvent);} else {final float offsetX = mScrollX-child.mLeft; final float offsetY = mScrollY-child.mTop; transformedEvent.offsetLocation (offsetX, offsetY); if (! Child.hasIdentityMatrix () {transformedEvent.transform (child.getInverseMatrix ());} handled = child.dispatchTouchEvent (transformedEvent);}}

The third parameter is null, that is, child = null. Call the dispatchTouchEvent of super. The super of ViewGroup is View.

Handled = View.dispatchTouchEvent (event)

Go deep into the dispatchTouchEvent method of View, and the main processing logic is the following two pieces of code

ListenerInfo li = mListenerInfo; if (li! = null & & li.mOnTouchListener! = null & & (mViewFlags & ENABLED_MASK) = = ENABLED & & li.mOnTouchListener.onTouch (this, event)) {result = true;} if (! result & & onTouchEvent (event)) {result = true;}

We can conclude that onTouch has a higher priority than onTouchEvent, and if onTouch intercepts events, onTouchEvent cannot receive them. This is why the onClick event fails after the onTouch method returns true. The logic of onTouchEvent is relatively simple, so there is no analysis here.

To make it clear here, the distribution of the event distribution mechanism actually has two meanings. One is the distribution of events between different view, the distribution of parent layout to child layout; the other is the distribution of events to different listeners in view. OnTouch, onClick, and onLongClick are also distributed sequentially.

At this point, the DOWN event intercepted by the parent layout is over. Here is the MOVE event. Sliding after DOWN is a continuous process.

ACTION_MOVE event

Click on your finger and slide to continue to distribute move events

Final boolean intercepted;if (actionMasked = = MotionEvent.ACTION_DOWN | | mFirstTouchTarget! = null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; if (! disallowIntercept) {intercepted = onInterceptTouchEvent (ev); ev.setAction (action); / / restore action in case it was changed} else {intercepted = false }} else {/ / There are no touch targets and this action is not an initial down / / so this view group continues to intercept touches. Intercepted = true;}

ActionMasked = ACTION_MOVE, mFirstTouchTarget = null, go directly to else module, that is, intercepted = true.

/ / intercepted = true, cannot enter if (! canceled & &! intercepted) {for (int I = childrenCount-1; I > = 0; iMel -) {}}

We still can't get into the module of the same distributor view.

/ / Dispatch to touch targets.if (mFirstTouchTarget = = null) {/ / No touch targets so treat this as an ordinary view. Handled = dispatchTransformedTouchEvent (ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}

This is exactly the same logic as before, and the interception process of the parent layout is over.

Summary:

The implementation of calling the parent class (View) directly by the onTouchEvent method of ViewGroup

Once the parent layout intercepts Down events, subsequent move events are executed directly by the parent layout

The advantage of this analysis is that our state is determined, and the analysis code will not have too many possibilities to confuse the logic. here is the event distribution if the parent layout does not intercept.

Distribution process when the parent layout is not intercepted

The parent layout is not intercepted. Let's follow the normal process, or follow the above idea: down first and then move.

ACTION_DOWN

After entering the dispatchTouchEvent method of GroupView, the state is still cleared first, and then it is determined whether the current layout is blocked.

Final boolean intercepted;if (actionMasked = = MotionEvent.ACTION_DOWN | | mFirstTouchTarget! = null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; if (! disallowIntercept) {intercepted = onInterceptTouchEvent (ev); ev.setAction (action); / / restore action in case it was changed} else {intercepted = false }} else {/ / There are no touch targets and this action is not an initial down / / so this view group continues to intercept touches. Intercepted = true;}

Our setting is not to intercept, so intercepted = false. Here is the code for traversing the sub-view

Final View [] children = mChildren;for (int I = childrenCount-1; I > = 0; iMel -) {final int childIndex = getAndVerifyPreorderedIndex (childrenCount, I, customOrder); / / get a child in reverse order, that is, traverse final View child = getAndVerifyPreorderedView (preorderedList, children, childIndex) from the uppermost sub-view to the inner layer / / determine whether the position of the contact is within the range of view or whether view is playing animation. If both are not satisfied, directly traverse the next if (! child.canReceivePointerEvents () | |! isTransformedTouchPointInView (x, y, child, null) {continue;} newTouchTarget = getTouchTarget (child); if (newTouchTarget! = null) {/ / Child is already receiving touch within its bounds. / / Give it the new pointer in addition to the ones it is handling. NewTouchTarget.pointerIdBits | = idBitsToAssign; break;} resetCancelNextUpFlag (child) The / / dispatchTransformedTouchEvent function is a function that handles distribution, and the parent layout also uses this / / if the child view consumes the event, it assigns a value to the flag bit and break ends the loop. If there is no consumption, continue to loop to find the distribution if (dispatchTransformedTouchEvent (ev, false, child, idBitsToAssign)) {Note 1 / / Child wants to receive touch within its bounds. MLastTouchDownTime = ev.getDownTime (); if (preorderedList! = null) {/ / childIndex points into presorted list, find original index for (int j = 0; j)

< childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 如果子view消费了事件则给 alreadyDispatchedToNewTouchTarget 和 mFirstTouchTarget 赋值 // 保存 child newTouchTarget = addTouchTarget(child, idBitsToAssign); 注释2 alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false);} 分析上面干了啥 从最上层的子view开始往内层遍历 判断当前的view在位置上是否满足触点位置 调用 dispatchTransformedTouchEvent 判断是否子view消费了事件 如果消费了事件则记录 mFirstTouchTarget 和标志位,并跳出循环 如果没有没有消费事件则继续循环 注释1的逻辑 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } // ===== 执行位置 ==== handled = child.dispatchTouchEvent(transformedEvent); }} 这次过来 child != null ,调用的是 child.dispatchTouchEvent(event) 。child 可能是View,也可能是 ViewGroup。如果是 ViewGroup 又是一个递归的过程 。层层的递归返回 handled 告诉父布局是否消费了事件! 再看注释2的逻辑 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); // 此时 mFirstTouchTarget = null target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } 给 mFirstTouchTarget 赋值,下次 move 事件过来时 mFirstTouchTarget 就是有值的了!!即 target.next = null mFirstTouchTarget = newTouchTarget 保存 child 在 target 中 至此 ACTION_DOWN 事件结束 ACTION_MOVE 继上面点击后开始滑动 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } move事件不会重置,继续走 if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) 记得down事件中给mFirstTouchTarget 赋过值嘛,虽然不是down事件依旧可以进入此方法。也就是说这里依旧会判断父布局是否要拦截子view,这里也是以后咱们处理事件冲突的重点。当前的逻辑是不拦截,所以 intercepted = false if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) 只有ACTION_DOWN事件才会进行分发,所以不会进入遍历子view的逻辑代码!MOVE事件不会分发事件! // mFirstTouchTarget 有值,走else模块if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // alreadyDispatchedToNewTouchTarget 是 false if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { // 此处的结果是 false final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted; // 在这里被分发处理 child就是我们要分发的对象 if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; }} alreadyDispatchedToNewTouchTarget 在每次进来时都会重置为 false ,最后又会调用 dispatchTransformedTouchEvent 处理分发 if (child == null) { handled = super.dispatchTouchEvent(transformedEvent);} else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } // 递归调用它来分发 handled = child.dispatchTouchEvent(transformedEvent);} 至此move事件也结束,做个总结 DOWN 事件是事件分发,寻找接盘的 child 并保存在 mFirstTouchTarget 中 MOVE 事件虽然不需要遍历寻找接盘的view,但还可以被ViewGroup拦截的(比如ViewPager包裹着RecyclerView,DOWN事件时被RecyclerView拦截,横向滑动时被抛弃,这时候ViewPager是可以拦截横向滑动接盘的) 解决冲突方案 滑动冲突解决方案有两种:内部拦截、外部拦截。顾名思义,内部拦截是在子View中写逻辑拦截,外部拦截则是从父布局下手解决问题 都以ViewPager包裹RecyclerView滑动冲突为例 外部拦截public class BadViewPager extends ViewPager { private int mLastX, mLastY; public BadViewPager(@NonNull Context context) { super(context); } public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } // 外部拦截法:父容器处理冲突 // 我想要把事件分发给谁就分发给谁 @Override public boolean onInterceptTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { mLastX = (int) event.getX(); mLastY = (int) event.getY(); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (Math.abs(deltaX) >

Math.abs (deltaY) {/ / intercept return true;} break;} case MotionEvent.ACTION_UP when sliding sideways: {break;} default: break;} return super.onInterceptTouchEvent (event) }} Internal interception

ViewPager code

Public class BadViewPager extends ViewPager {private int mLastX, mLastY; public BadViewPager (@ NonNull Context context) {super (context);} public BadViewPager (@ NonNull Context context, @ Nullable AttributeSet attrs) {super (context, attrs);} @ Override public boolean onInterceptTouchEvent (MotionEvent event) {if (event.getAction () = = MotionEvent.ACTION_DOWN) {super.onInterceptTouchEvent (event) / / here is the key return false;} return true;}}

RecyclerView code

Public class MyListView extends ListView {public MyListView (Context context) {super (context);} public MyListView (Context context, AttributeSet attrs) {super (context, attrs);} / / Internal interception: sub-view handles event conflicts private int mLastX, mLastY; @ Override public boolean dispatchTouchEvent (MotionEvent event) {int x = (int) event.getX (); int y = (int) event.getY () Switch (event.getAction ()) {case MotionEvent.ACTION_DOWN: {getParent () .requestDisallowInterceptTouchEvent (true); break;} case MotionEvent.ACTION_MOVE: {int deltaX = x-mLastX; int deltaY = y-mLastY If (Math.abs (deltaX) > Math.abs (deltaY)) {getParent () .requestDisallowInterceptTouchEvent (false);} break;} case MotionEvent.ACTION_UP: {break;} default: break } mLastX = x; mLastY = y; return super.dispatchTouchEvent (event);}}

It is important to note here that the parent layout must return false when ACTION_DOWN. The reasons are as follows:

When the DOWN event is distributed, the resetTouchState (); function is executed

Private void resetTouchState () {clearTouchTargets (); resetCancelNextUpFlag (this); mGroupFlags & = ~ FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE;}

MGroupFlags & = ~ FLAG_DISALLOW_INTERCEPT

When judging the interception of the parent layout

Final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0 restore action in case it was changed if (! disallowIntercept) {intercepted = onInterceptTouchEvent (ev); ev.setAction (action); / / restore action in case it was changed} else {intercepted = false;}

That is, mGroupFlags & = ~ FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT! = 0 = "false

Using the if statement is always true, where ViewPager intercepts events, so RecyclerView cannot slide up and down. So modify the onInterceptTouchEvent function of the parent layout during internal interception!

This is the end of this article on "sample analysis of event distribution mechanism and handling in custom view in Android". I hope the above content can be of some help to you, so that you can learn more knowledge. if you think the article is good, please share it for more people to see.

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