重写Android事件分发机制

本文详细探讨了Android的事件分发机制,从MotionEvent的down、move、up事件开始,逐步分析了Activity、PhoneWindow、DecorView、ViewGroup和View在事件处理中的角色。重点解析了dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent方法,以及事件传递流程,包括Activity到DecorView,再到ViewGroup和View的过程。文章还提到了关键属性mFirstTouchTarget和mGroupFlags,并概述了down事件的处理策略,包括ViewGroup的拦截和子View的事件消费情况。后续内容计划涵盖事件处理的总结、流程图和常见问题解答。

Android事件分发机制,也被称为“事件处理机制”,“Touch处理机制”等等,指的是对用户触摸屏幕后引发的一系列Touch事件的处理机制,例如用户点击一次屏幕,该事件到底是页面上的哪个View来处理,为什么?

MotionEvent事件主要有:down,move,up

主要分析的类:ViewGroup,View和特殊的View(ListView,ScrollView等)

主要分析的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent

知识背景

Activity管理着一个Window,具体的实现类PhoneWindow,PhoneWindow里面有个DecorView的对象,DecorView分为两部分,title和content。我们在Activity里面调用setContentView,就是给content部分加载布局

事件传递流程源码分析(API 27)

  • 先看Activity.java,在dispatchTouchEvent中,会执行getWindow().superDispatchTouchEvent(ev),调用mWindow的superDispatchTouchEvent(ev),即将事件传递给PhoneWindow
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // getWindow()放回mWindow对象,调用mWindow的superDispatchTouchEvent
    if (getWindow().superDispatchTouchEvent(ev)) {  
        return true;
    }
    return onTouchEvent(ev);
}

public Window getWindow() {
    return mWindow;   
}

private Window mWindow;  // PhoneWindow是Window的唯一实现类

final void attach(...) {
    ...
    // 给mWindow赋值
    mWindow = new PhoneWindow(this, window, activityConfigCallback);      ...
}
  • 在PhoneWindow中又直接将事件传递到DecorView的superDispatchTouchEvent
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
	// 调用mDecor的方法
    return mDecor.superDispatchTouchEvent(event);  
}

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;  // 申明

public PhoneWindow(...) {
    ...
    mDecor = (DecorView) preservedWindow.getDecorView();  // 初始化
    ...    
}
  • DecorView调用父类ViewGroup的superDispatchTouchEvent
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    // DecorView继承FrameLayout,而FrameLayout没有重写dispathToucheEvent,所以这里将会调用FrameLayout的父类ViewGroup的dispatchTouchEvent
    return super.dispatchTouchEvent(event);
}
  • ViewGroup部分源码,主要方法dispatchTouchEvent,dispatchTransformedTouchEvent
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent 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);
        // 每次ACTION_DOWN,都会通过resetTouchState,将mFirstTouchTarget和mGroupFlags重置
        resetTouchState(); 
    }

    // Check for interception ====================================
    final boolean intercepted;  // 申明局部变量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;
    }
    // 是否拦截与actionMasked,mFirstTouchTarget,mGroupFlags,onInterceptTouchEvent()相关
    /**
     * 这一段代码来判断ViewGroup是否拦截该事件
     * 先看第一个if,有两个条件:条件1如果是down事件需要进一步判断是否拦截,
       条件2如果不是down事件,但mFirstTouchTarget不为空需要继续判断
     * mFirstTouchTarget什么时候不为空?在down事件时,如果有子View对事件进行消费(返回true),则mFirstTouchTarget不为空
     * 如果第一个if为false,则intercepted为false;该事件ViewGroup自己处理
     (不一定是消费,而是进入dispatchTransformedTouchEvent方法,这个后面再详细解释)
     * 如果第一个if为true,需要进一步判断,这里有一个mGroupFlags,
     是子View要求ViewGroup是否拦截的标记(子View通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)来标记)
     * 如果子View请求不拦截,即disallowIntercept为true,则intercepted = false;
     * 如果子View请求ViewGroup拦截或者没有要求,那么disallowIntercept为false,
     这里需要第三次判断,即通过ViewGroup的onInterceptTouchEvent来判断是否拦截
     *
     * 从事件流来看是否拦截:
     * down事件:判断条件只有一个,就是onInterceptTouchEvent方法
     * 后续事件:先down事件是否有子控件消费,即mFirstTouchTarget != nul,如果没有直接拦截;
     如果被子控件消费,然后看子控件是否请求父类拦截,
     即调用requestDisallowInterceptTouchEvent,如果要求不拦截,再去看onInterceptTouchEvent方法
     */
    // Check for interception ====================================
    
    ...


	// 判断是否被取消或拦截
	if (!canceled && !intercepted) { 
	    ...
	    // 事件过滤,只处理down
	    if (actionMasked == MotionEvent.ACTION_DOWN
	        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
	        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
	        ...
	        // 理论上newTouchTarget为空,所以主要判断childrenCount,如果有就执行循环语句
	        if (newTouchTarget == null && childrenCount != 0) { 
	            for (int i = childrenCount - 1; i >= 0; i--) {
	            // isTransformedTouchPointInView判断事件发生坐标是否在子view上
	                if (!canViewReceivePointerEvents(child)
	                    || !isTransformedTouchPointInView(x, y, child, null)) {  
	                    continue;        
	                }
	            }    
	        }
	        ...
	        // 这里dispatchTransformedTouchEvent的作用时判断child是否消费当前事件ev
	        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {   
	            ...
	            mLastTouchDownX = ev.getX();
	            mLastTouchDownY = ev.getY();
	            // addTouchTarget方法将对mFirstTouchTarget赋值
	            newTouchTarget = addTouchTarget(child, idBitsToAssign);
	            alreadyDispatchedToNewTouchTarget = true;
	            ...
	            break;
	        }
	        ...
	    }
	    ...
	}

	/**
	  * 这段代码是在不取消且不拦截的情况下,对down事件的处理
	  * 首先,条件必须时不取消不拦截,有子View
	  * 然后只处理down事件
	  * 如何处理,在子View集合中寻找符合条件的view,调用dispatchTransformedTouchEvent,看该View是否消费该事件;
	  * 如果有子View消费该事件,给mFirstTouchTarget赋值,并alreadyDispatchedToNewTouchTarget = true;
	  */
  
	// Dispatch to touch targets.
	if (mFirstTouchTarget == null) {
	    // mFirstTouchTarget比较好理解,在处理down事件时,没有被子View消费
	    // No touch targets so treat this as an ordinary view.
	    handled = dispatchTransformedTouchEvent(ev, canceled, null,
	            TouchTarget.ALL_POINTER_IDS);
	} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        /** 如果当前时down事件,且事件被子View消费,这handler为true;没有被子View消费,不要执行到这里
         *  如果不是down事件,alreadyDispatchedToNewTouchTarget为false
         */ 
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            // 进入dispatchTransformedTouchEvent方法,传入target.child
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
    ...
    return handled; // 最后返回的handled表示事件是否被消费,被子View消费也算
}  
/**
  * 这段代码对事件行进处理
  * down事件:如果子View处理了,这里便不再处理,直接handled = true,
  如果子View没有处理,调用dispatchTransformedTouchEvent,View参数传入null
  * 其他事件:如果子View没有处理,与down事件一样。
  如果子View处理了,调用dispatchTransformedTouchEvent,View参数传入target.child
  */
  
// 在ViewGroup的dispatchTouchEvent中多处调用的dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    // 伪代码
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    // 伪代码
    return handled;
}
/**
 * dispatchTransformedTouchEvent方法中
 * 如果传入View为null,那么调用ViewGroup的super.dispatchTouchEvent,即View的dispatchTouchEvent方法
 * 如果传入View不为空,那么调用View本身的dispatchTouchEvent
 * 如果View是ViewGroup的子类,那么又会进入上面重复的步骤,知道View是View的子类为止
 * 所以,最终会执行View的dispatchTouchEvent
 */
  • View部分源码,主要方法dispatchTouchEvent,onTouchEvent,performClick
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        // 如果设置了onTouchListener,且onTouch返回true
        result = true;
    }

    if (!result && onTouchEvent(event)) {
        // 如果onTouchEvent返回true
        result = true;
    }
    ...
    return result;
}

/**
 * 先判断是否设置了OnTouchListener
 * 如果设置了OnTouchListener,看接口里的onTouch方法是否返回true;
 如果返回true,事件被onTouch消费;如果返回false,事件没有被消费,进入onTouchEvent,事件是否被消费取决与onTouchEvent的返回值
 * 如果没有设置OnTouchListener,直接进入onTouchEvent
 事件是否被消费取决与onTouchEvent的返回值
 * 总的来说,OnTouchListener中的onTouch方法优先于onTouchEvent方法
 */
public boolean onTouchEvent(MotionEvent event) {
    ...
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return clickable;  // 如果View为DISABLED,clickable决定是否消费事件
    }
    
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {  // 如果设置了mTouchDelegate,事件可以叫给mTouchDelegate消费
            return true;
        }
    }
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        ...
        return true; 
        /**
         * 如果clickable或者TOOLTIP,事件被消费
         * TOOLTIP:Indicates this view can display a tool tip on hover or long press.
         是表示这个View在悬停或者长按时能显示工具提示。例:EditText,再长按是可以出现复制粘贴工具提示
         */
        
    }
    
    // 再看if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)这个条件内部,分别对down,move,up,cancel事件进行处理
    // 先看down
    case MotionEvent.ACTION_DOWN:
        ...
        if (!clickable) {
            // clickable为false,就开启一个长按延迟任务
            checkForLongClick(0, x, y);
            break;
        }
        
        
        if (isInScrollingContainer) {
            
            mPrivateFlags |= PFLAG_PREPRESSED;
            if (mPendingCheckForTap == null) {
                mPendingCheckForTap = new CheckForTap();
            }
            mPendingCheckForTap.x = event.getX();
            mPendingCheckForTap.y = event.getY();
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
        } else {
            // Not inside a scrolling container, so show the feedback right away
            setPressed(true, x, y);
            checkForLongClick(0, x, y);
        }
        ...
        break;
    /**
     * 如果是处在正滑动的父View中,也会开启一个CheckForTap任务,CheckForTap中会执行setPressed和开启一个长按延迟任务
     * 如果父View没有滑动,则直接执行setPressed和开启一个长按延迟任务
     */
     
    // 再看move
    // Be lenient about moving outside of buttons
    case MotionEvent.ACTION_MOVE:
        ...
        if (!pointInView(x, y, mTouchSlop)) {
            // Outside button
            // Remove any future long press/tap checks
            removeTapCallback();
            removeLongPressCallback();
            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        }
        ...
        break;
    /**
     * 如果移到View的外面,取消之前开启的延迟任务:CheckForTap任务和长按任务
     */

    // 接着看cancal
    case MotionEvent.ACTION_CANCEL:
        if (clickable) {
            setPressed(false);
        }
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        break;
    ...
    // cancel取消了所有
    
    // 最后看up
    case MotionEvent.ACTION_UP:
        ...
        removeLongPressCallback();  // 取消长按任务
        ...
        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();
            }
            // post(mPerformClick)基本上都会返回true,mPerformClick任务会执行PerformClick()
            // 这里就保证了performClick()一定会被调用;
            if (!post(mPerformClick)) {
                performClick();
            }
        }
        ...
        removeTapCallback();    // 取消任务
        ...
        break;
    // up会调用performClick(),取消了长按任务和CheckForTap任务
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        // 如果设置了OnClickListener,会执行onClick方法,但不会应该事件消费的结果,该方法的返回值在onTouchEvent中并没有被使用
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

/**
 * View中事件分发总结:
 * 1. 是否设置了OnTouchListener,如果设置了,ouTouch返回true,则消费该事件
 * 2. 如果设置了,onTouch返回false,或者没设置OnTouchListener,进入onTouchEvent
 * 3. 在onTouchEvent中,返回结果基本与clickable一致
 
 * OnTouchEvent.onTouch,onTouchEvent,OnClickListener.onClick的顺序:
 onTouch->onTouchEvent->onClick
 
 * onTouchEvent里面关于LongClick和Click事件处理
 * down:标记PFLAG_PRESSED,并发起长按任务
 * move:如果移出View的范围,取消标记PFLAG_PRESSED和长按任务。如果没有取消长按任务,但时间到达延迟时长会调用performLongClick,
 如果设置长按监听事件,并修改mHasPerformedLongPress = true;在之后up时,不在执行performClick方法
 * up:如果没有执行长按事件,也没有移出View的范围,会执行performClick,如果设置了单击监听事件会被执行。
 * cancel:取消所有任务,状态
 */

流程总结:

  • 首先每次触摸屏幕会长生一个事件流,包含一个down事件,若干个move事件,还有一个up事件
  • Activity先调用自身的dispatchTouchEvent(在该方法中,执行getWindow().superDispatchTouchEvent(ev),getWindow返回Window的实现类PhoneWindow的对象)
  • PhoneWindow.superDispatchTouchEvent
  • DecorView.superDispatchTouchEvent(内部调用super.dispatchTouchEvent)
  • ViewGroup.dispatchTouchEvent(DecorView继承FrameLayou,FrameLayout没有重写dispatchTouchEvent,所以直接调用父类ViewGroup的方法)
  • 到进入ViewGroup.dispatchTouchEvent之前,对事件down,move,up的处理方式都一样的,之后要分事件讨论,这里有两个重要的属性mFirstTouchTarget,mGroupFlags

ViewGroup.dispatchTouchEvent-down事件

  • 先将mFirstTouchTarget为null,取消mGroupFlags中FLAG_DISALLOW_INTERCEPT标记。然后根据VieGroup自身的onInterceptTouchEvent方法决定是否拦截该事件。分支1
  • 分支1:如果拦截,会调用dispatchTransformedTouchEvent,View参数并传入null。执行super.dispatchTouchEvent,即ViewGroup的父类View的dispatchTouchEvent(View.dispatchTouchEvent后面单独分析)
  • 分支1:如果不拦截,遍历所有子View,根据touch事件的x,y坐标,确定子View,并调用dispatchTransformedTouchEvent,View参数传入child(钢刚才找到的子View),执行child.dispatchTouchEvent。如果child是ViewGroup就再次上面的流程。这里直接看child不是ViewGroup的情况,view.dispatchTouchEvent的返回值。分支2
  • 分支2:如果返回true,表示事件被消费,mFirstTouchTarget会被赋值
  • 分支2:如果返回false,表示子View不消费该事件,继续看其他子View。

先写到这…

TODO:

  1. 总结
  2. 流程图
  3. 常见问题:例如Button的move事件移出控件范围,后续事件的传递,点击事件为什么不响应

参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值