Maison  >  Article  >  Java  >  Une compréhension approfondie de la distribution d'événements Android Touch

Une compréhension approfondie de la distribution d'événements Android Touch

高洛峰
高洛峰original
2017-01-16 16:43:59989parcourir

Cet article vous amène à en savoir plus sur la distribution des événements tactiles. Le contenu spécifique est le suivant
1. Actions tactiles et séquences d'événements

(1) Actions des événements tactiles

<.> Il existe au total trois types d'actions tactiles : ACTION_DOWN, ACTION_MOVE, ACTION_UP. Lorsque le doigt de l'utilisateur touche l'écran, un événement tactile avec l'action ACTION_DOWN est généré. A ce moment, si le doigt de l'utilisateur quitte l'écran immédiatement, un événement tactile avec l'action ACTION_UP est généré si le doigt de l'utilisateur continue de glisser après avoir touché ; l'écran, lorsque la distance de glissement dépasse Si la constante de distance prédéfinie dans le système est atteinte, un événement tactile avec l'action ACTION_MOVE est généré La constante de distance prédéfinie dans le système pour déterminer si le glissement du doigt de l'utilisateur sur l'écran est un. L'action ACTION_MOVE est appelée TouchSlop, qui peut être transmise via ViewConfiguration get(getContext()).getScaledTouchSlop().

(2) Séquence d'événements

Lorsque le doigt de l'utilisateur touche l'écran, glisse sur l'écran, puis quitte l'écran, ce processus génère une série d'événements tactiles : ACTION_DOWN--> ; Plusieurs ACTION_MOVE -->ACTION_UP. Cette série d'événements tactiles est une séquence d'événements.

2. Distribution des événements tactiles

(1) Présentation

Lorsqu'un moment de contact se produit, le système est chargé de transmettre l'événement tactile à un traitement View (TargetView). , le processus de transmission des événements tactiles à TargetView est la distribution des événements tactiles.

Ordre de distribution des événements tactiles : Activité-->Vue de niveau supérieur-->Vue enfant de la vue de niveau supérieur--> .-->Vue cible

Séquence de réponse à l'événement tactile : TargetView --> Conteneur parent de TargetView --> Vue de niveau supérieur -->Activité

(2) Le processus spécifique de distribution des événements.

a. Répartition des événements tactiles par activité

Lorsque le doigt de l'utilisateur touche l'écran, un événement tactile est généré. Le MotionEvent qui encapsule l'événement tactile est d'abord transmis à l'activité actuelle. La méthode dispatchTouchEvent de l’activité est responsable de la distribution de l’événement tactile. Le travail réel de distribution des événements tactiles est effectué par la fenêtre de l'activité actuelle, et la fenêtre transmettra les événements tactiles à DecorView (la vue de niveau supérieur de l'interface utilisateur actuelle). Le code de la méthode dispatchTouchEvent de Activity est le suivant :

À partir du code ci-dessus, on peut voir que la méthode superDispatchTouchEvent de PhoneWindow termine en fait son travail via la méthode superDispatchTouchEvent de DecorView. Fenêtre de l'activité en cours directement Cet événement tactile est transmis à DecorView. En d’autres termes, l’événement tactile a été distribué comme suit : Activity-->Window-->DecorView.
public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  }
  if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
  }
  return onTouchEvent(ev);
}

b. Distribution des événements tactiles par vue de niveau supérieur

Après distribution par activité et fenêtre, l'événement tactile a maintenant été transmis à la méthode dispatchTouchEvent de DecorView. DecorView est essentiellement un ViewGroup (plus précisément, FrameLayout). Le travail effectué par la méthode dispatchTouchEvent de ViewGroup peut être divisé en les étapes suivantes. Le code principal de la première étape est le suivant :

Chapitre. Il y a deux tâches principales dans la première étape : premièrement, la méthode resetTouchState sur la ligne 6 termine la réinitialisation de l'indicateur FLAG_DISALLOW_INTERCEPT ; deuxièmement, la méthode CancelAndClearTouchTargets sur la ligne 5 effacera la cible tactile du MotionEvent actuel. Concernant la marque FLAG_DISALLOW_INTERCEPT et la cible tactile, vous trouverez ci-dessous des instructions pertinentes.
//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();
}

Le travail principal de la deuxième phase est de décider si le ViewGroup actuel intercepte cet événement tactile. Le code principal est le suivant :

À partir du code ci-dessus, nous pouvons savoir quand. un événement tactile est délivré. En arrivant au ViewGroup, il déterminera d'abord si l'action de l'événement tactile est ACTION_DOWN. Si l'événement est ACTION_DOWN ou si mFirstTouchTarget n'est pas nul, il sera déterminé s'il faut intercepter l'événement tactile en fonction de FLAG_DISALLOW_INTERCEPT. drapeau. Alors, qu’est-ce que mFirstTouchTarget ? Lorsque l'événement tactile est traité avec succès par la sous-vue du ViewGroup, mFirstTouchTarget sera attribué à la vue qui a traité avec succès l'événement tactile, qui est la cible tactile soulevée ci-dessus.
//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;
}

Résumez le processus du code ci-dessus : Lorsque la sous-vue n'interfère pas avec l'interception de ViewGroup (disallowIntercept dans le code ci-dessus est faux), si l'événement en cours est ACTION_DOWN ou mFirstTouchTarget n'est pas vide, ViewGroup sera appelé La méthode onInterceptTouchEvent détermine s'il faut finalement intercepter cet événement ; sinon (il n'y a pas de TargetView et cet événement n'est pas ACTION_DOWN), le ViewGroup actuel interceptera cet événement. Une fois que ViewGroup intercepte un événement tactile, mFirstTouchTarget ne se verra pas attribuer de valeur. Par conséquent, lorsque ACTION_MOVE ou ACTION_UP est transmis à ViewGroup, mTouchTarget sera nul, donc la condition de la ligne 3 du code ci-dessus sera fausse et ViewGroup. va l'intercepter. La conclusion que l'on peut en tirer est qu'une fois que ViewGroup intercepte un événement, les événements restants dans la même séquence d'événements seront interceptés par défaut sans demander s'il faut l'intercepter (c'est-à-dire que onInterceptTouchEvent ne sera plus appelé).

    这里存在一种特殊情形,就是子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;
         }           
       }
     }
  }
}

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

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

    由以上代码可知,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;
}

    由上述代码可知,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;
}

    由以上代码可知,只要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;
}

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

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

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn