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