Home >Java >javaTutorial >Detailed explanation of Android View event distribution mechanism

Detailed explanation of Android View event distribution mechanism

高洛峰
高洛峰Original
2017-01-16 16:33:331063browse

Android development, touch is everywhere. For some students who don't know much about source code, they may have some doubts about this. The distribution mechanism of View events will not only encounter these problems when meeting business requirements, but it is also often asked in some written interview questions. It can be said to be a cliché. I have read many articles written by people in this area before. They are either too long-winded or too vague, and some of the details are controversial, so I will reorganize this content again and let you understand it in ten minutes. View Event distribution mechanism.

To put it bluntly, the event distribution mechanism of these touch events is to figure out the three methods, dispatchTouchEvent(), OnInterceptTouchEvent(), onTouchEvent(), and the problem of stacking these three methods with n ViewGroups and Views , no matter how complex the structure is, it can be split into 1 ViewGroup + 1 View.

In fact, ViewGroup and View are very similar. View just does not have sub-containers, so there is no interception problem. Dispatch is also very simple, so once you understand ViewGroup, you will almost understand it.

Three key methods

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup handles the initiator of event distribution. View/ViewGroup receives the touch event and dispatches it first. This is the method that starts, and then in this method it is judged whether to process the interception or distribute the event to the sub-container

public boolean onInterceptTouchEvent(MotionEvent ev)

Special for ViewGroup, this method can be achieved The direction of distribution of control events. Generally, this method can be used to determine whether to give the event to the ViewGroup alone or continue to pass it to the sub-container. It is the best place to handle event conflicts.

public boolean onTouchEvent(MotionEvent event)

The real handler of touch events, and finally each event will be processed here

Core issue

What are the difficulties of the time distribution mechanism? I think the difficulties are as follows: Point: Three method calling rules to determine the object that handles the event and the resolution of the event conflict.

Event delivery rules

Generally, there will be a series of MotionEvents in one click, which can be simply divided into: down->move->….->move->up, when once When the event is distributed to the ViewGroup, the calling sequence in the ViewGroup between the above three methods can be expressed by a simple code

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);
 
public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isConsumed = false;
  if(onInterceptTouchEvent(ev)){
   isCousumed = this.onTouchEvent(ev);
  }else{
   isConsumed = childView.dispatchTouchEvent(ev);
  }
  return isConsumed;
}

The return result true indicates that the event has been processed, and the return false indicates that it has not been processed. . The above code is easy to understand and looks very simple. It can be summarized in one sentence. After the ViewGroup receives the event, it calls dispatch. In dispatch, it first checks whether to intercept. If intercepted, the ViewGroup eats the event. Otherwise, it is handed over to someone with processing capabilities. subcontainer processing.

However, simplicity is simplicity. It is written like this just to facilitate understanding. Of course, the event processing process of ViewGroup is not that simple. Many details are ignored here. I will continue to add them next. Going back to what was said above, a series of events we often deal with are usually one down, multiple moves and one up. The above pseudocode alone cannot perfectly solve these problems. Let's look directly at ViewGroup's dispatchTouchEvent.

onInterceptTouchEvent calling conditions

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;
}

Explain the above code, it seems very simple, but is it really simple? . Before explaining, let’s talk about the meaning of intercepted. intercepted == false means that the parent container ViewGroup does not intercept the event temporarily, and the event has the opportunity to be passed to the child View for processing. Returning true means that the parent container directly intercepted the series of events and will not pass them on later. Give it to the child View. If the sub-View wants to obtain the event, it can only make the value false

The onInterceptTouchEvent call returns false (returning false can be passed to the sub-View, which corresponds to the content in else of the pseudo code above. To pass the event to the sub-container, the following requirements must be met: The content is better understood) It may be triggered if any one of two conditions is met (of course it is only possible):

One is when it is down, and the other is mFirstTouchTarget! =null, when is mFirstTouchTarget not empty? Interested students can look at the timing of calling the addTouchTarget method in ViewGroup. mFirstTouchTarget is assigned a value here. The source code is too long and I won’t post it.

mFirstTouchTarget is used to save the child View that consumes the ACTION_DOWN event in the ViewGroup, that is, in the above pseudocode, child.dispatchTouchEvent(ev) returns true when it is ACTION_DOWN, as long as the dispatch of the child View is in ACTION_DOWN If true is returned, it will not be null (this assignment process only occurs in ACTION_DOWN. If the sub-ViewACTION__DOWN does not assign it a value, the subsequent sequence of events will not happen again). On the contrary, if there is no sub-View processing, the object will be null. Of course, it is not enough to meet the above two conditions, you must also meet !disallowIntercept.

The disallowIntercept variable is very interesting. Its value is mainly affected by the flag FLAG_DISALLOW_INTERCEPT. This value can be set by the sub-View of ViewGroup. If the sub-View of ViewGroup calls the requestDisallowInterceptTouchEvent method, it will change FLAG_DISALLOW_INTERCEPT, resulting in disallowIntercept. The value is true. In this case, the intercept will be skipped, causing the interception to fail.

但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。

所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。

上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。

再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:

if (!canceled && !intercepted) {
....
if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 ....
 for(child : childList){
   if(!child satisfied condition....){
     continue;
   }
   newTouchTarget = addTouchTarget(child, idBitsToAssign);//在这里给mFirstTouchTarget赋值
 }
 
 }
}

可以在这里看到intercepted为false在ACTION_DOWN里才能给上面说过的mFirstTouchTarget赋值,只有mFirstTouchTarget不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。

mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器ViewGroup处理了,真正处理事件传递的方法是dispatchTransformedTouchEvent,再看源码:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
      View child, int desiredPointerIdBits) {
   final boolean handled;
 
    // Canceling motions is a special case. We don't need to perform any transformations
    // or filtering. The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
      event.setAction(MotionEvent.ACTION_CANCEL);
      if (child == null) {
        handled = super.dispatchTouchEvent(event);
      } else {
        handled = child.dispatchTouchEvent(event);
      }
      event.setAction(oldAction);
      return handled;
    }
 
}

 

看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。

那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。

所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:

if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
      } else {
     ...
     dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)
   }

这就是为什么mFirstTouchTarget能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。

这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。

孩子是谁的

继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码、

if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
   continue;
}

ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。

伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。

一些没说到但也很重要的点

Everything explained above is very simple. It starts with a ViewGroup + a View. The executor of event distribution is the ViewGroup, and the sub-container only has one View. But of course it is not that simple in actual development, but don’t be afraid, no matter how complicated it is The situation can also be split into this mode, but the levels are more recursive and more complicated, and the principle is still the same.

A few additional points by the way:

When a series of click events are triggered from the user clicking on the screen, the real event delivery process is: Activity (PhoneWindow)->DecorView->ViewGroup- >View, there is a DecorView before reaching the ViewGroup. The event is passed from the Activity, but these things are actually the same as the ViewGroup. The Activity can be regarded as a large ViewGroup. When all the children contained in its DecorView When no one in the View can consume the event (there is a loophole in saying this, just understand what I mean) it will eventually be handed over to the Activity for processing.

Event conflict resolution can be handled in several points according to the above principles. The easiest time to think of the processing time is in onInterceptTouchEvent. For example, when a vertically sliding ViewGroup is nested in a horizontally sliding ViewGroup, you can use ACTION_MOVE here to determine who should pass subsequent events to for processing. Of course, you can also According to the flag bit FLAG_DISALLOW_INTERCEPT mentioned above, it is easier to think of to control the flow of events in conjunction with the dispatchTouchEvent of the child View. However, I have seen other experts control the flow of events by sharing the MotionEvent method, that is, saving it in the parent container. It is also feasible to use MotionEvent and pass in the child View's custom event handling method at the appropriate time to share events.

As long as any View rejects ACTION_DOWN (returns false) in a series of events, subsequent events will not be passed over. But if other events are rejected, subsequent events can still be passed. For example, if ACTION_MOVE is not processed by the View, this unprocessed event will eventually be consumed by the Activity (not the parent container of the View), but subsequent events will still be passed. Continue to pass to the View.

Reasonable use of ACTION_CANCEL can control the life cycle of a series of events, making event processing more flexible.

To understand the mechanism of event distribution, it is basically enough to understand the above principles. You can also see the event distribution of various cool custom controls written by many awesome masters on github. Based on these, you can also see Understand, of course there are a lot of extensions and more in-depth content that I won’t go into here due to space limitations. What’s more important is to look at the source code.
Finally, I would like to give you a classic quote: What you see on paper will eventually make you realize it, but you will definitely know that you have to do it!

The above is the collection of information on the Android View event distribution mechanism. We will continue to add relevant information in the future. Thank you for your support of this site!

For more detailed articles on the Android View event distribution mechanism, please pay attention to the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn