Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 詳細分析Android中onTouch事件傳遞機制

詳細分析Android中onTouch事件傳遞機制

編輯:關於Android編程

onTach介紹

ontach是Android系統中整個事件機制的基礎。Android中的其他事件,如onClick、onLongClick等都是以onTach為基礎的。

onTach包括從手指按下到離開手機屏幕的整個過程,在微觀形式上,具體表現為action_down、action_move和action_up等過程。

onTach兩種主要定義形式如下:

1.在自定義控件中,常見的有重寫onTouchEvent(MotionEvent ev)方法。如在開發中經常可以看到重寫的onTouchEvent方法,

並且其中有針對不同的微觀表現(action_down、action_move和action_up等)做出的相應判斷,執行邏輯並可能返回不同的布爾值。

2.在代碼中,直接對現有控件設置setOnTouchListener監聽器。並重寫監聽器的onTouch方法。onTouch回調函數中有view和MotionEvent

onTouch事件傳遞機制

大家都知道一般我們使用的UI控件都是繼承自共同的父類——View。所以View這個類應該掌管著onTouch事件的相關處理。那就讓我們去看看:在View中尋找Touch相關的方法,其中一個很容易地引起了我們的注意: dispatchTouchEvent(MotionEvent event)

根據方法名的意思應該是負責分發觸摸事件的,下面給出了源碼:

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

源碼有點長,但我們不必每一行都看。首先注意到dispatchTouchEvent的返回值是boolean類型的,注釋上的解釋:@return True if the event was handled by the view, false otherwise.也就是說如果該觸摸事件被這個View消費了就返回true,否則返回false。在方法中首先判斷了該event是否是否得到了焦點,如果沒有得到焦點直接返回false。然後讓我們把目光轉向if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event))這個片段,看到這裡有一個名為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;
}

正是通過上面的方法來設置 mOnTouchListener 的,我想上面的方法大家肯定都很熟悉吧,正是我們平時經常用的 xxx.setOnTouchListener ,好了我們從中得知如果設置了OnTouchListener則第二個判斷也為true,第三個判斷為如果該View是否為enable,默認都是enable的,所以同樣為true。還剩最後一個:li.mOnTouchListener.onTouch(this, event) ,顯然是回調了第二個判斷中監聽器的onTouch()方法,如果onTouch()方法返回true,則上面四個判斷全部為true,dispatchTouchEvent()方法會返回true,並且不會執行if (!result && onTouchEvent(event))這個判斷;而在這個判斷中我們又看到了一個熟悉的方法:onTouchEvent() 。所以想要執行onTouchEvent,則在上面的四個判斷中必須至少有一個false。

那就假定我們在onTouch()方法中返回的是false,這樣就順利地執行了onTouchEvent,那就看看onTouchEvent的源碼吧:

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

這段源碼比 dispatchTouchEvent 的還要長,不過同樣我們挑重點的看:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
看到這句話就大概知道了主要是判斷該view是否是可點擊的,如果可以點擊則接著執行,否則直接返回false。可以看到if裡面用switch來判斷是哪種觸摸事件,但在最後都是返回true的。

還有一點要注意:在 ACTION_UP 中會執行 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;
}

可以看到上面的li.mOnClickListener.onClick(this); ,沒錯,我們好像又有了新的發現。根據上面的經驗,這句代碼會去回調我們設置好的點擊事件監聽器。也就是我們平常用的xxx.setOnClickListener(listener);

/**
 * 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事件的傳遞機制基本已經分析完成了,也算是告一段落了。

好了,這下我們可以解決開頭的問題了,順便我們再來小結一下:在dispatchTouchEvent中,如果設置了OnTouchListener並且View是enable的,那麼首先被執行的是OnTouchListener中的onTouch(View v, MotionEvent event) 。若onTouch返回true,則dispatchTouchEvent不再往下執行並且返回true;不然會執行onTouchEvent,在onTouchEvent中若View是可點擊的,則返回true,不然為false。還有在onTouchEvent中若View是可點擊以及當前觸摸事件為ACTION_UP,會執行performClick() ,回調OnClickListener的onClick方法。

下面是我畫的一張草圖:

還有一點值得注意的地方是:假如當前事件是ACTION_DOWN,只有dispatchTouchEvent返回true了之後該View才會接收到接下來的ACTION_MOVE,ACTION_UP事件,也就是說只有事件被消費了才能接收接下來的事件。

總結

以上就是關於Android中onTouch事件傳遞機制的詳細分析,希望對各位Android開發者們的學習或者工作能有一定的幫助,如果有疑問大家可以留言交流。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved