说明:终于写到了事件分发机制的最后一篇,如果还没看过Android学习笔记之事件分发机制(一)和Android学习笔记之事件分发机制(二)的话可以先看看,再结合源码会有助于理解。
前言
第一篇主要讲了dispatchTouchEvent
、onTouch
、onTouchEvent
和onClick
之间的关系。
第二篇主要讲了事件的分发路径: Activity -> ViewGroup -> View。
这两篇都还有一些东西讲得不是很清楚,所以这篇会顺带把之前一些难以理解的地方给讲明白。
源码版本
Android 22
其他版本的源码可能会有一些不同,但大概的思路都是一样的。
说明:为了节省篇幅和复杂性,源码我只提取了其中有用到的。具体的源码请大家自己查看。
主线一
首先,我们先来看这一条主线:dispatchTouchEvent
、onTouch
、onTouchEvent
和onClick
之间的关系。
找到View.java中的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)) {
//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;
}
先来看下注释:
将点击屏幕事件分发到指定的View。
当事件被当前的View处理(消费)时返回true,否则返回false。
然后跳到第13行。if中有四个判断条件。第一个和第二个我们可以直接认为是true了,因为onTouch
能被执行也就意味着前两个条件为true,不用去追踪源码了。看一下第三个条件吧,这里的意思是判断当前的View是否是Enable的,Button默认是Enable的,所以第三个条件也为true。也就是说,onTouch
决定了result的值。假设onTouch
返回了true,result的值变为true。来到19行,第一个条件为false,所以直接跳出判断,后面的onTouchEvent
是不会被执行的。
所以第一个结论来了,当前View为Enable的前提下,只有当onTouch
返回false时,onTouchEvent
才会被执行。
假设我们将onTouch
返回false,再进入onTouchEvent
中探个究竟。有点长,所以只挑重点来讲。
public boolean onTouchEvent(MotionEvent event) {
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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) {
// 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:
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}
先看2-10行。如果当前View是被disable但仍然可以点击的,返回true,即当前View消费掉此次事件,但没有对它们做出反应。
从12行开始了一个很长的if块,一直到74行(中间省略了很多代码)。先不管if里面是什么,只看12行和73行。如果当前View是可以点击的,最后会返回true消费掉该事件。如果不可点击,直接返回false。
好了,再来看12行到73行之间的代码,在46行找到了
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;
}
看到了很熟悉的onClick
有没有!执行完onClick
后会返回true,即消费掉该事件。
先来总结一下主线一吧。对于一个View来说,事件首先会到达dispatchTouchEvent
,然后在该方法里面会先执行onTouch
,接着如果onTouch
返回false的话就去执行onTouchEvent
,然后onClick
方法在onTouchEvent
中被调用。onTouch
和onTouchEvent
结合起来得到的最后结果会作为dispatchTouchEvent
的返回值。
看到这里,希望你能看得明白。如果可以的话,那么接下来的也会很好理解了,不过我更希望你顺着这个思路自己分析Activity和ViewGroup的源码。
主线二
主线二是: Activity -> ViewGroup -> View
先看Activity.java
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
结合之前第二篇的实验结果,onTouchEvent
最开始是没有被执行的,也就是说,事件分发发生在这里面。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
当事件被消费的时候,getWindow().superDispatchTouchEvent(ev)
返回true,从而让Activity的dispatchTouchEvent
返回true。
再来看onTouchEvent
的源码
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
看一下注释就可以了,当事件没有被任何View处理的时候,事件会返回给Activity处理。这也就可以解释为什么onTouchEvent
有时候会被执行有时候没被执行了。
接着看ViewGroup.java,只看一个方法就好了。
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
在第二篇中已经讲过了这个方法,现在是想来看下它的注释。
大致意思是说:实现这个方法来截获所有的触摸屏幕事件,可以在事件发给你(ViewGroup,下同)的孩子之前监听到事件,并接管这些事件,从而使你的孩子无法收到这些触摸事件。
使用这个方法需要小心点,它和View.onTouchEvent
有着复杂的交互,使用这个方法需要同时也重写onTouchEvent
。事件会以下面的顺序接收到:
- 你会在这里收到
ACTION_DOWN
事件 ACTION_DOWN
事件会被你的孩子处理或者你自己的onTouchEvent
处理。这意味着你必须在onTouchEvent
中返回true,进而你才能继续看到其余的事件(而不是寻找你的父节点去处理)。还有,在onTouchEvent
中返回true的话,在onInterceptTouchEvent
中你不会再接收到剩下的任何事件,所有的事件会像正常情况一样在onTouchEvent
中被处理。- 如果在该方法中返回false的话,接下来的事件会先被分发到这里,然后到达目标View的
onTouchEvent
。 - 如果在该方法中返回true的话,目标View会接收到
ACTION_CANCEL
。进一步的事件将不会出现在这里而是直接到达你的onTouchEvent
方法。
返回true会从你的子节点中偷走事件,然后将事件分发给自己的onTouchEvent
处理,目标View会收到ACTION_CANCEL
事件,进一步的消息将不会出现在onInterceptTouchEvent
中。
讲了一大段,其实讲得有点啰嗦。大致意思就是如果在这个方法中返回true的话,事件会被自己的onTouchEvent
方法处理,不会传递到孩子节点中。同时,在onTouchEvent
中要返回true,否则系统就会去寻找父节点处理该事件。
主线一和主线二就讲到这里了,希望大家能自己分析一下源码,再自己写一写效果会加倍的.
在第二篇的最后还遗留了一个问题,看完这篇,分析起来就很清晰了.
当onInterceptTouchEvent
返回true的时候, CustomLayout自己的onTouchEvent
会被调用,最后返回super.onTouchEvent(event)
,而这里的结果最后又会作为dispatchTouchEvent
的返回值,从而判断是否消费了该事件.为了方便大家查看,我再贴一下图
从结果来看,super.onTouchEvent(event)
的值为false.不信?自己试试看呗.返回false后,该事件没有被任何View消费(注意:该事件是不会分发给CustomButton的),最后回传给了MainActivity自己处理,由于CustomLayout没有消费该事件,所以ACTION_DOWN
在MainActivity中又被处理了一次.
后来,我们让CustomLayout中的onTouchEvent
返回true,即CustomLayout消费了该事件,所以才有了后面的事件.
The End
安卓的事件分发机制写到这里总算完了,希望这几篇博文能让你对事件分发机制有进一步的了解.