Android 事件分发机制
与分发相关机制相关的3个方法 1、dispatchTouchEvent() 分发机制 2、onInterceptTouchEvent() 拦截机制 3、onTouchEvent 点击
从阅读源码中学习,先来分析View的事件分发,然后在探究ViewGroup的事件分发。
eg: 在只有一个Activity的简单的工程中,并且Activity中只有一个按钮,这个按钮注册了点击事件,需要调用:
在onClick方法里面实现,就可以在按钮被点击的时候执行。如果再给它添加一个touch事件,需要调用:
在onTouch方法里面可以做很多事情,比如手指的按下 down,抬起 up,移动 move 等事件。通过onClick,onTouch两个事件,运行程序打印日志可以看到,onTouch先于onClick执行。因此事件的传递顺序是先经过onTouch 再到onClick的。
仔细看会发现,onTouch是有返回值的,这里我们返回的是false,如果把返回值改为true,运行的结果是onClick将不执行。可以先理解为onTouch方法返回true就认为这个事件被onTouch消费掉了,因此不会再传递下去。
通过阅读源码,我们知道,只要你触摸到任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。当我们点击按钮的时候,就会调用Button类里的dispatchTouchEvent方法,可是在Button类里找不到这样的方法,接着往它的父类TextView里找,会发现也没有,接着往TextView的父类View找,在View里面找到了这个方法。
dispatchTouchEvent方法的源码:
首先是进行了一个判断,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。
先看第一个条件,mOnTouchListener变量在哪里赋值?在view中有
mOnTouchListener是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。
第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。
第三个条件比较关键,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。
再回到我们说传递顺序是先经过onTouch 再到onClick的。经过分析,dispatchTouchEvent中最先执行的就是onTouch方法,优先于onClick执行,如果onTouch方法返回true,就会让dispatchTouchEvent方法直接返回true,不再往下执行,onClick就不再执行了。
接下来我们分析onClick在onTouchEvent(event)方法中的调用。 onTouchEvent的源码:
**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; }**
从 if ((viewFlags & ENABLED_MASK) == DISABLED) { 中,如果控件可点击就会进入switch判断中去,如果当前事件是抬起手指,则会进入MotionEvent.ACTION_UP这个case当中。在经过这种判断后会执行
***public boolean performClick() { sendaccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }* 这个方法。
如果mOnClickListener 不为空,就会调用onClick方法,那mOnClickListener又在哪里赋值的?赋值方法如下: **public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; }**
通过分析,知道调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick方法。
从图中我们可以看的出来,View只有dispatchTouchEvent 和onTouchEvent两种机制。 ViewGroup有三种消费机制:
处理事件: 用户在点击View(View指Button,TextView等)后,
1、返回值是true时,自己消费,不往里面传。返回值是false时,传递给ViewGroup,返回值为false时,自己不消费,往下一级传递,为true时,自己消费。
2、activity启动时,点击事件分发dispatchTouchEvent ,传递给ViewGroup,返回值为true时调用onInterceptTouchEvent方法拦截,然后调用自己的onTouchEvent方法,不再往下传递。
归纳 事件分发过程由dispatchTouchEvent控制,返回值为false时往下传递,下方考虑是否onInterceptTouchEvent拦截,返回值为true时,调用onTouchEvent方法,自己消费,否则继续往下传递。
总价 事件分发可以比喻成,爷爷把好吃的给父亲,父亲再给儿子,如果父亲自己吃了,就是拦截了。 事件消费可以比喻成,儿子把好吃的吃了,自己消费了,不再给父亲,爷爷传递,儿子没有消费,又传递给父亲,父亲拦截自己消费。 事件分发是由外往内一级一级传递,事件消费是由内往外一级一级传递的。
新闻热点
疑难解答