(一)Android事件分发机制 - View篇
16lz
2021-01-23
本文适用于对Android事件分发机制有一定基础的开发者阅读,主要是通过对View类中的事件分发、事件消费方法的源代码进行解析以达到完全理解其原理的目的
- (一)Android事件分发机制 - View篇
- (二)Android事件分发机制 - ViewGroup篇
- (三)Android事件分发机制 - Activity篇
- (四)Android事件分发机制 - 总结篇
我们知道View中包含dispatchTouchEvent
和onTouchEvent
方法,接下来我们通过源代码(基于Android6.0)看看这些方法内部到底做了哪些事情。
1、View#dispatchTouchEvent源码解析
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; // 当前View是否可见(未被其他窗口遮盖住且未隐藏) if (onFilterTouchEventForSecurity(event)) { 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;}
从上面看出:
- 只有以下四个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent(event)方法
第一个条件:li! = null 第二个条件:li.mOnTouchListener != null; 第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED; 第四个条件:li.mOnTouchListener.onTouch(this, event);
- 下面我们来看下这四个判断条件:
第一个条件:li! = null
- li是ListenerInfo类对象,而ListenerInfo类作用是保存点击、长按点击、上下文点击等监听listener用以在需要的时候进行回调;
- 只要我们注册了监听事件如下文的mOnTouchListener,那么li就一定不为null。
第二个条件:li.mOnTouchListener != null
// mOnTouchListener是在View类的setOnTouchListener方法里赋值的public void setOnTouchListener(OnTouchListener l) { // 只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空) getListenerInfo().mOnTouchListener = l;}
第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED
- 该条件是判断当前点击的控件是否enable;
- 由于很多View默认是enable的,因此该条件恒定为true,当然我们也可以调用View#setEnabled()方法来改变此值。
第四个条件:mOnTouchListener.onTouch(this, event)
回调注册的Touch事件的onTouch方法:
- 如果返回true,就会让上述四个条件全部成立,从而返回true;
- 如果返回false,就会去执行onTouchEvent(event)方法。
onTouch和onTouchEvent的区别
-
onTouch
方法优先于onTouchEvent
方法执行; - 如果
onTouch
方法返回true
,onTouchEvent
将不会再执行。
2、View#onTouchEvent源码解析
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // 如果当前View是DISABLED状态且是可点击/可长按则会消费掉事件,不让它继续传递 if ((viewFlags & ENABLED_MASK) == DISABLED) { ... return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } // 如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true,如果大家希望自己的View增加它的touch范围,可以尝试使用TouchDelegate if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { // / 如果有TouchDelegate的话,优先交给它处理 return true; // 处理成功返回true,否则接着往下走 } } // 如果view可点击/可长按,则最终一定return true if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: // 抬起操作 ... break; case MotionEvent.ACTION_DOWN: // 按下操作 ... break; case MotionEvent.ACTION_CANCEL: // 取消操作 ... break; } return true; } return false;}
MotionEvent.ACTION_DOWN
switch (action) { ... case MotionEvent.ACTION_DOWN: // 设置mHasPerformedLongPress = false 表示长按事件还未触发; mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // 判断当前View是否在滑动控件里面 boolean isInScrollingContainer = isInScrollingContainer(); // 如果当前View在一个可滑动的父View中,我们触摸它时需要延迟一小段时间用于判断是否为屏幕滚动事件 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; // 给mPrivateFlags设置一个PREPRESSED的标识 if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 发送一个延迟为100ms的点击(非滑动)延迟消息,到达延时时间后会执行CheckForTap()里面的run方法 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; ...}
CheckForTap
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; // 取消mPrivateFlags的PREPRESSED setPressed(true, x, y); // 设置PRESSED标识为true,刷新背景 checkForLongClick(ViewConfiguration.getTapTimeout()); }}
checkForLongClick
private void checkForLongClick(int delayOffset) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { // 支持长按 mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); // 发送一个延迟消息,到达延迟时间后会执行CheckForLongPress()里面的run方法 postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }}
CheckForLongPress
private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; @Override public void run() { // 用户从DOWN触发开始算起,向消息队列插入的长按响应消息如果在延迟时间内没有被取消则触发长按 if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { // 根据长按的返回结果来设置mHasPerformedLongPress值 mHasPerformedLongPress = true; } } } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; }}
performLongClick
public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { // 长按返回结果为false - 不拦截事件 // ContextMenu是Android的context menu上下文菜单,比如EditeText就可以通过长按来弹出拥有“cut”,"copy","paste"等项的ContextMenu。 // 是否弹出context menu上下文菜单 - 是则拦截事件 handled = showContextMenu(); } if (handled) { // 如果弹出了context menu上下文菜单 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled;}
简单总结一下,ACTION_DOWN中都做什么:
- 将mHasPerformedLongPress置为false,mPrivateFlags置为PREPRESSED,而后发出一个延时为100ms的点击任务mPendingCheckForTap;
- 如果在100ms内没有触发ACTION_UP,则执行mPendingCheckForTap任务:清除PREPRESSED标志,并将mPrivateFlags设置为PRESSED,同时发出一个延时为 500ms - 100ms 的任务mPendingCheckForLongPress用于检测是否为长按;
- 如果在500ms内(从ACTION_DOWN触发开始)没有触发ACTION_UP,则认为是长按,此时执行mPendingCheckForLongPress任务:如果mOnLongClickListener不为null,则执行回调,同时当它的onLongClick方法返回true,才会把mHasPerformedLongPress设置为true。
MotionEvent.ACTION_MOVE
switch (action) { ... case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // 判断当然触摸点有没有移出我们的View if (!pointInView(x, y, mTouchSlop)) { // 移除点按定时任务 标志重置为0 removeTapCallback(); // 判断是否包含PRESSED标识,因为可能已经超过100ms,此时mPrivateFlags标志为PRESSED if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // 移除长按定时任务 removeLongPressCallback(); setPressed(false); } } break; ...}
-
如果触摸点在View外,则执行removeTapCallback
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; // 重置标志 // 移除消息队列中在ACTION_DOWN时插入的mPendingCheckForTap消息 removeCallbacks(mPendingCheckForTap); } }
-
如果标志为PRESSED,则执行removeLongPressCallback
private void removeLongPressCallback() { if (mPendingCheckForLongPress != null) { // 移除消息队列中在ACTION_DOWN时插入的mPendingCheckForLongPress消息 removeCallbacks(mPendingCheckForLongPress); } }
简单总结一下,ACTION_MOVE中都做了什么:
检测触摸是否划出了控件,如果划出了:
- 100ms内,直接移除mPendingCheckForTap;
- 100ms后,则将标志中的PRESSED去除,同时移除长按的检查mPendingCheckForLongPress。
MotionEvent.ACTION_UP
switch (action) { ... case MotionEvent.ACTION_UP: // 判断mPrivateFlags是否包含PREPRESSED boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; // 如果mPrivateFlags包含PRESSED或者PREPRESSED if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { setPressed(true, x, y); } // 没有执行长按操作 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除长按消息 removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // 执行点击 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; ... }
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;}
最后总会进入执行到UnsetPressedState
private final class UnsetPressedState implements Runnable { @Override public void run() { setPressed(false); // 取消mPrivateFlags中的PRESSED标志 }}
setPressed方法
public void setPressed(boolean pressed) { final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED); if (pressed) { mPrivateFlags |= PFLAG_PRESSED; } else { mPrivateFlags &= ~PFLAG_PRESSED; } if (needsRefresh) { refreshDrawableState(); } // 把setPress转发下去给所有的子View dispatchSetPressed(pressed);}
简单总结一下,ACTION_UP中都做了什么:
- 如果是在500ms内触发ACTION_UP,即长按还未发生,则首先移除长按检测,执行onClick回调;
- 如果是500ms以后,那么有两种情况:
1、设置了mOnLongClickListener,且mOnLongClickListener.onLongClick返回 mHasPerformedLongPress = true,则点击事件onClick事件无法触发;
2、未设置mOnLongClickListener或者mOnLongClickListener.onLongClick返回 mHasPerformedLongPress = false,则点击事件onClick事件可以触发。 - 最后执行mUnsetPressedState.run(),将setPressed传递下去,可以在View中复写dispatchSetPressed方法接收,然后将PRESSED标识去除。
setOnLongClickListener和setOnClickListener是否只能执行一个
不是的,只要setOnLongClickListener
中的onLongClick
返回false
,则两个都会执行;返回true
则会屏蔽setOnClickListener
。
我们总结一下:
- 整个View的事件分发的流程是
dispatchEvent -> mOnTouchListener.onTouch -> onTouchEvent -> mOnClickListener.onClick
,也就是说,我们平时调用的setOnClickListener事件,优先级是最低的; - 如果在回调
onTouch()
里返回true
,则不执行onTouchEvent()
方法,更不会执行mOnClickListener.onClick()
方法,也表示View
消费了Touch
事件,返回false
则继续执行onTouchEvent()
方法; - 一个
clickable
或者longClickable
的View
会永远消费Touch
事件,不管他是enabled
还是disabled
的; -
View
的长按事件是在onTouchEvent
的ACTION_DOWN
事件中执行,要想执行长按事件该View
必须是longClickable
的; -
View
的点击事件是在onTouchEvent
的ACTION_UP
中执行,想要执行点击事件的前提是消费了ACTION_DOWN
和ACTION_MOVE
,并且没有设置OnLongClickListener
,如设置了OnLongClickListener
,则必须使onLongClick()
方法返回false
。
结合下面这篇文章看更好理解哦:
Android触摸屏事件派发机制详解与源码分析一(View篇)
更多相关文章
- Android事件分发/传递机制总结
- Android事件分发机制 详解攻略
- Android 中三种使用线程的方法
- Android触控事件
- 防止事件导致的oncreate的多次调用
- Android Activity onConfigurationChanged()方法 监听状态改变
- Android高手进阶教程(二十)之---Android与JavaScript方法相互调
- android中的坐标系以及获取坐标的方法
- Android与JavaScript方法相互调用