View事件分发机制
Posted 爱搬砖的摄影师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View事件分发机制相关的知识,希望对你有一定的参考价值。
点击事件的传递规则
点击事件产生后,传递过程:Activity->Window->顶层View->分发到具体的View。前两个传递比较简单,不用说。顶层View一般为ViewGroup,ViewGroup会首先根据onInterceptTouchEvent判断是否拦截,如果拦截,那么就会调用自己的onTouchEvent方法进行处理,如果不拦截,就会分发给子View,子View再调用自己的dispatchTouchEvent做进一步分发。如果所有的View都不消费掉这个事件,那么会调用Activity的onTouchEvent方法来处理。
对于一个View而言,OnTouchListener的优先级高于自己的onTouchEvent方法,在onTouchEvent方法中,TouchDelegate的优先级最高,其次是OnLongClickListener(如果是长按的话),最后才是OnClickListener。
源码解析
当触摸事件发生时,Activity的dispatchTouchEvent首先被调用。
// Activity$dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev)
if (ev.getAction() == MotionEvent.ACTION_DOWN)
onUserInteraction();
// 首先分发给Window去处理,如果该事件没有被消费掉,那么
// 会调用Activity的ouTouchEvent方法
if (getWindow().superDispatchTouchEvent(ev))
return true;
return onTouchEvent(ev);
// PhoneWindow$superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event)
return mDecor.superDispatchTouchEvent(event);
// DecorView$superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event)
return super.dispatchTouchEvent(event);
// ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
...
// 出于安全考虑,如果设置了窗口被遮挡后丢弃事件的属性,那么窗口被
// 遮挡时,事件直接被丢弃,而不会分发出去。也就是说,当出现了Toast、
// 对话框等的时候,触摸事件就不会被响应了。该属性一般不会被设置。
if (onFilterTouchEventForSecurity(ev))
...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN)
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
// 这个方法中会重置FLAG_DISALLOW_INTERCEPT标识,该标识表示当前
// ViewGroup不能拦截触摸事件,也就是说,无论如何该标识都不能控制
// ACTION_DOWN事件的拦截
resetTouchState();
// 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.
// 也就是说如果ViewGorup拦截了某触摸手势的ACTION_DOWN事件后,
// 该触摸手势的后续事件都交给这个ViewGroup进行处理,不再需要用
// onInterceptTouchEvent判断是否拦截
intercepted = true;
...
...
// 还是ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
...
// ViewGroup不拦截时的事件分发
for (int i = childrenCount - 1; i >= 0; i--)
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null)
if (childWithAccessibilityFocus != child)
continue;
childWithAccessibilityFocus = null;
i = childrenCount - 1;
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null))
ev.setTargetAccessibilityFocus(false);
continue;
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null)
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null)
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++)
if (children[childIndex] == mChildren[j])
mLastTouchDownIndex = j;
break;
else
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
...
if (mFirstTouchTarget == null)
// ViewGroup进行拦截或者在子View中没有处理事件时,ViewGroup
// 调用父类View的dispatchTouchEvent进行处理。
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
...
明显,在正常的事件(通常是ACTION_DOWN)分发时,ViewGroup会依次查询每个子View,如果子View能够接收到触摸事件,就会把事件交给该子View去处理。判断是否能接收到触摸事件有两点:view没有在播放动画,也没有准备播放动画;触摸事件的坐标处于view的范围内。子View收到事件后,会调用自己的dispatchTouchEvent来进一步分发。如果子View处理了该事件,那么mFirstTouchTarget就会在addTouchTarget方法中被赋值,并且终止事件的分发。在之前的代码中,可以看到,如果mFirstTouchTarget为null,也就是没有任何子View处理该事件(通常是ACTION_DOWN),那么接下来的所有事件都会被当前的ViewGroup拦截。
// ViewGroup$dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
...
// Perform any necessary transformations and dispatch.
if (child == null)
handled = super.dispatchTouchEvent(transformedEvent);
else
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix())
transformedEvent.transform(child.getInverseMatrix());
handled = child.dispatchTouchEvent(transformedEvent);
...
// View$dispatchTouchEvent
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在处理触摸事件时,会先判断是否设置了OnTouchListener,如果设置了而且onTouch方法处理了该事件,就不再调用onTouchEvent进行处理;否则才会调用onTouchEvent。明显,onTouch优先级高于onTouchEvent。
// View$onTouchEvent
public boolean onTouchEvent(MotionEvent event)
...
if ((viewFlags & ENABLED_MASK) == DISABLED)
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
if (mTouchDelegate != null)
if (mTouchDelegate.onTouchEvent(event))
return true;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
switch (action)
case MotionEvent.ACTION_UP:
// 如果在ListView等可以滚动的容器中ACTION_DOWN时,
// 会有一个延迟之后,才会置位PFLAF_PRESSED,此前都是
// PFLAG_PREPRESSED被置位。
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();
break;
...
return true;
return false;
明显,只要是CLICKABLE或者LONG_CLICKABLE或者CONTEXT_CLICKABLE的,不管View是否是enabled状态,都会消费点击事件。如果View是enabled的,那么会看是否设置了代理,如果有代理,那么先调用代理的onTouchEvent方法看是否被消费掉,如果没有消费,那么才会真正由View来做处理。在ACTION_UP时如果点击事件没有被OnLongClickListener.onLongClick方法消费掉,那么就会调用OnClickListener的onClick方法。
// View$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)
handled = showContextMenu();
if (handled)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return handled;
// View$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;
以上是关于View事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章