Android 事件分發機制總結篇
一、前言
事件分發是基礎的知識點,開發中會經常遇到,平時開發只是為了解決問題,片面的理解,不夠深入,不成體系。這兩天有點時間,從 View,ViewGroup 到 Activity 體系的學習總結一下。
二、View 事件
我們知道view事件處理順序是(Down,Move,Up):
a、dispatchTouchEvent
b、 setOnTouchListener的onTouch
c、onTouchEvent
2.1、首先看下view的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
第二行if的判斷,mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就返回true,否則就去執行onTouchEvent(event)方法並返回。
第一個條件mOnTouchListener != null,根據view原始碼找到是在:
public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }
可以看出如果我們註冊了touch事件,mOnTouchListener 就不會為null。
第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點選的控制元件是否是enable的,按鈕預設都是enable的,因此這個條件恆定為true。
第三個條件mOnTouchListener.onTouch(this, event);這裡也就是註冊的touch事件返回值,如果返回ture,就會讓這三個條件全部成立,從而整個方法直接返回true,如果返回false,就會再去執行onTouchEvent(event)方法。
看下onTouchEvent(event)原始碼(部分程式碼):
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { // 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; if ((mPrivateFlags & 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 (!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) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } return true; } return false; }
程式碼有點多,不用慌,我們看重點,手指擡起是進入up這個case中,經過處理會進入performClick()方法中:
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; } 如果mOnClickListener !=null,就會執行onClick,到這裡可以看到在dispatchTouchEvent中最先執行的就是onTouch方法,因此onTouch肯定是要優先於onClick執行的,而如果在onTouch方法裡返回了true,就會讓dispatchTouchEvent方法直接返回true,不會再繼續往下執行onClick就不會再執行了。
三、ViewGroup 事件
先寫一個demo:
TouchEventFather 繼承自 LinearLayout
public class TouchEventFather extends LinearLayout { public TouchEventFather(Context context) { super(context); } public TouchEventFather(Context context, AttributeSet attrs) { super(context, attrs); } public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("TouchEventFather", "dispatchTouchEvent: " + TouchEventUtil.getTouchAction(ev.getAction())); return super.dispatchTouchEvent(ev); } public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("TouchEventFather", " onInterceptTouchEvent:" + TouchEventUtil.getTouchAction(ev.getAction())); return super.onInterceptTouchEvent(ev); } public boolean onTouchEvent(MotionEvent ev) { Log.e("TouchEventFather", "onTouchEvent :" + TouchEventUtil.getTouchAction(ev.getAction())); return super.onTouchEvent(ev); } }
MyButton 繼承自 Button :
public class MyButton extends Button { private static final String TAG = "MyButton"; public MyButton(Context context) { super(context); } public MyButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e(TAG, "onTouchEvent: " + TouchEventUtil.getTouchAction(event.getAction())); return super.onTouchEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.e(TAG, "dispatchTouchEvent: " + TouchEventUtil.getTouchAction(event.getAction())); return super.dispatchTouchEvent(event); } }
在xml中的:
<com.example.csv.testdemo.view.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#468AD7" android:gravity="center" android:orientation="vertical"> <com.example.csv.testdemo.view.MyButton android:id="@+id/touch_my_button" android:layout_width="60dp" android:layout_height="30dp" android:background="#000000" /> </com.example.csv.testdemo.view.TouchEventFather>
在activity中(部分程式碼):
private void initView() { btn = (Button) findViewById(R.id.touch_my_button); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e(TAG, "MyButton|onTouch: " + TouchEventUtil.getTouchAction(ev.getAction())); return false; } }); } //重寫dispatchTouchEvent @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e(TAG, " dispatchTouchEvent: " + TouchEventUtil.getTouchAction(ev.getAction())); return super.dispatchTouchEvent(ev); } //重寫onTouchEvent @Override public boolean onTouchEvent(MotionEvent event) { Log.e(TAG, " onTouchEvent:" + TouchEventUtil.getTouchAction(event.getAction())); return super.onTouchEvent(event); }
現在我們點選MyButton 控制元件,打印出的日誌是這樣:
從日誌可以看出,事件大致分發流程為:TouchActivity的dispatchTouchEvent ->TouchEventFather的dispatchTouchEvent-> TouchEventFather的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent ->Mybutton的onTouchEvent 。
在TouchEventFather控制元件中出現一個onInterceptTouchEvent方法, 看下原始碼:
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
只有三行程式碼,預設返回flse,表示不攔截事件,ture表示攔截事件,不在繼續向下傳遞,事件在當前控制元件處消費處理。
再來看下dispatchTouchEvent(MotionEvent ev)原始碼(部分程式碼):
public boolean dispatchTouchEvent(MotionEvent ev) { if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { mMotionTarget = null; } if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; if (child.dispatchTouchEvent(ev)) { mMotionTarget = child; return true; } } } } } } }
在第6行可以看到一個條件判斷,如果disallowIntercept和!onInterceptTouchEvent(ev)其中有一個為true,就會進入到這個條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能,預設是false,也可以通過呼叫requestDisallowInterceptTouchEvent方法對這個值進行修改。那麼當第一個值為false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的內部,也就是onInterceptTouchEvent方法的返回值取反!在我們onInterceptTouchEvent方法中返回false,就會讓第二個值為true,從而進入到條件判斷的內部,如果我們在onInterceptTouchEvent方法中返回true,就會讓第二個值為false,從而跳出了這個條件判斷。
如果條件成立進入到第十二行,會遍歷viewGroup中包涵的所有子view,接下來判斷當前view是不是正在點選的view,如果是繼續呼叫子view的dispatchTouchEvent方法,我們知道控制元件如果可點選dispatchTouchEvent返回值為true,那麼 if (child.dispatchTouchEvent(ev)) 條件成立,當前viewGroup的dispatchTouchEvent方法返回true,所以viewGroup後面的方法也不在執行(上圖日誌也得以驗證,TouchEventFather的onTouchEvent沒有執行 )。
四、總結
4.1、view事件執行順序dispatchTouchEvent-> setOnTouchListener的onTouch->onTouchEvent,如果setOnTouchListener返回ture,後續事件onTouchEvent不在執行
4.2、onClick是在onTouchEvent(event)方法中的,所以onTouch優先於onClick執行
4.3、Android 點選事件執行順序是Activity—>ViewGroup—>View
4.4、如果子View將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件
4.5、在ViewGroup中onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,把事件交給自己處理,則會執行自己對應的onTouchEvent方法。返回false代表不對事件進行攔截,事件繼續向下傳遞,預設返回false(也可以通過呼叫requestDisallowInterceptTouchEvent方法對這個值進行修改)
4.6、如果重寫dispatchTouchEvent方法,dispatchTouchEvent無論返回true還是false,事件都不再進行分發, 只有當其返回super.dispatchTouchEvent(ev),才表明其具有向下層分發的願望。