ホームページ >Java >&#&チュートリアル >Android Touch イベント配信についての深い理解

Android Touch イベント配信についての深い理解

高洛峰
高洛峰オリジナル
2017-01-16 16:43:59992ブラウズ

この記事では、タッチイベントの分布について詳しく説明します
1. タッチアクションとイベントシーケンス

(1) タッチイベントのアクション

タッチアクションには次の 3 種類があります。 、ACTION_MOVE、およびACTION_UP。ユーザーの指が画面に触れると、アクション「ACTION_DOWN」のタッチイベントが生成されます。このとき、ユーザーの指がすぐに画面から離れると、ユーザーの指がタッチした後もスライドし続けると、アクション「ACTION_UP」のタッチイベントが生成されます。スライド距離がシステムで事前定義された距離定数に達すると、システムで事前定義された距離定数のアクション ACTION_MOVE を伴うタッチ イベントが生成され、画面上でのユーザーの指のスライドが距離定数であるかどうかが判断されます。 ACTION_MOVE アクションは TouchSlop と呼ばれ、ViewConfiguration を通じて渡されます。getScaledTouchSlop() を取得します。

(2) イベントシーケンス

ユーザーの指が画面に触れ、画面上をスライドし、画面から離れると、このプロセスにより一連のタッチ イベントが生成されます: ACTION_DOWN-->複数の ACTION_MOVE-->ACTION_UP 。この一連のタッチイベントがイベントシーケンスである。

2. タッチイベントの配信

(1) 概要

タッチタイムが発生すると、システムはタッチイベントをView(TargetView)に渡して処理する処理を行います。イベントの配信です。

タッチ イベントの配信順序: アクティビティ -> トップレベル ビュー -> トップレベル ビューのサブビュー -> ターゲット ビュー

の応答順序。タッチイベント: TargetView --> TargetView の親コンテナ -->アクティビティ

(2) タッチイベントの配信の具体的なプロセス

events

ユーザーの指が画面に触れると、タッチ イベントをカプセル化する MotionEvent が最初に現在のアクティビティに渡され、タッチ イベントの配布が行われます。タッチ イベントを配布する実際の作業は、現在のアクティビティの Window によって実行され、Window はタッチ イベントを DecorView (現在のユーザー インターフェイスのトップレベル ビュー) に渡します。 Activity のdispatchTouchEvent メソッドのコードは次のとおりです:

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

上記のコードから、PhoneWindow の superDispatchTouchEvent メソッドが実際には DecorView の superDispatchTouchEvent メソッドを通じて作業を完了することがわかります。つまり、現在の Activity の Window はこのタッチ イベントを DecorView に直接渡します。つまり、タッチ イベントは、Activity->Window->DecorView のように分散されています。

b. トップレベル View によるタッチ イベントの配布

Activity と Window による配布後、タッチ イベントは DecorView のdispatchTouchEvent メソッドに渡されます。 DecorView は本質的に ViewGroup (より具体的には FrameLayout) です。ViewGroup のdispatchTouchEvent メソッドによって実行される作業は、次の段階に分けることができます。 最初の段階の主なコードは次のとおりです。 2 つ目: 最初に、FLAG_DISALLOW_INTERCEPT フラグのリセットが 6 行目のresetTouchState メソッドで完了し、2 番目に、5 行目の cancelAndClearTouchTargets メソッドが現在の MotionEvent のタッチ ターゲットをクリアします。 FLAG_DISALLOW_INTERCEPT マークとタッチターゲットについては、以下に関連する手順があります。

第 2 フェーズの主な作業は、現在の ViewGroup がこのタッチ イベントをインターセプトするかどうかを決定することです。主なコードは次のとおりです:

//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();
}

上記のコードから、タッチ イベントが ViewGroup に渡されるとき、アクションが ACTION_DOWN であるかどうかが最初に判断されます。イベントが ACTION_DOWN であるか、mFirstTouchTarget が null でない場合は、FLAG_DISALLOW_INTERCEPT フラグに基づいてタッチ イベントをインターセプトするかどうかが決定されます。では、mFirstTouchTarget とは何でしょうか?タッチ イベントが ViewGroup のサブ View によって正常に処理されると、mFirstTouchTarget はタッチ イベントを正常に処理した View (上記で発生したタッチ ターゲット) に割り当てられます。

上記のコードのプロセスの概要: サブビューが ViewGroup のインターセプトを妨げない場合 (上記のコードの disallowIntercept が false)、現在のイベントが ACTION_DOWN であるか、mFirstTouchTarget が空でない場合、onInterceptTouchEvent メソッドViewGroup の が呼び出され、最終的にこのイベントをインターセプトするかどうかを決定します。それ以外の場合 (TargetView がなく、このイベントが ACTION_DOWN ではない)、現在の ViewGroup がこのイベントをインターセプトします。 ViewGroup がタッチ イベントをインターセプトすると、mFirstTouchTarget には値が割り当てられなくなります。そのため、ACTION_MOVE または ACTION_UP が ViewGroup に渡されると、mTouchTarget は null になり、上記のコードの 3 行目の条件は false となり、ViewGroup はそれを迎撃します。このことから得られる結論は、ViewGroup がイベントをインターセプトすると、同じイベント シーケンス内の残りのイベントは、インターセプトするかどうかを尋ねることなく、デフォルトでインターセプトされる (つまり、onInterceptTouchEvent は再度呼び出されない) ということです。

    这里存在一种特殊情形,就是子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中文网!

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。