*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
所以我们要开始讲解事件分发机制了,说到事件分发机制,这个知识点主要是在自定义view的时候用到,那么什么是事件分发机制呢。
这里我用大白话概述一下:我们在自定义view,或者在使用某个控件,当给这个view或者控件设置事件的时候,比如有setOnTouchListener
、setOnClickListener
这些方法的时候,这些方法总有一个执行顺序吧,事件分发机制主要就是了解这些方法执行的先后顺序,或者说执行这些事件的顺序和方法之间的关系,比如点击事件,触摸事件,手指上抬下按等等之类的,主要就是要搞清楚这些事件发生的先后顺序和他们之间的关系。
搞清楚这些东西有什么好处呢,首先,即便假设可能由于这些方法名字太像了,所以你还是没有搞清楚这些方法的执行顺序和相互关系,不过在搞清楚的过程中,至少也搞清楚每个方法,就搞清楚这些方法是干嘛的了吧,知道这些方法是干嘛的又能有什么好处呢,至少不仅仅就只会一个setOnClickListener
点击事件了吧,如果你搞清楚了setOnTouchListener
方法,也许你就可以实现一个view,手指点击按住后拖动,手指放开后,view又回到原来的位置,这种效果,可不是一个简单的setOnClickListener
就能够实现的。
如果以上那么大一段文字引起了你阅读这篇文章的兴趣,那你就继续吧。
(p.s. 以下代码追踪基于Android 8.0的源码,即API 26)
不知道有哪些方法跟触摸点击有关啊,那就看源码吧!从我们最熟知的setOnClickListener
开始,setOnClickListener
做了啥,跳进View.class里面,发现这个方法长这样:
// View.class
public void setOnClickListener(@Nullable OnClickListener l) {
...
getListenerInfo().mOnClickListener = l;
}
反正就是赋值,给这个接口赋值,所以我们来看看mOnClickListener
在什么地方使用的,代码一顿追踪,mOnClickListener
在这里performClick()
被使用了:
// View.class
public boolean performClick() {
...
li.mOnClickListener.onClick(this);
...
}
先不管这里面具体的一个实现流程是啥,知道mOnClickListener
在这里被用到就行了,看这个方法名:performClick
翻译过来“执行点击”,嗯~靠谱,继续追踪,于是来到了:
// View.class
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_UP:
...
performClick();
...
break;
...
}
...
}
看到这里,我们需要稍微总结一下了。为什么要在这里总结呢,因为不一样了啊,哪里不一样了。首先,前面两个方法setOnClickListener
和performClick
,概念单一,就一个设置具体实现类接口,还有一个执行点击事件嘛,但是onTouchEvent
好像跟以上两种不太一样,因为他有个参数MotionEvent
,翻译过来运动事件或者手势事件,这个事件包含了我们很多手势操作,比如手指上抬下按,在屏幕上拖动等等。所以完整的onTouchEvent
方法是如下形状:
// View.class
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
// 手指上抬
case MotionEvent.ACTION_UP:
...
performClick();
...
break;
// 手指下按
case MotionEvent.ACTION_DOWN:
...
break;
// 手指取消
case MotionEvent.ACTION_CANCEL:
...
break;
// 手指滑动
case MotionEvent.ACTION_MOVE:
...
break;
}
...
}
所以我们可以很清晰的看到,点击事件只是在手指抬起的这个行为后执行的,只是用到了这么多操作行为中的其中一个而已。那么我们是不是就可以得出一个结论,performClick()
方法其实只有在手指上抬的时候执行,也就是当手指接触屏幕到离开屏幕的这个过程中,performClick()
只执行了一次,而onTouchEvent
可能就执行了多次,至少2次吧,即上抬和下按。
接下来我们开始追踪onTouchEvent
方法,然后发现了以下源码。
// View.class
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
...
}
看到此次,有人提问 (问我= =)!为啥这里的源码不写成:
// View.class
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (!result && onTouchEvent(event)) {
result = true;
}
...
}
因为有个onTouch()
方法,待会要讲,主要是由于view有个setOnTouchListener()
方法,先不管这些,我们只看上面那个简单的dispatchTouchEvent()
方法,这个方法翻译成中文“分发触摸事件”,好像有点谱了,事件分发机制的源头可能就在此处吧。先不管,继续跟踪,我们来到:
// View.class
public final boolean dispatchPointerEvent(MotionEvent event) {
...
return dispatchTouchEvent(event);
...
}
继续跟踪:
// ViewRootImpl.class
private int processPointerEvent(QueuedInputEvent q) {
...
boolean handled = mView.dispatchPointerEvent(event);
...
}
妈耶!都跑到私有方法去了,不管,继续跟踪:
// ViewRootImpl.class
final class ViewPostImeInputStage extends InputStage {
...
@Override
protected int onProcess(QueuedInputEvent q) {
...
return processPointerEvent(q);
...
}
...
}
不管,反正也不知道这个类是干嘛的,继续跟,跟源码死磕到底!
// ViewRootImpl.class
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
...
}
WTF?都跑到setView
设置view那里去了,算了算了,弃坑重练,还是定位到靠谱的dispatchTouchEvent()
方法就终止追踪了吧。有朋友可能会问,到这里为啥就不跟了,你说呢,都setView
了,这之前的代码连view都没有了,哪还来的事件。
现在我们来总结一下有哪些方法,从上到下的顺一遍,
dispatchTouchEvent()
onTouch()
onTouchEvent()
setOnClickListener
根据源码,我们的猜想是按照这样一个顺序,那我们来验证一下,首先写一个类,并且让这个类实现那些事件方法,所以这个类,基本上就长这样了。
public class EventView extends View {
private static final String TAG = "EventView";
public EventView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: ");
return false;
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: ");
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(event);
}
}
鉴于已经知道触摸行为常见的有3种,按下移动抬起,为了使日志最短,所以我们飞快的点击了屏幕,不给手指在屏幕上的移动的机会,得到了如下日志:
dispatchTouchEvent:
onTouch:
onTouchEvent:
dispatchTouchEvent:
onTouch:
onTouchEvent:
onClick:
为了看的更加清楚,我们改改源码,打印出具体的行为:
public class EventView extends View {
private static final String TAG = "EventView";
public EventView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: action = " + event.getAction());
return false;
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: ");
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: action = " + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent: action = " + event.getAction());
return super.dispatchTouchEvent(event);
}
}
日志:
dispatchTouchEvent: action = 0
onTouch: action = 0
onTouchEvent: action = 0
dispatchTouchEvent: action = 1
onTouch: action = 1
onTouchEvent: action = 1
onClick:
所以这里面的0和1都是啥意思,还有,这些方法长的太像了,我已经蒙圈了,只认识onClick
了。
先看看0和1都是啥意思,看源码呗,不是都说源码是最好的老师吗。
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
好了,知道了,手指按下就是0,手指上抬就是1。上面的日志就被改成下面这副模样:
dispatchTouchEvent: action = 手指下按
onTouch: action = 手指下按
onTouchEvent: action = 手指下按
dispatchTouchEvent: action = 手指上抬
onTouch: action = 手指上抬
onTouchEvent: action = 手指上抬
onClick:
然后我们根据方法的名字,将方法名字翻译成中文,上面的日志又被改成以下模样:
分发触摸事件: action = 手指下按
触摸: action = 手指下按
触摸事件: action = 手指下按
分发触摸事件: action = 手指上抬
触摸: action = 手指上抬
触摸事件: action = 手指上抬
onClick:
大家感受一下这个顺序,给你10秒钟。
接下来我们进入下一个环节。
首先我们先来到最初的dispatchTouchEvent
方法中去寻觅过程。
(当前源码API 26;不用看这些源码,我就摆摆场面= =)
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)) {
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;
}
}
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;
}
某些读者表示,这些源码又多又乱,我在看一篇博客,我要怎么看这些源码,里面的变量是啥意思都不知道,还不能进行变量跟踪,我要怎么看,如果是在IDE里面打开这些源码,兴许我还有几分愿意阅读的兴趣。
以上问题就是我平时看博客的时候脑子里面想到的事情,最讨厌贴上一片源码,然后就开始讲道理了,源码看都看不懂,或者说不想看= =
教大家一个小技巧,看老版本的源码,因为Android源码只会越来越多啊,所以老版本的源码肯定比新版本的少。
目前我这里找到的最老的源码只有API 15 的,所以我们来看看API 15里面的事件分发是怎么写的吧,同一个方法dispatchTouchEvent
:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
是不是觉得少了很多,不过还是挺多的,那我们怎么看呢,就看这里面出现的关键点,源码少了很多,我们就能快速定位我们的关键点在什么地方了。
首先这个方法里面出现了两个很重要的地方,onTouch
和onTouchEvent
方法,所以把跟这些代码无关的地方,我们就都给筛掉,所以就变成了以下模样:
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
...
}
好像基本就这样了,也不能再怎么筛了,所以趁着源码才这几行的机会,我们好好来看一下,首先是ListenerInfo
类,这个类是干啥的,看名字好像是接口监听信息,瞅瞅源码:
static class ListenerInfo {
protected OnFocusChangeListener mOnFocusChangeListener;
private ArrayList mOnLayoutChangeListeners;
private CopyOnWriteArrayList mOnAttachStateChangeListeners;
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
}
果然基本所有的接口都在这里面,当接口很多的时候,用这种方式统一管理接口,真是个不错的方法,学到了,果然看源码还是有很多好处的嘛。
好的,我们继续来看dispatchTouchEvent
方法(复制了一遍,免得往上翻)
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
...
}
ListenerInfo
我们已经知道是怎么回事了,就来看看第一个if,因为第一个if就包含我们其中一个关注点onTouch
,这个if条件还挺多的,一共有4个条件,我们一个一个看:
我想,这个一个不用我说了吧,就判断这个接口管理类是否为null
判断这个接口是否为null,我们来看看这个值是在哪里赋值的:
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
仿佛看到常见方法了,或者关键方法了,这个方法是view的。
说到这里,我要夸夸Google的程序员,确实厉害(Google程序员:还用你夸?)
这个&用的很传神,在API 26 的View.class
里面有一群这样的注释:
/**
* Masks for mPrivateFlags2, as generated by dumpFlags():
*
* |-------|-------|-------|-------|
* 1 PFLAG2_DRAG_CAN_ACCEPT
* 1 PFLAG2_DRAG_HOVERED
* 11 PFLAG2_LAYOUT_DIRECTION_MASK
* 1 PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL
* 1 PFLAG2_LAYOUT_DIRECTION_RESOLVED
* 11 PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK
* 1 PFLAG2_TEXT_DIRECTION_FLAGS[1]
* 1 PFLAG2_TEXT_DIRECTION_FLAGS[2]
* 11 PFLAG2_TEXT_DIRECTION_FLAGS[3]
* 1 PFLAG2_TEXT_DIRECTION_FLAGS[4]
* 1 1 PFLAG2_TEXT_DIRECTION_FLAGS[5]
* 11 PFLAG2_TEXT_DIRECTION_FLAGS[6]
* 111 PFLAG2_TEXT_DIRECTION_FLAGS[7]
* 111 PFLAG2_TEXT_DIRECTION_MASK
* 1 PFLAG2_TEXT_DIRECTION_RESOLVED
* 1 PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT
* 111 PFLAG2_TEXT_DIRECTION_RESOLVED_MASK
* 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[1]
* 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[2]
* 11 PFLAG2_TEXT_ALIGNMENT_FLAGS[3]
* 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[4]
* 1 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[5]
* 11 PFLAG2_TEXT_ALIGNMENT_FLAGS[6]
* 111 PFLAG2_TEXT_ALIGNMENT_MASK
* 1 PFLAG2_TEXT_ALIGNMENT_RESOLVED
* 1 PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT
* 111 PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK
* 111 PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK
* 11 PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK
* 1 PFLAG2_ACCESSIBILITY_FOCUSED
* 1 PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED
* 1 PFLAG2_VIEW_QUICK_REJECTED
* 1 PFLAG2_PADDING_RESOLVED
* 1 PFLAG2_DRAWABLE_RESOLVED
* 1 PFLAG2_HAS_TRANSIENT_STATE
* |-------|-------|-------|-------|
*/
与(&),一个符号巧妙的搞定了判断两个值是否等于相同,好了不吹了,偏题了= =
总之这个判断大概就是判断该View是否可用。
这里,重点环节,回调了onTouch
方法,我们就可用在事件onTouchEvent
事件执行之前,先一步窥探有什么事件,甚至拦截接下来的事件。
为什么要先一步呢,因为我们在自定义view的时候,可以很方便的重写onTouchEvent
方法,但是如果使用的是系统控件,就不能那么方便的得到这些事件了,如果这时候可以巧妙的使用setOnTouchListener
,那么就能先一步得到这些事件了。
第一个if的条件分析完了,为了避免再次你们继续翻上去看那个方法,无形增加后摇时间,所以我重新复制一下:
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
...
}
第一个if我们只看了条件,内容就一个return true,我们来看看第二个if,条件居然直接就是onTouchEvent
方法的返回值,内容体也是return true。
那我们就根据这点代码来总结一下吧!
不过还是有不必要的代码,我再简化一下吧:
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
...
}
这样看的是不是就足够清楚了,代码就是这样,剔除了那些非核心代码后,核心代码其实就短短几句。
第一个if,我们可以看到,其实这里的onTouch
方法是我们手动实现的,使用setOnTouchListener
,就可以在这个设置的接口里面具体实现onTouch
的内容了。
然后由于我们还可以把控onTouch
的返回值,如果我们将onTouch
的返回值设为true,那么第一个if就结束了,dispatchTouchEvent
也就直接结束了,那么第二个if就不会执行了,相当于我们可以通过onTouch
的返回值,直接拦截view自己实现的onTouchEvent
方法。假设有个自定义的DragView,可以想拖哪就拖到哪,如果给这个类setOnTouchListener
,那么这个控件的拖动方式就全凭你管了啊,想想都刺激。
所以使用setOnTouchListener
可以拦截onTouchEvent
方法,默默记住这个知识点。
然后我们接着看第二个if,好像onTouchEvent
也可以拦截这个if下面的代码哈,然后其他的就没啥了,反正这个if下面又没有什么触摸事件了,这里这个onTouchEvent
的返回值是true是false应该都没啥关系了吧,如果你这样想,那么你就错了。别忘了,onTouchEvent
可再也不是我们实现的了,这是系统实现的,那还不赶紧进来看看,里面长啥样。
老规矩,把API 15的源码搬上来:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
// 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 (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.
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
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) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
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 |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
mPrivateFlags |= PRESSED;
refreshDrawableState();
checkForLongClick(0);
}
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
if (!pointInView(x, y, mTouchSlop)) {
// 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;
}
好了好了,我知道你们直接跳过来了,知道你们不会看,所以我准备了一份终极简化版:
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
performClick();
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
是不是看着眼睛干净多了,去掉的大概都是一些什么view是否可用,能不能点击之类,各种对象的处理和判断啦,大概就这些东西,总之不影响我们研究核心的内容。
可以看到onTouchEvent
的返回值直接就是true,也就可以认为是事件分发的终点了。那么我们来看看这个方法里面做了什么事,首先switch
区分了触摸事件的类型,上抬下按什么的,然后我们发现手指上抬的时候,执行了一个performClick
,关于onTouchEvent
方法,其他就没什么好说的了。既然如此,我们就来大概看看performClick
里面是些什么东西了,不过我想你们应该猜到了。
直接上终极简化版吧,一目了然的感觉真好。
public boolean performClick() {
li.mOnClickListener.onClick(this);
return true;
}
没错,就是回调了setOnClickListener
里面设置的接口。
讲到这里,那view的事件分发基本就算讲完了,顺便一提,关于
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
...
}
现在我们也就知道了,如果onTouchEvent返回false,会影响的就是点击事件了,也就是说,如果我们在重写onTouchEvent的时候,如果返回值是false,那么就没有点击事件了,不过你要把点击事件设置到手指刚刚触碰到屏幕的那一刻也行。
现在我们已经看完了view的整个事件分发的流程源码,重要方法也差不多了解了,那么现在我们来归纳总结一下。
大概总结一下就是:
dispatchTouchEvent
这里,这个方法主要是将触摸事件传给onTouch
和onTouchEvent
。setOnTouchListener
来自主实现onTouch
里面的内容。onTouch
方法的返回值,我们可以决定是否拦截系统实现的onTouchEvent
方法。onTouchEvent
方法里面的触摸行为分为很多种,比如手指下按上抬什么的,当手指上抬的时候,onTouchEvent
里面会执行点击操作。onTouchEvent
时,如果onTouchEvent
的返回值设为false,将不会执行点击操作,不过既然都在重写onTouchEvent
了,内部你要怎么实现你的点击事件都可以= =所以,View的事件分发可以这样说,主要方法:
dispatchTouchEvent
onTouch
onTouchEvent
onClick
顺序就是这样一个顺序,上面的方法可以拦截下面的方法,这里的拦截是指不让下面的方法运行。不过我们主要了解的还是后面3个方法。
说完了view的事件后,我们来谈谈ViewGroup的触摸事件,ViewGroup的触摸事件跟View的触摸事件大体上都差不多,只是有一个地方不一样,举个例子?
假设我们现在写了这样两个类:
public class TouchViewGroup extends LinearLayout {
private static final String TAG = "TouchViewGroup";
public TouchViewGroup(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: " + event.getAction());
return false;
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: ");
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent: " + ev.getAction());
return super.dispatchTouchEvent(ev);
}
}
public class TouchView extends View {
private static final String TAG = "TouchView";
public TouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: " + event.getAction());
return false;
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: ");
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
}
两个类,一个是ViewGroup,一个是View,就打印下日志,其他啥也没有了。将这个View放进ViewGroup中,我们运算一下试试。打印结果是:
TouchViewGroup: dispatchTouchEvent: 0
TouchView: onTouch: 0
TouchView: onTouchEvent: 0
TouchViewGroup: dispatchTouchEvent: 1
TouchView: onTouch: 1
TouchView: onTouchEvent: 1
TouchView: onClick:
有疑问吗?️
View倒是没有啥问题,但是这个ViewGroup就。。。
为啥ViewGroup没有调用onTouch
、onTouchEvent
、onClick
这三个方法,根据view的事件分发机制,我们可以猜测肯定是ViewGroup里面某个方法把onTouch
、onTouchEvent
、onClick
这三个方法给拦截了,既然ViewGroup的dispatchTouchEvent
打印出来了,其他的方法却没有打印出来,肯定是dispatchTouchEvent
里面做了什么有拦截性质的操作,让我们在源码较少的API 15里面去寻找答案。
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Handle an initial down.
...
// Check for interception.
...
// Check for cancelation.
...
// Update list of touch targets for pointer down, if needed.
...
}
看源码也不知道是哪里,算了看注释吧,突然发现注释里面有一个注释是Check for interception
,拦截检查?听名字靠谱!认真瞧瞧:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
}
代码还是有多又看不懂,不过这个intercepted
变量肯定是关键,然后看到了在哪里赋值后,这个方法在我眼中已经变成如下模样了:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
intercepted = onInterceptTouchEvent(ev);
...
}
onInterceptTouchEvent
这个方法翻译过来“拦截触摸事件”,还有返回值?哇,跟View的那些触摸事件很像啊,这个返回值肯定就是控制拦截的,不管三七二十一,我们先看看这个方法的源码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
是我眼花了吗,还能有这么简单的源码?就返回一个false,根据我们对View的了解,这里返回false,应该是没有拦截才对啊,等等!我们先做个实验。在自定义的ViewGroup里面,重写onInterceptTouchEvent
方法,直接返回true,看看效果,用实践出真理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
然后运行一下再看看:
TouchViewGroup: dispatchTouchEvent: 0
TouchViewGroup: onInterceptTouchEvent: 0
TouchViewGroup: onTouch: 0
TouchViewGroup: onTouchEvent: 0
TouchViewGroup: dispatchTouchEvent: 1
TouchViewGroup: onTouch: 1
TouchViewGroup: onTouchEvent: 1
TouchViewGroup: onClick:
哇,全是ViewGroup的东西,原来onInterceptTouchEvent
拦截的是View里面的触摸事件啊!
所以这里的开关就是这个onInterceptTouchEvent
的返回值,如果是false就走View的事件,如果是true,就走ViewGroup的事件。
既然View跟ViewGroup的事件分发机制都摸清楚了,那么我们就来总结一下吧!
首先说说View的事件分发机制,虽然前面已经总结过一次了,不过在这里再总结一次。
dispatchTouchEvent(MotionEvent ev)
负责处理MotionEvent这些触摸事件,然后按照顺序,这里有3个方法:
onTouch setOnTouchListener(xxx)
onTouchEvent 系统实现,或者自定义view的时候,自己实现
onClick setOnClickListener(xxx)
用动画的方式看怎么样?
图1
正常情况下,程序就跟着这个顺序执行下去了:
图2
我们可以使用setOnTouchListener
来实现onTouch
,然后可以通过控制onTouch
的返回值,来决定是否继续执行下面的两个方法,返回值为true,则不继续执行,为false,则继续执行。像这样?
图3
为false,我就不做图了,跟图2类似。
这里面三个方法,前面执行的方法有决策权,可以决定是否执行他之后的方法,返回值为true,则不执行之后的方法,为false则执行之后的方法。
老实说,ViewGroup的事件分发机制跟View基本一样,毕竟ViewGroup继承View嘛。跟事件有关的那几个方法也是一样的,都是:
onTouch
onTouchEvent
onClick
不过如果执行了ViewGroup默认执行View的这三个方法,不会执行ViewGroup的这三个方法,如果想要执行ViewGroup的这三个方法,我们必须修改ViewGroup的onInterceptTouchEvent方法的返回值,为true则可以执行ViewGroup的触摸事件,为false则执行View的触摸事件。
关于Android View的事件分发机制大概就是这些,如果想要了解更多有关事件分发的东西,还是推荐看Android源码,遇到不清楚的地方,大家可以在网上找找相关博客,应该就能解惑了。