search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

Android View事件分發機制總結

這裡View不包括ViewGroup,它沒有子元素不需要向下傳遞事件,只能自己去處理事件,因此只有dispatchTouchEvent方法和onTouchEvent方法。源碼分析之前,咱們先來一個簡單例子。

咱們首先自定義一個Button叫MyButton,並且重寫其dispatchTouchEvent方法和onTouchEvent方法,代碼如下。

public class MyButton extends Button { public static final String TAG = MyButton.class.getSimpleName; public MyButton(Context context) { super(context); } public MyButton(Context context, AttributeSet attrs) { super(context, attrs); } public MyButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "dispatchTouchEvent---ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "dispatchTouchEvent---ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG, "dispatchTouchEvent---ACTION_UP"); break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "onTouchEvent---ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "onTouchEvent---ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG, "onTouchEvent---ACTION_UP"); break; } return super.onTouchEvent(event); } } 然後將MyButton添加至MainActivty布局文件中,代碼如下。

最後在MainActivity中給MyButton設置onTouchListener,代碼如下。

public class MainActivity extends AppCompatActivity { private MyButton btn_click; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_click = (MyButton) findViewById(R.id.btn_click); btn_click.setOnTouchListener(new View.OnTouchListener { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction) { case MotionEvent.ACTION_DOWN: Log.i(MyButton.TAG, "onTouch---ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(MyButton.TAG, "onTouch---ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(MyButton.TAG, "onTouch---ACTION_UP"); break; } return false; } }); } }

通過日誌可以總結一個規律:

不管DOWN,MOVE,UP都會按照這個順序執行:dispatchTouchEvent--->onTouch--->onTouchEvent。

咱們將上面onTouch的返回值由false改為true,重新運行程序,日誌列印如下:

我們發現MyButton的onTouchEvent不執行了。

接下來咱們帶著規律和疑問去源碼找找為什麼會這樣。

dispatchTouchEvent

/** * 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) { ... boolean result = false; ... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //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; } 直接看18行:首先會判斷

mOnTouchListener是否為空(即是否設置OnTouchListener),View是否enable,mOnTouchListener.onTouch(this, event)方法是否返回true,如果三個條件
都成立,直接返回true,那麼onTouchEvent方法不會被調用。
至此我們可以得出一個結論:OnTouchListener優先順序高於OnTouchEvent,這樣做的好處是方便在外界處理點擊事件。

/** * Implement this method to handle touch screen motion events. *

* 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: *

    *
  • obeying click sound preferences *
  • dispatching OnClickListener calls *
  • handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when * accessibility features are enabled *

* * @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, x, y); } 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; }

以上源碼大致可以分三個階段:
  • 24-33行:如果當前View處於不可點擊狀態,從源碼上看,很顯然View照樣會消耗點擊事件,儘管它看起來不可用。
  • 34-38行:如果View設置有代理,那麼還會執行TouchDelegate的onTouchEvent方法。
  • 40行起:對點擊事件的具體處理過程,只要View的CLICKABLE和LONG_CLICKABLE有一個為true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管它是不是DISABLE狀態。然後就是當ACTION_UP事件發生時,會觸發performClick方法,如果View設置了OnClickListener,那麼performClick方法內部會調用onClick方法。

performClick方法如下:

/** * Call this view's OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ 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; }

View的LONG_CLICKABLE默認為false,CLICKABLE是否為false和具體View有關,基本原則是:不可點擊的View的CLICKABLE默認為false,可點擊的默認為true。

注意:通過setOnClickListener會自動將CLICKABLE設為true,setOnLongClickListener會自動將LONG_CLIAKABLE設為true,同時也解答了為什麼好多同學說自己設置了setClickable無效,因為它在setOnClickListener之前設置了setCliakable(false),通過源碼也很好解釋,如下。

/** * 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; } /** * Register a callback to be invoked when this view is clicked and held. If this view is not * long clickable, it becomes long clickable. * * @param l The callback that will run * * @see #setLongClickable(boolean) */ public void setOnLongClickListener(@Nullable OnLongClickListener l) { if (!isLongClickable) { setLongClickable(true); } getListenerInfo.mOnLongClickListener = l; } 以上就是關於View事件分發的總結,。ViewGroup的事件分發總結正在書寫中,個人覺得View事件分發單純靠記憶是很難記住的,一定要理解,要跟著源碼耐心的走幾遍才能掌握好。水平有限,如有錯誤,歡迎大家指正。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦