Android事件分发

Android事件分发是指触摸屏幕的事件分发,在手指触摸屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • MotionEvent.ACTION_DOWN ——手指刚接触屏幕
  • MotionEvent.ACTION_MOVE——手指在屏幕上面滑动
  • MotionEvent.ACTION_UP——手指从屏幕上松开的一瞬间
    点击事件的分发过程由三个很重要的方法来共同完成:
    public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent(MotionEvent event)和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
    public boolean onInterceptTouchEvent(MotionEvent ev)
    在上述方法内部调用,ViewGroup独有方法,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
    public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
    事件分发是按照Activity -> ViewGroup->View进行的
    代码演示
class MainActivity : ComponentActivity() {companion object{private const val TAG = "MainActivity"}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.main_layout)}override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {when(ev?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: DOWN")MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: UP")}return super.dispatchTouchEvent(ev)}override fun onTouchEvent(event: MotionEvent?): Boolean {when(event?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: DOWN")MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: UP")}return super.onTouchEvent(event)}
}private const val TAG = "DispatchViewGroup"
class DispatchViewGroup : LinearLayout {constructor(context : Context) : this(context ,null)constructor(context : Context,attributeSet: AttributeSet?) : this(context,attributeSet,0)constructor(context : Context,attributeSet: AttributeSet?,defStyleAttr: Int):super(context,attributeSet,defStyleAttr)override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {when(ev?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: DOWN")MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: UP")}return super.dispatchTouchEvent(ev)}override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {when(ev?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "onInterceptTouchEvent: DOWN")MotionEvent.ACTION_UP -> Log.d(TAG, "onInterceptTouchEvent: UP")}return super.onInterceptTouchEvent(ev)}override fun onTouchEvent(event: MotionEvent?): Boolean {when(event?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: DOWN")MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: UP")}return super.onTouchEvent(event)}
}class DispatchView : TextView {constructor(context : Context) : super(context)constructor(context : Context, attributeSet: AttributeSet?) : super(context,attributeSet)constructor(context : Context, attributeSet: AttributeSet?, defStyleAttr: Int):super(context,attributeSet,defStyleAttr)override fun dispatchTouchEvent(event: MotionEvent?): Boolean {when(event?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: down")MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: up")}return super.dispatchTouchEvent(event)}override fun onTouchEvent(event: MotionEvent?): Boolean {when(event?.action){MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: down")MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: up")}return super.onTouchEvent(event)}
}

点击Button,打印的log是:

2023-08-23 20:01:41.150 14174-14174 MIUIInput               com.example.dispathceventdemo        D  [MotionEvent] ViewRootImpl windowName 'com.example.dispathceventdemo/com.example.dispathceventdemo.MainActivity', { action=ACTION_DOWN, id[0]=0, pointerCount=1, eventTime=144660345, downTime=144660345, phoneEventTime=20:01:41.140 } moveCount:0
2023-08-23 20:01:41.151 14174-14174 MainActivity            com.example.dispathceventdemo        D  dispatchTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  dispatchTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  onInterceptTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchView            com.example.dispathceventdemo        D  dispatchTouchEvent: down
2023-08-23 20:01:41.151 14174-14174 DispatchView            com.example.dispathceventdemo        D  onTouchEvent: down
2023-08-23 20:01:41.152 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  onTouchEvent: DOWN
2023-08-23 20:01:41.152 14174-14174 MainActivity            com.example.dispathceventdemo        D  onTouchEvent: DOWN
2023-08-23 20:01:41.379 14174-14174 MIUIInput               com.example.dispathceventdemo        D  [MotionEvent] ViewRootImpl windowName 'com.example.dispathceventdemo/com.example.dispathceventdemo.MainActivity', { action=ACTION_UP, id[0]=0, pointerCount=1, eventTime=144660578, downTime=144660345, phoneEventTime=20:01:41.373 } moveCount:1
2023-08-23 20:01:41.379 14174-14174 MainActivity            com.example.dispathceventdemo        D  dispatchTouchEvent: UP
2023-08-23 20:01:41.379 14174-14174 MainActivity            com.example.dispathceventdemo        D  onTouchEvent: UP

当点击事件没有处理时,可以看到时间处理的流程大致如下:
在这里插入图片描述

源码分析

Activity中dispatchTouchEvent

    /*** 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);}

1.根据描述,dispatchTouchEvent被调用来处理屏幕触摸事件,可以在Window接收到触摸事件之前拦截触摸事件。
2.ACTION_DOWN时,会调用onUserInteraction,表示用户在和屏幕交互,onUserInteraction是一个空方法,可以重写该方法,可以在此实现与用户的交互功能。
3.getWindow().superDispatchTouchEvent 是调用Window中的superDispatchTouchEvent,Window是一个抽象类,唯一实现类是PhoneWindow。
4.superDispatchTouchEvent返回true,dispatchTouchEvent就结束,不会执行Activity中的onTouchEvent方法。

   /***PhoneWindow**/@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}

PhoneWindow中调用了DecorView中 superDispatchTouchEvent(MotionEvent event)

    //DecorViewpublic boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}

这里的super.dispatchTouchEvent调用的是ViewGroup中的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {//mInputEventConsistencyVerifier是用来调试使用的,正式版本中为nullif (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(ev, 1);}// If the event targets the accessibility focused view and this is it, start// normal event dispatch. Maybe a descendant is what will handle the click.//处理辅助功能,在设置里面开启辅助功能才能生效if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {ev.setTargetAccessibilityFocus(false);}//申明事件是否被消费boolean handled = false;//onFilterTouchEventForSecurity进行安全检查,为了防范恶意软件误导用户//主要过滤的是当前Window被遮挡的情况下的触摸事件。if (onFilterTouchEventForSecurity(ev)) {//Android是用一个32位的Int值表示一次TouchEvent,低8位表示具体动作,比如按下,抬起,滑动//这里涉及到多点触控//第一根手指按下产生事件:ACTION_DOWN//第二根手指按下产生事件:ACTION_POINTER_DOWN//此时抬起一根手指产生事件:ACTION_POINTER_UP//再抬起另一根手指产生事件:ACTION_UP//通过 ev.getAction() 得到的值包含了 动作(低8位)、触控点索引(9-16位)等,而不单单是上述的几种行为动作final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// 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.//设置状态和初始化,为一个新的TouchEvent做准备。会调用cancelAndClearTouchTargets()和resetTouchState()cancelAndClearTouchTargets(ev);resetTouchState();}// Check for interception.//ViewGroup拦截结果final boolean intercepted;//刚刚按下屏幕时,mFirstTouchTarget是为null,cancelAndClearTouchTargets中清除上一次点击事件时,会将mFirstTouchTarget置空if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {//disallowIntercept = 是否禁用事件拦截的功能(默认为false),可通过调用requestDisallowInterceptTouchEvent() 修改//用来确定ViewGroup是否禁止拦截事件final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {//默认onInterceptTouchEvent返回false,不拦截,如果想拦截,应该重写onInterceptTouchEvent这个方法//如果ACTION_DOWN 时候返回true,子View怎么都无法响应触摸事件intercepted = onInterceptTouchEvent(ev);//防止onInterceptTouchEvent 中对action 修改//所以重新设置一遍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;}// If intercepted, start normal event dispatch. Also if there is already// a view that is handling the gesture, do normal event dispatch.if (intercepted || mFirstTouchTarget != null) {ev.setTargetAccessibilityFocus(false);}// Check for cancelation.//canceled = true条件//1.此View 被ViewGroup或Window移除//2. actionMasked == MotionEvent.ACTION_CANCEL final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;// Update list of touch targets for pointer down, if needed.final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;//split 作用是判断可以把事件分发到多个子View , 多点触摸开关//这个同样在ViewGroup中提供了public的方法: setMotionEventSplittingEnabled ( boolean  split)来设置.final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0&& !isMouseEvent;<!-- view处理触摸事件的关键代码 -->        TouchTarget newTouchTarget = null;boolean alreadyDispatchedToNewTouchTarget = false;if (!canceled && !intercepted) {//如果是辅助功能事件,会寻找他的targetview来接收这个事件View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()? findChildWithAccessibilityFocus() : null;//ACTION_POINTER_DOWN :代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,有新出现了一个触摸点。//ACTION_HOVER_MOVE : 鼠标事件 , 指针在窗口或者View区域移动,但没有按下。if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 多点触控会有不同的索引,获取索引号//该索引位于MotionEvent中的一个数组没索引值就是数组的下标值//只有up或者down事件才会携带索引值final int actionIndex = ev.getActionIndex(); // always 0 for down// 这个整型变量记录了TouchTarget中view所对应的触控点id// 触控点id的范围是0-31,整型变量中一个二进制为1,则对应绑定该id的触控点//这里根据是否需要分离,对触控点id进行记录// 而如果不需要分离,则默认接受所有触控点的事件// ALL_POINTER_IDS = -1; (-1 二进制 = 11111111111111111111111111111111)final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;//清空这个手指idBitsToAssign对应的TouchTarget链表。removePointersFromTouchTargets(idBitsToAssign);final int childrenCount = mChildrenCount;if (newTouchTarget == null && childrenCount != 0) {//获取触摸坐标final float x =isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);final float y =isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);// Find a child that can receive the event.// Scan children from front to back.final ArrayList<View> preorderedList = buildTouchDispatchChildList();final boolean customOrder = preorderedList == null&& isChildrenDrawingOrderEnabled();final View[] children = mChildren;//倒序遍历子viewfor (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, 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.触摸坐标(x,y)在child的可视范围内//2.child可接受触摸事件,是指child的是可见的(VISIBLE);或者虽然不可见,但是位于动画状态。if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}// 从缓存中获取newTouchTarget newTouchTarget = getTouchTarget(child);if (newTouchTarget != null) {//newTouchTarget != null 表示之前处理过 // 重新设置手指id 跳出循环newTouchTarget.pointerIdBits |= idBitsToAssign;break;}resetCancelNextUpFlag(child);// 调用dispatchTransformedTouchEvent()将触摸事件分发给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 indexfor (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {mLastTouchDownIndex = j;break;}}} else {mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();//如果child.dispatchTouchEvent(event) = true//子view包装成TouchTarge , 头插法插入到mFirstTouchTarget 的单链表中//把 mFirstTouchTarget 指向了child,同时把newTouchTarget也指向了childnewTouchTarget = 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 (preorderedList != null) preorderedList.clear();}if (newTouchTarget == null && mFirstTouchTarget != null) {//这种情况 , mFirstTouchTarget!=null  ,newTouchTarget == null//表示之前能消费事件 , 但是现在不行了(手指不在之前能消费那个view范围内)//但是newTouchTarget = mFirstTouchTarget , 事件还是交给之前能消费那个view处理 newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;}}}// Dispatch to touch targets.if (mFirstTouchTarget == null) {// 没有能消费的子view . 自己super.dispatchTouchEvent处理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;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//如果子view消费了事件,handled = truehandled = true;} else {final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;//如果cancelChild =true , 给子view发送ACTION_CANCEL事件if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}//如果cancelChild =true , 将子view从mFirstTouchTarget 链表移除if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}//遍历mFirstTouchTarget链表predecessor = target;target = next;}}// Update list of touch targets for pointer up or cancel, if needed.if (canceled|| actionMasked == MotionEvent.ACTION_UP|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//重置触摸状态resetTouchState();} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {final int actionIndex = ev.getActionIndex();final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);removePointersFromTouchTargets(idBitsToRemove);}}if (!handled && mInputEventConsistencyVerifier != null) {//移除idBitsToRemove 对应的TouchTargetmInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);}return handled;}

viewGroup中处理事件流程比较长。主要处理步骤如下:
1.接收到ACTION_DOWN时,将之前保存的触摸状态全部清空
2.检查disallowIntercept 是否禁用事件拦截的功能(默认为false),可通过调用requestDisallowInterceptTouchEvent() 修改,如果是disallowIntercept = true,禁止调用onInterceptTouchEvent方法
3.当!canceled && !intercepted即没有被取消和ViewGroup拦截,会进入到childView的接受流程
4.通过dispatchTransformedTouchEvent来处理子view是否接收点击事件,

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;//如果取消事件,那么不需要做其他额外的操作,直接派发事件即可final int oldAction = event.getAction();if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}//oldPointerIdBits表示现在所有的触控id// desiredPointerIdBits来自于该view所在的touchTarget,表示该view感兴趣的触控点id// 因为desiredPointerIdBits有可能全是1,所以需要和oldPointerIdBits进行位与// 得到真正可接受的触控点信息final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// If for some reason we ended up in an inconsistent state where it looks like we// might produce a motion event with no pointers in it, then drop the event.// 控件处于不一致的状态。正在接受事件序列却没有一个触控点id符合if (newPointerIdBits == 0) {return false;}// 来自原始MotionEvent 的新的MotionEvent,只包含目标感兴趣的触控点// 最终派发的是这个MotionEventfinal MotionEvent transformedEvent;//两者相等,表示该view接受所有的触控点的事件//这个时候transfromedEvent相当于原始MotionEvent的复制if (newPointerIdBits == oldPointerIdBits) {if (child == null || child.hasIdentityMatrix()) {if (child == null) {handled = super.dispatchTouchEvent(event);} else {// 当目标控件不存在通过setScaleX()等方法进行的变换时// 为了效率会将原市事件简单地进行控件位置与棍定量变换之后// 发送给目标的dispatchTouchEvent()方法返回final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;event.offsetLocation(offsetX, offsetY);handled = child.dispatchTouchEvent(event);event.offsetLocation(-offsetX, -offsetY);}return handled;}transformedEvent = MotionEvent.obtain(event);} else {//如果两者不相等,对事件进行拆分//只生成目标感兴趣的触控点信息//这里返回事件包括了许该事件的类型,触控点索引等。transformedEvent = event.split(newPointerIdBits);}// Perform any necessary transformations and dispatch.// 对Motion Event的坐标系,转换为目标控件的坐标系并进行分发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());}// 调用子view的方法进行分发handled = child.dispatchTouchEvent(transformedEvent);}// 分发完毕,回收MotionEventtransformedEvent.recycle();return handled;}

View中的dispatchTouchEvent

    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;//系统调试的代码,在ViewGroup的dispatchTouchEvent中也有if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesture//停止滚动stopNestedScroll();}//安全检查,过滤掉一些不合法的事件,比如当前的View的窗口被遮挡了。 if (onFilterTouchEventForSecurity(event)) {//handleScrollBarDragging 处理鼠标拖拽时的滚动,手指触摸时,返回falseif ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//ListenerInfo是View的内部类,里面有各种各样的listener,例如OnClickListener,OnLongClickListener,OnTouchListener等等ListenerInfo li = mListenerInfo;//1.设置了mOnTouchListener//2.View可点击//3.执行OnTouchListener.onTouch方法if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {//执行了OnTouchListener.onTouch,表示该View处理了当前触摸事件result = true;}//1.没有设置setOnTouchListener//2.执行onTouchEventif (!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;}

View中dispatchTouchEvent大致流程如下:
在这里插入图片描述
1.View中dispatchTouchEvent默认返回false。
2.OnTouchListener.onTouch是先于onTouchEvent执行的
3. 当OnTouchListener.onTouch 返回true时,onTouchEvent无法执行。

 public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();final int viewFlags = mViewFlags;final int action = event.getAction();//判断是否可点击,长按和上下文点击(上下文菜单,鼠标右键)//mViewFlags设置了CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE中任一一个flagsfinal boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//view 是DISABLED,通过setEnable设置//PFLAG4_ALLOW_CLICK_WHEN_DISABLED 标志位被设置为 0,即不允许 View 在 disable 状态下被点击//PFLAG4_ALLOW_CLICK_WHEN_DISABLED 是在Android12时引入,用来设置是否允许 View 在 disable 状态下被点击if ((viewFlags & ENABLED_MASK) == DISABLED&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {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;}//如果控件设置了触摸代理,需要通过代理判断是否消耗了触摸事件//mTouchDelegate是通过setTouchDelegate()设置//TouchDelegate可以用来扩大触摸面积if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return true;}}//View是clickable is true//调用 setTooltipText()给 View 设置了提示文本,当用户将鼠标悬停在视图上或长按视图时,Android 会显示 tooltipText 属性的值if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {case MotionEvent.ACTION_UP:mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;//抬手后 tooltip延迟一会消失if ((viewFlags & TOOLTIP) == TOOLTIP) {handleTooltipUp();}if (!clickable) {removeTapCallback();removeLongPressCallback();//触控笔按键被按下或者鼠标右键按下的时候为true,在按键释放或触控笔提起来的时候为falsemInContextButtonPress = false;mHasPerformedLongPress = false;//下一个ACTION_UP类型事件应该被忽略,按键释放或触控笔提起来的时候被设置为truemIgnoreNextUpEvent = false;break;}//PFLAG_PREPRESSED是在ACTION_DOWN事件发生时,在可滚动容器内设置延迟//如果PFLAG_PRESSED标识存在,说明控件处于按压状态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.//变量focusTaken默认为false,影响点击事件是否执行boolean focusTaken = false;//isFocusable 是否可以去获取焦点//isFocusableInTouchMode 判断该控件在触摸模式下是否可以获取焦点//isFocused 是判断当前是否已经获取到焦点if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {focusTaken = requestFocus();//获取焦点}//prepressed 代表CheckForTap类型事件消息还没有执行,就触发了ACTION_UP类型事件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);}//mHasPerformedLongPress 变量代表已经执行了长按事件,如果执行了长按事件,就不会执行点击事件了if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {// This is a tap, so remove the longpress checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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)) {//执行点击事件performClickInternal();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.run();}removeTapCallback();}mIgnoreNextUpEvent = false;break;case MotionEvent.ACTION_DOWN://如果触摸源是触摸屏//将会在变量mPrivateFlags3上设置PFLAG3_FINGER_DOWN标志if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {mPrivateFlags3 |= PFLAG3_FINGER_DOWN;}//将变量mHasPerformedLongPress的值设为false//该变量是为了在一次长按过程中执行过长按之后,避免执行点击事件mHasPerformedLongPress = false;//clickable = false,会进入mHasPerformedLongPressif (!clickable) {//长按处理checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);break;}//判断鼠标右键点击了,会调用showContextMenu()方法,显示上下文菜单if (performButtonActionOnTouchDown(event)) {break;}// Walk up the hierarchy to determine if we're inside a scrolling container.//isInScrollingContainer 判断当前控件是否处于一个可以滚动的容器boolean isInScrollingContainer = isInScrollingContainer();//如果在一个滚动的容器中,会将按压延迟一段时间,来区分该事件是不是滚动事件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 awaysetPressed(true, x, y);checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);}break;case MotionEvent.ACTION_CANCEL:if (clickable) {setPressed(false);}removeTapCallback();removeLongPressCallback();mInContextButtonPress = false;mHasPerformedLongPress = false;mIgnoreNextUpEvent = false;mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;break;case MotionEvent.ACTION_MOVE:if (clickable) {//设置RippleDrawable的控件使用drawableHotspotChanged(x, y);}//getClassification得到手势事件的类别,返回有三种类型//CLASSIFICATION_NONE,没有额外的信息事件//CLASSIFICATION_AMBIGUOUS_GESTURE 分类常量:模糊手势。用户对当前事件流的意图尚未确定。手势动作(如滚动)应被禁止,直到分类解析为另一个值或事件流结束。//CLASSIFICATION_DEEP_PRESS 用户有意使劲压在屏幕上,这种类型的事件可以应用于加速长按事件的发生final int motionClassification = event.getClassification();final boolean ambiguousGesture =motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;int touchSlop = mTouchSlop;//最小滑动距离//hasPendingLongPressCallback 检查消息队列中是否有待执行的长按事件消息if (ambiguousGesture && hasPendingLongPressCallback()) {//pointInView 检查触摸点是否已经超出了控件的范围if (!pointInView(x, y, touchSlop)) {//取消掉长按事件removeLongPressCallback();long delay = (long) (ViewConfiguration.getLongPressTimeout()* mAmbiguousGestureMultiplier);// Subtract the time already spentdelay -= event.getEventTime() - event.getDownTime();checkForLongClick(delay,x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);}touchSlop *= mAmbiguousGestureMultiplier;}// Be lenient about moving outside of buttonsif (!pointInView(x, y, touchSlop)) {// Outside button// Remove any future long press/tap checks//移除延时单击动作removeTapCallback();//移除长按动作removeLongPressCallback();if ((mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;}final boolean deepPress =motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;if (deepPress && hasPendingLongPressCallback()) {// process the long click action immediatelyremoveLongPressCallback();checkForLongClick(0 /* send immediately */,x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);}break;}return true;}return false;}
    /*** Entry point for {@link #performClick()} - other methods on View should call it instead of* {@code performClick()} directly to make sure the autofill manager is notified when* necessary (as subclasses could extend {@code performClick()} without calling the parent's* method).*/private boolean performClickInternal() {//是为了通知自动填充服务该控件执行了点击事件,然后接着执行performClick()方法。notifyAutofillManagerOnClick();return performClick();}//主要为了执行注册的点击事件public boolean performClick() {notifyAutofillManagerOnClick();final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {//先播放点击音效playSoundEffect(SoundEffectConstants.CLICK);//执行setOnClickListener时注册的回调li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}

从OnToucEvent代码中可以看出:
1.检查是否设置了TouchDelegate,设置了就调用mTouchDelegate.onTouchEvent,返回true就直接返回true
2.长按事件是在ACTION_DOWN时处理的
3.短按事件是在ACTION_UP中处理,调用performClickInternal
3.OnClickListener.onClick 和 playSoundEffect点击音效都是在performClickInternal中执行

常见问题

1.activity viewgroup和view都不消费action_down,那么action_up事件是怎么传递的?
当ViewGroup和View都不处理ACTION_DOWN时,ACTION_MOVE和ACTION_UP事件就不会传递到ViewGroup和View的dispathTouchEvent。

2.为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了?
主要是因为mFirstTouchTarget,mFirstTouchTarget是一个链表,存储了TouchTarget对象,TouchTarget的作用场景在事件派发流程中,用于记录派发目标,即消费了事件的子View。当View或ViewGroup没有处理DOWN事件时,DecorView中的mFirstTouchTarget == null

dispatchTouchEvent(MotionEvent ev){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.intercepted = true;}
}

可以看到intercepted = true,默认拦截。
3. 同时对父 View 和子 View 设置点击方法,优先响应哪个?
优先响应View,ViewGroup在dispatchTocuEvent中,如果先响应父 view,那么子 view 将永远无法响应,父 view 要优先响应事件,必须先调用 onInterceptTouchEvent 对事件进行拦截,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 处理。
4.View中OnTouchEvent,OnClickListener和OnTouchListeners,OnLongClickListener三者的优先级是什么样的?
OnTouchListeners 》OnTouchEvent 》OnLongClickListener 》OnClickListener
OnTouchListeners 和 OnTouchEvent都是在dispatchTouchEvent中执行的,OnTouchListeners比OnTouchEvent执行顺序早,
如果OnTouchListeners中onTouch方法返回true,后面的OnTouchEvent不会执行,会导致OnTouchListeners,OnLongClickListener也不会被执行。OnLongClickListener在OnTouchEvent中的DOWN事件就会被执行,OnClickListener会在UP时被调用,而且OnLongClickListener返回true,OnTouchEvent就不会被执行。

参考

https://blog.csdn.net/q1165328963/article/details/120773934

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/86940.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023京东酒类市场数据分析(京东数据开放平台)

根据鲸参谋平台的数据统计&#xff0c;今年7月份京东平台酒类环比集体下滑&#xff0c;接下来我们一起来看白酒、啤酒、葡萄酒的详情数据。 首先来看白酒市场。 鲸参谋数据显示&#xff0c;7月份京东平台白酒的销量为210万&#xff0c;环比下滑约49%&#xff1b;销售额将近19…

【ArcGIS Pro二次开发】(62):复制字段

应网友需求&#xff0c;做了这么一个复制字段的小工具。 假定这样一个场景&#xff0c;手头有一个要素1&#xff0c;要素里有10个字段&#xff0c;另一个要素2&#xff0c;除了shape_area等图形字段外&#xff0c;没有其它字段。 现在的需求是&#xff0c;想把要素1中的8个字…

【C++】list类的模拟实现

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录 前言一、list类的模拟实现1.1 list的…

【Leetcode】124.二叉树中的最大路径和(Hard)

一、题目 1、题目描述 二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root ,返回其…

2024年新iPad Pro将实现6年来最大的升级

彭博社的Mark Gurman长期以来一直将iPad Pro的下一次重大更新定在2024年&#xff0c;在最新一期的Power On时事通讯中&#xff0c;他详细阐述了一些细节&#xff0c;这些细节将使其成为“自2018年以来该产品的首次重大更新” 尽管Gurman将最近的iPad升级描述为“最近特别小”&…

Linux之iptables防火墙

目录 一.网络安全技术 二.防火墙 2.1.防火墙分类 2.2.iptables工具简述 2.3.iptables基本语法 2.4.控制类型 2.5.查看规则 2.6.添加规则 2.7.黑白名单 2.8.根据规则编号删除 清空 替换规则 2.9.默认策略 2.10.隐藏扩展模块 2.11.显示扩展模块 三.iptables保存规则…

【多线程】Thread类的用法

文章目录 1. Thread类的创建1.1 自己创建类继承Thread类1.2 实现Runnable接口1.3 使用匿名内部类创建Thread子类对象1.4 使用匿名内部类创建Runnable子类对象1.5 使用lambda创建 2. Thread常见的构造方法2.1 Thread()2.2 Thread(Runnable target)2.3 Thread(String name)2.4 Th…

ReactNative 密码生成器实战

效果展示图 使用插件 Formik 负责表单校验、监听表单提交、数据校验错误信息展示 Yup 负责表单校验规则 分析页面 从上述的展示图我们可以看到的主要元素有&#xff1a;输入框、单选按钮和按钮。其中生成的密码长度不可能很大也不可能为负数和 0&#xff0c;所以我们可以限…

VR智慧课堂 | 临床兽医学VR实验教学有哪些好处?

随着科技的不断发展&#xff0c;虚拟现实(VR)技术已经逐渐渗透到各个领域&#xff0c;为人们带来了前所未有的体验。在动物医学实验教学中&#xff0c;VR技术的应用也日益受到关注。本文将探讨临床兽医学VR实验教学的好处。 首先&#xff0c;VR技术能够提高动物医学实验的安全性…

跳跃游戏 II【贪心算法】

跳跃游戏 II class Solution {public int jump(int[] nums) {int cur 0;//当前最大覆盖路径int next 0;//下一步的最大覆盖路径int res 0;//存放结果&#xff0c;到达终点时最少的跳跃步数for (int i 0; i < nums.length; i) {//遍历数组&#xff0c;以给出数组以一个…

门禁系统忘记登入密码,现在更换电脑如何迁移旧电脑门禁系统的数据

环境&#xff1a; ivms-4200 v3.10.0.6_c 问题描述&#xff1a; 门禁系统忘记登入密码,现在更换电脑如何迁移旧电脑门禁系统的数据&#xff0c;旧电脑记住密码&#xff0c;忘了密码和密保了 解决方案&#xff1a; 1.前往海康官网下载4200客户端&#xff0c;在新电脑上安装 …

Python OCR 使用easyocr库将图片中的文章提取出来

Python OCR 使用easyocr库将图片中的文章提取出来 初环境内容步骤一&#xff1a;安装easyocr库步骤二&#xff1a;导入必要的库步骤三&#xff1a;创建OCR阅读器对象步骤四&#xff1a;指定要识别的图片路径步骤五&#xff1a;执行OCR识别并提取文章内容步骤六&#xff1a;遍历…