view事件分发机制是Android知识的重点也是难点,想要实现view的嵌套滑动、滑动菜单等酷炫功能都得先掌握好事件分发机制。View的事件分发机制主要涉及到dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
三个方法。
- public boolean dispatchTouchEvent(MotionEvent event)
负责分发触摸事件至目标view,返回值表示当前事件是否被当前view消费。
- public boolean onInterceptTouchEvent(MotionEvent ev)
表示是否拦截当前触摸事件。如果返回true,表示拦截事件供自己消费,事件不再继续往下层view传递。
- public boolean onTouchEvent(MotionEvent event)
处理触摸事件的具体动作。返回值表示事件是否被消费。
《Android开发艺术探索》中使用如下伪代码描述View的事件分发,非常便于理解。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
简单描述下就是,从最上层捕捉到触摸事件的View开始,在dispatchTouchEvent
方法中调用onInterceptTouchEvent
方法检查是否拦截当前事件。如果拦截,直接调用onTouchEvent
消费掉事件;如果不拦截,调用子view的dispatchTouchEvent
方法,至于调用哪个子view,会根据触摸点坐标来确定目标子view。事件就是一层一层向下分发,直至最终被消费,流程结束。
上述伪代码只是为了帮助我们更好理解地View事件分发机制的大致流程,实际上还有很多细节值得深究。比如,子view可以调用requestDisallowInterceptTouchEvent方法强制打断父view的分发机制,抢夺事件消费的权利;再比如事件一直传递到最下层的view,该view的onTouchEvent返回false(表示不消费事件),那么事件还得继续递归往上层回传。另外,事件也并不总是被onTouchEvent
最终处理。可以看下View的dispatchTouchEvent
源码,ViewGroup在拦截事件时最终也是调用的super.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) {
// 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)) {
//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;
}
从源码可以看到,事实上,如果target view设置了onTouchListener,onTouchListener的onTouch事件会优先于onTouchEvent事件执行。如果onTouch事件返回了true,表示事件被消费流程结束,onTouchEvent就不会调用了。另外,View的OnClickListener优先级最低,只有在onTouchEvent检测触摸事件为tap单击事件时它才会被执行。
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
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 && !mIgnoreNextUpEvent) {
// 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();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
// 其他Action事件
...
}
return true;
}
return false;
}