Android时间分发
View的时间分发过程dispatchTouchEvent —> onTouch –-> onTouchEvent
/**
* 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 (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;
}
dispatchTouchEvent的返回值由是否设置触摸回调的返回值或者onTouchEvent的返回值决定。
一个触摸事件发生后,首先如果设置了触摸事件的侦听,并且返回了true,表示该事件已经被消费了,dispatchTouchEvent方法会直接返回true。如果回调返回了false。那么会执行onTouchEvent方法,dispatchTouchEvent的返回值由onTouchEvent的返回值决定。默认情况下,只要可以点击并且是enable的,都会返回true。下面是onTouchEvent方法
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
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 (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 & 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);
}
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 |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
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 & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
长按事件是在ACTION_DOWN 里面执行的,点击事件是在ACTION_UP 里面执行的。长按事件的执行逻辑如下,
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
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;
}
如果执行了长按事件,并且长按的回调返回了true,那么mHasPerformedLongPress = true ,就不会再去执行performClick();
因此想要长按事件和单击事件同时发生,就要在长按的回调函数返回false。
单击事件和长按事件其实都是封装在触摸事件里面的。
ViewGroup的事件分发过程dispatchTouchEvent ---> onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
默认情况下onInterceptTouchEvent返回false,即不拦截。
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
// 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// ACTION_DOWN事件,即按下事件
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we‘re disallowing intercept or if we‘re allowing and we didn‘t
// intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作
if (disallowIntercept || !onInterceptTouchEvent(ev)) // 1、是否禁用拦截、是否拦截事件的判断
// reset this event‘s action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内
final View child = children[i];
// 该child是可见的
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
// 3、获取child的坐标范围
child.getHitRect(frame);
// 4、判断发生该事件坐标是否在该child坐标范围内
if (frame.contains(scrolledXInt, scrolledYInt))
// offset the event to the view‘s coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下,
// dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true,
// 那么mMotionTarget为触摸事件所在位置的child。
if (child.dispatchTouchEvent(ev))
//默认的实现下View.dispatchTouchEvent(ev)返回值一定为true
// Event handled, we have a target now.
mMotionTarget = child;
return true;
//表示子view已经能将触摸时间消费掉
}
}
}
}
}
}// end if
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we‘ve already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn‘t an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
// 6、如果mMotionTarget为空,那么执行super.dispatchTouchEvent(ev),
// 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已. (指没有找到view,也可能是下面两种情况)
// 1,拦截事件 或者2.在不拦截事件target view的onTouchEvent返回false的情况都会执行到这一步. 这种情况下
//执行super.dispatchTouchEvent(ev);也就是当成view来分发事件,过程同 view的时间分发过程一致
if (target == null) {
// We don‘t have a target, this means we‘re handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用super.dispatchTouchEvent(ev); 表示子view没能将触摸时间消费掉,就会将触摸事件传递给父view
}
// if have a target, see if we‘re allowed to and want to intercept its
// events
// 7、如果没有禁用事件拦截,并且onInterceptTouchEvent(ev)返回为true,即进行事件拦截.
//-----似乎只有target!=null也就是子view处理down还返回true,然后拦截事件发生了才会执行下面的if
//也就是对move 和 up 事件的拦截会执行到这里。
//由于在down时child.dispatchTouchEvent(ev)返回了true,所以target有了值。下面的代码是让子view执行ACTION_CANCEL事件
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
//
if (!target.dispatchTouchEvent(ev)) {
//拦截move事件,会让子view分发cancel事件
// target didn‘t handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don‘t dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target‘s coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
// 事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件
// 执行到这里的条件是子view 在ACTION_DOWN时返回true,这样target不为null,并且
//还不会执行target == null 的判断才会执行到这里
return target.dispatchTouchEvent(ev);
}
拦截的使用方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
return true ;
case MotionEvent.ACTION_MOVE:
return true ;
case MotionEvent.ACTION_UP:
return true ;
}
return false;
}
如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;onInterceptTouchEvent(ev) return true的时候,mMotionTarget 为null ;
如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。onInterceptTouchEvent(ev) return true的时候,此时target还不为null,会执行target.dispatchTouchEvent(ev)来分发cancel事件,接着会把mMotionTarget 置为null ;
拦截down事件,会执行到target==null,然后调用super.dispatchTouchEvent(ev);即父view来处理。
拦截move事件,子view会处理cancel事件,父view也不会处理,
requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截
如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;那还有补救的措施。requestDisallowInterceptTouchEvent(true)便可以使子view接收到,因为会跳过if (!disallowIntercept && onInterceptTouchEvent(ev)),执行return target.dispatchTouchEvent(ev);通过源码很容易解释。
但是如果是在ACTION_DOWN时返回true来拦截的,那么子view无论怎么做都不可能捕获任何事件,因为此时target == null,肯定会执行return super.dispatchTouchEvent(ev);