Home > Java > javaTutorial > body text

An in-depth understanding of Android Touch event distribution

高洛峰
Release: 2017-01-16 16:43:59
Original
965 people have browsed it

This article will take you to learn more about the distribution of touch events. The specific content is as follows
1. Touch actions and event sequences

(1) Actions of touch events

There are a total of touch actions Three types: ACTION_DOWN, ACTION_MOVE, ACTION_UP. When the user's finger touches the screen, a touch event with the action ACTION_DOWN is generated. At this time, if the user's finger leaves the screen immediately, a touch event with the action ACTION_UP is generated; if the user's finger continues to slide after touching the screen, when the sliding distance exceeds If the distance constant predefined in the system is reached, a touch event with the action ACTION_MOVE is generated. The distance constant predefined in the system to determine whether the sliding of the user's finger on the screen is an ACTION_MOVE action is called TouchSlop, which can be passed through ViewConfiguration. get(getContext()).getScaledTouchSlop() gets.

(2) Event sequence

When the user's finger touches the screen, slides on the screen, and then leaves the screen, this process will generate a series of touch events: ACTION_DOWN-->Several ACTION_MOVE -->ACTION_UP. This series of touch events is an event sequence.

2. Distribution of touch events

(1) Overview

When a touch time occurs, the system is responsible for giving the touch event to a View (TargetView) Processing, the process of passing touch events to TargetView is the distribution of touch events.

Distribution order of touch events: Activity-->Top-level View-->Sub-View of top-level View-->. . .-->Target View

Touch event Response sequence: TargetView --> TargetView's parent container --> . . . --> Top-level View -->Activity

(2) Specific process of toush event distribution

a. Distribution of touch events by Activity

When the user's finger touches the screen, a touch event is generated. The MotionEvent that encapsulates the touch event is first passed to the current Activity. The dispatchTouchEvent method of the Activity is responsible for the touch event. distribution. The actual work of distributing touch events is done by the current Activity's Window, and the Window will pass the touch events to DecorView (the top-level View of the current user interface). Activity's dispatchTouchEvent method code is as follows:

public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  }
  if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
  }
  return onTouchEvent(ev);
}
Copy after login

From the above code, it can be seen that PhoneWindow's superDispatchTouchEvent method actually completes its work through DecorView's superDispatchTouchEvent method. In other words, the current Activity's Window directly transfers this touch The event is passed to DecorView. In other words, the touch event has been distributed as follows: Activity-->Window-->DecorView.

b. Distribution of touch events by top-level View

After distribution by Activity and Window, the touch event has now been passed to the dispatchTouchEvent method of DecorView. DecorView is essentially a ViewGroup (more specifically, FrameLayout). The work done by the dispatchTouchEvent method of ViewGroup can be divided into the following stages. The main code of the first stage is as follows:

//Handle an initial down.
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();
}
Copy after login

The first stage There are two main tasks: one is to complete the reset of the FLAG_DISALLOW_INTERCEPT mark in the resetTouchState method on line 6; the other is that the cancelAndClearTouchTargets method on line 5 will clear the touch target of the current MotionEvent. Regarding the FLAG_DISALLOW_INTERCEPT mark and touch target, there will be relevant instructions below.

The main work of the second phase is to decide whether the current ViewGroup intercepts this touch event. The main code is as follows:

//Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWM || 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.
  intercept =true;
}
Copy after login

From the above code we can know that when a touch event is passed to the ViewGroup When, it will first determine whether the action of this touch event is ACTION_DOWN. If this event is ACTION_DOWN or mFirstTouchTarget is not null, it will decide whether to intercept this touch event based on the FLAG_DISALLOW_INTERCEPT flag. So what is mFirstTouchTarget? When the touch event is successfully processed by the ViewGroup's sub-View, mFirstTouchTarget will be assigned to the View that successfully processed the touch event, which is the touch target raised above.

Summarize the process of the above code: When the sub-View does not interfere with the interception of ViewGroup (disallowIntercept in the above code is false), if the current event is ACTION_DOWN or mFirstTouchTarget is not empty, ViewGroup will be called The onInterceptTouchEvent method determines whether to ultimately intercept this event; otherwise (there is no TargetView and this event is not ACTION_DOWN), the current ViewGroup will intercept this event. Once the ViewGroup intercepts a touch event, mFirstTouchTarget will not be assigned a value. Therefore, when ACTION_MOVE or ACTION_UP is passed to the ViewGroup, mTouchTarget will be null, so the condition in line 3 of the above code will be false, and the ViewGroup will Intercept it. The conclusion that can be drawn from this is that once ViewGroup intercepts an event, the remaining events in the same event sequence will be intercepted by default without asking whether to intercept (that is, onInterceptTouchEvent will not be called again).

这里存在一种特殊情形,就是子View通过requestDisallowInterceptTouchEvent方法设置父容器的FLAG_DISALLOW_INTERCEPT为true,这个标记指示是否不允许父容器拦截,为true表示不允许。这样做能够禁止父容器拦截除ACTION_DOWN以外的所有touch事件。之所以不能够拦截ACTION_DOWN事件,是因为每当ACTION_DOWN事件到来时,都会重置FLAG_DISALLOW_INTERCEPT这个标记位为默认值(false),所以每当开始一个新touch事件序列(即到来一个ACTION_DOWN动作),都会通过调用onInterceptTouchEven询问ViewGroup是否拦截此事件。当ACTION_DOWN事件到来时,重置标记位的工作是在上面的第一阶段完成的。

接下来,会进入第三阶段的工作:

final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
  // 不是ACTION_CANCEL并且不拦截
  if (actionMasked == MotionEvent.ACTION_DOWN) {
     // 若当前事件为ACTION_DOWN则去寻找这次事件新出现的touch target
     final int actionIndex = ev.getActionIndex(); // always 0 for down
 
     ...
 
     final int childrenCount = mChildrenCount;
     if (newTouchTarget == null && childrenCount != 0) {
       // 根据触摸的坐标寻找能够接收这个事件的touch target
       final float x = ev.getX(actionIndex);
       final float y = ev.getY(actionIndex);
 
       final View[] children = mChildren;
       // 遍历所有子View
       for (int i = childrenCount - 1; i >= 0; i--) {
         final int childIndex = i;
         final View child = children[childIndex];
         // 寻找可接收这个事件并且touch事件坐标在其区域内的子View
         if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
           continue;
         }
 
         newTouchTarget = getTouchTarget(child); // 找到了符合条件的子View,赋值给newTouchTarget
         if (newTouchTarget != null) {
           //Child is already receiving touch within its bounds.
           //Give it the new pointer in addition to ones it is handling.
           newTouchTarget.pointerIdBits |= idBitsToAssign;
           break;
         }
         resetCancelNextUpFlag(child);
         // 把ACTION_DOWN事件传递给子组件进行处理
         if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
           //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();
           //把mFirstTouchTarget赋值为newTouchTarget,此子View成为新的touch事件的起点
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           alreadyDispatchedToNewTouchTarget = true;
           break;
         }           
       }
     }
  }
}
Copy after login

当ViewGroup不拦截本次事件,则touch事件会分发给它的子View进行处理,相关代码从第21行开始:遍历所有ViewGroup的子View,寻找能够处理此touch事件的子View,若一个子View不在播放动画并且touch事件坐标位于其区域内,则该子View能够处理此touch事件,并且会把该子View赋值给newTouchTarget。

若当前遍历到的子View能够处理此touch事件,就会进入第38行的dispatchTransformedTouchEvent方法,该方法实际上调用了子View的dispatchTouchEvent方法。dispatchTransformedTouchEvent方法中相关的代码如下:

if (child == null) {
  handled = super.dispatchTouchEvent(event);
} else {
  handled = child.dispatchTouchEvent(event);
}
Copy after login

若dispatchTransformedTouchEvent方法传入的child参数不为null,则会调用child(即处理touch事件的子View)的dispatchTouchEvent方法。若该子View的dispatchTouchEvent方法返回true,则dispatchTransformedTouchEvent方法也会返回true,则表示成功找到了一个处理该事件的touch target,会在第55行把newTouchTarget赋值给mFirstTouchTarget(这一赋值过程是在addTouchTarget方法内部完成的),并跳出对子View遍历的循环。若子View的dispatchTouchEvent方法返回false,ViewGroup就会把事件分发给下一个子View。

若遍历了所有子View后,touch事件都没被处理(该ViewGroup没有子View或是所有子View的dispatchTouchEvent返回false),ViewGroup会自己处理touch事件,相关代码如下:

if (mFirstTouchTarget == null) {
  handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
Copy after login

由以上代码可知,ViewGroup自己处理touch事件时,会调用dispatchTransformedTouchEvent方法,传入的child参数为null。根据上文的分析,传入的chid为null时,会调用super.dispatchTouchEvent方法,即调用View类的dispatchTouchEvent方法。

c. View对touch事件的处理

View的dispatchTouchEvent方法的主要代码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
  boolean result = false;
  . . .
   
  if (onFilterTouchEventForSecurity(event)) {
    //noinspection SimplifiableIfStatement
    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;
    }
    . . .
    return result;
}
Copy after login

由上述代码可知,View对touch事件的处理过程如下:由于View不包含子元素,所以它只能自己处理事件。它首先会判断是否设置了OnTouchListener,若设置了,会调用onTouch方法,若onTouch方法返回true(表示该touch事件已经被消耗),则不会再调用onTouchEvent方法;若onTouch方法返回false或没有设置OnTouchListener,则会调用onTouchEvent方法,onTouchEvent对touch事件进行具体处理的相关代码如下:

if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
      boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
      if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        . . .
        if (!mHasPerformedLongPress) {
          //This is a tap, so remove the longpress check 
          removeLongPressCallback();
           
          //Only perform take click actions if we were in the pressed state
          if (!focusTaken) {
            //Use a Runnable and post this rather than calling performClick directly.
            //This lets other visual state of the view update before click actions start.
            if (mPerformClick == null) {
              mPerformClck = new PeformClick();
            }
            if (!post(mPerformClick)) {
              performClick();
            }
          }
        }
        . . .
      }
      break;
  }
  . . .
  return true;
}
Copy after login

由以上代码可知,只要View的CLICKABLE属性和LONG_CLICKABLE属性有一个为true(View的CLICKABLE属性和具体View有关,LONG_CLICKABLE属性默认为false,setOnClikListener和setOnLongClickListener会分别自动将以上俩属性设为true),那么这个View就会消耗这个touch事件,即使这个View处于DISABLED状态。若当前事件是ACTION_UP,还会调用performClick方法,该View若设置了OnClickListener,则performClick方法会在其内部调用onClick方法。performClick方法代码如下:

public boolean performClick() {
  final boolean result;
  final ListenerInfo li = mListenerInfo;
  if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);
    result = true;
  } else {
    result = false;
  }
  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  return result;
}
Copy after login

以上是我学习Android中触摸事件分发后的简单总结,很多地方叙述的还不够清晰准确

更多Android Touch事件分发深入了解相关文章请关注PHP中文网!

Related labels:
source:php.cn
Statement of this Website
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
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!