Androidにおけるontouch事件の伝達メカニズムを詳細に分析する。


onTachの紹介
オンチはAndroidシステムにおけるイベント全体のメカニズムの基礎である。Androidの中のその他の事件、例えばonClick、onLongClickなどはすべてonTachを基礎としたのです。
onTachは指から携帯電話の画面を離れるまでの過程を含み、ミクロの形式では具体的にaction_として表現されています。ダウン、action_moveとaction_アップロードなどの過程。
onTachの二つの主な定義形式は以下の通りです。
1.カスタムコントロールでは、よくある上書きonTouchEvent(MotionEvent ev)方法があります。開発中によく書き換えたontouchEventの方法を見ることができます。
また、その中には異なるミクロ表現があります。ダウン、action_moveとaction_upなど)の対応する判断は、論理を実行して異なるブール値を返すことができます。
2.コードの中で、直接に既存のコントロールにset OnTouch Listenerモニターを設定します。モニターのワンタッチ方法を書き直します。ontouchコール関数にはviewとMotionEventがあります。
ontouch事件伝達メカニズム
一般的に私たちが使っているUIコントロールは共通の親から継承されているということを知っています。だからViewという類はontouch事件の関連処理を管理しているべきです。ビューの中でタッチに関する方法を探してみましょう。そのうちの一つが私たちの注意を引きやすいです。
方法名によっては、タッチイベントを配信する責任があります。以下のソースコードを示します。

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
 public boolean dispatchTouchEvent(MotionEvent event) {
 // If the event should be handled by accessibility focus first.
 if (event.isTargetAccessibilityFocus()) {
 // We don't have focus or no virtual descendant has it, do not handle the event.
 if (!isAccessibilityFocusedViewOrHost()) {
  return false;
 }
 // We have focus and got the event, then use normal event dispatch.
 event.setTargetAccessibilityFocus(false);
 }

 boolean result = false;

 if (mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
 }

 final int actionMasked = event.getActionMasked();
 if (actionMasked == MotionEvent.ACTION_DOWN) {
 // Defensive cleanup for new gesture
 stopNestedScroll();
 }

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

 if (!result && mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
 }

 // Clean up after nested scrolls if this is the end of a gesture;
 // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
 // of the gesture.
 if (actionMasked == MotionEvent.ACTION_UP ||
  actionMasked == MotionEvent.ACTION_CANCEL ||
  (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
 stopNestedScroll();
 }

 return result;
}
ソースは少し長いですが、すべての行を見る必要はありません。まず、dispatch TouchEventの戻り値はbootleanタイプであることに気づき、注釈上の解釈 dispatchTouchEvent(MotionEvent event) は、このタッチイベントがこのViewによって消費されたらtrueに戻ります。そうでなければfalseに戻ります。方法では、まず、このイベントがフォーカスを得ているかどうかを判断し、焦点を得ていない場合はfalseに直接戻ります。次に、:@return True if the event was handled by the view, false otherwise.のこのセグメントに目を向けてみましょう。ここにliという局所変数があり、ListenerInfo類に属し、mListenerInfo賦を経て価値があります。ListenerInfoは包装類だけで、中には大量のモニターが封入されています。
Viewの中からmListenerInfoを探しに行きます。次のコードが見られます。

ListenerInfo getListenerInfo() {
 if (mListenerInfo != null) {
 return mListenerInfo;
 }
 mListenerInfo = new ListenerInfo();
 return mListenerInfo;
}
ですから、私たちはmListenerInfoが空ではないことを知ることができます。だからliも空ではないです。最初はtrueと判断して、li.mOnTouchListenerを見て、前にListenerInfoは一つのモニターのパッケージです。だから私たちは同様にmOnTouchListenerを追跡します。

/**
 * Register a callback to be invoked when a touch event is sent to this view.
 * @param l the touch listener to attach to this view
 */
public void setOnTouchListener(OnTouchListener l) {
 getListenerInfo().mOnTouchListener = l;
}
上記の方法でmOnTouch Listenerを設置しています。上の方法はきっとおなじみでしょう。私達が普段使っているxx.set OnTouch Listenerです。いいです。OnTouch Listenerを設置すれば、第二の判断もtrueです。第三の判断はViewがenableであれば、デフォルトです。最後の一つが残っています。if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event))は第二の判断において、モニターのli.mOnTouchListener.onTouch(this, event) 方法を変更しました。onTouch()方法がtrueに戻ると、上の四つの判断は全部trueで、onTouch()方法はtrueに戻ります。dispatchTouchEvent()の判断は実行されません。この判断では、私たちはまたおなじみの方法を見ました。if (!result && onTouchEvent(event))。ですから、ontouchEventを実行するには、上の4つの判断の中に少なくとも一つのfalseが必要です。onTouchEvent()方法で戻ったのはfalseだと仮定してください。これでワンタッチイベントを順調に実行しました。ワンタッチイベントのソースを見てみましょう。

/**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
 final float x = event.getX();
 final float y = event.getY();
 final int viewFlags = mViewFlags;
 final int action = event.getAction();

 if ((viewFlags & ENABLED_MASK) == DISABLED) {
 if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
  setPressed(false);
 }
 // A disabled view that is clickable still consumes the touch
 // events, it just doesn't respond to them.
 return (((viewFlags & CLICKABLE) == CLICKABLE
  || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
  || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
 }

 if (mTouchDelegate != null) {
 if (mTouchDelegate.onTouchEvent(event)) {
  return true;
 }
 }

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
  (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
  (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
 switch (action) {
  case MotionEvent.ACTION_UP:
  boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
   // take focus if we don't have it already and we should in
   // touch mode.
   boolean focusTaken = false;
   if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
   focusTaken = requestFocus();
   }

   if (prepressed) {
   // The button is being released before we actually
   // showed it as pressed. Make it show the pressed
   // state now (before scheduling the click) to ensure
   // the user sees it.
   setPressed(true, x, y);
   }

   if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
   // 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) {
    mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
    performClick();
    }
   }
   }

   if (mUnsetPressedState == null) {
   mUnsetPressedState = new UnsetPressedState();
   }

   if (prepressed) {
   postDelayed(mUnsetPressedState,
    ViewConfiguration.getPressedStateDuration());
   } else if (!post(mUnsetPressedState)) {
   // If the post failed, unpress right now
   mUnsetPressedState.run();
   }

   removeTapCallback();
  }
  mIgnoreNextUpEvent = false;
  break;

  case MotionEvent.ACTION_DOWN:
  mHasPerformedLongPress = false;

  if (performButtonActionOnTouchDown(event)) {
   break;
  }

  // Walk up the hierarchy to determine if we're inside a scrolling container.
  boolean isInScrollingContainer = isInScrollingContainer();

  // For views inside a scrolling container, delay the pressed feedback for
  // a short period in case this is a scroll.
  if (isInScrollingContainer) {
   mPrivateFlags |= PFLAG_PREPRESSED;
   if (mPendingCheckForTap == null) {
   mPendingCheckForTap = new CheckForTap();
   }
   mPendingCheckForTap.x = event.getX();
   mPendingCheckForTap.y = event.getY();
   postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  } else {
   // Not inside a scrolling container, so show the feedback right away
   setPressed(true, x, y);
   checkForLongClick(0);
  }
  break;

  case MotionEvent.ACTION_CANCEL:
  setPressed(false);
  removeTapCallback();
  removeLongPressCallback();
  mInContextButtonPress = false;
  mHasPerformedLongPress = false;
  mIgnoreNextUpEvent = false;
  break;

  case MotionEvent.ACTION_MOVE:
  drawableHotspotChanged(x, y);

  // Be lenient about moving outside of buttons
  if (!pointInView(x, y, mTouchSlop)) {
   // Outside button
   removeTapCallback();
   if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
   // Remove any future long press/tap checks
   removeLongPressCallback();

   setPressed(false);
   }
  }
  break;
 }

 return true;
 }

 return false;
}
このソースコードはdispatch TouchEventのものよりも長いですが、同じように私達が重点を選んで見ます。onTouch()はこの文を見て大体分かりました。主にviewがクリックできるかどうかを判断します。クリックすれば続けて実行します。そうでなければ直接falseに戻ります。ifではどのタッチかをスイッチで判断しますが、最後はtrueに戻ります。
もう一つ注意してください。ACTION_。UPではif (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
方法が実行されます。

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;
}
上のperformClick() が見えます。そうです。また新しい発見があるようです。上記の経験によって、このコードは私達が設定したクリックイベントモニターに返信します。つまり私たちが普段使っているli.mOnClickListener.onClick(this); です。

/**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 *
 * @param l The callback that will run
 *
 * @see #setClickable(boolean)
 */
public void setOnClickListener(@Nullable OnClickListener l) {
 if (!isClickable()) {
 setClickable(true);
 }
 getListenerInfo().mOnClickListener = l;
}
上記の方法の設定がmListenerInfoのクリックモニターで確認できます。上記の予想を確認しました。ここにきてontouch事件の伝達メカニズムはほぼ分析され、一段落しました。
はい、これで最初の問題を解決できます。ついでにもうちょっとまとめます。dispatch TouchEventにOnTouch Listenerを設置して、Viewがenableであるなら、まずOnTouch Listenerのxxx.setOnClickListener(listener);が実行されます。ontouchがtrueに戻ると、dispatch TouchEventはもう次のように実行しなくて、trueに戻ります。そうでなければワンタッチイベントを実行します。ワンタッチイベントでViewがクリックできるなら、trueに戻ります。そうでなければfalseです。またワンタッチイベントでViewがクリックできるなら、現在のタッチイベントはACT IONです。UPは、onTouch(View v, MotionEvent event) を実行し、OClikListenerのオンClick方法をフィードバックします。
次は私が描いたスケッチです。

もう一つ注目すべき点は、今の事件がACT IONだとしたら。DOWNは、dispatch TouchEventだけがtrueに戻りました。このViewは次のACT ION_を受信します。MOVE、ACTION_UPイベントは、つまりイベントが消費されてからしか受信できないということです。
締め括りをつける
以上、Androidにおけるontouch事件の伝達メカニズムについて詳しく分析しました。Android開発者の皆さんの学習や仕事に役に立つと思います。