Android一文让你轻松搞定Touch事件分发

源码分析

下面,咱们一起通过源码,全面解析事件分发机制,即按顺序讲解:

  • Activity事件分发机制

  • ViewGroup事件分发机制

  • View事件分发机制

Activity事件分发机制

Android事件分发机制首先会将点击事件传递到Activity中,具体是执行dispatchTouchEvent()进行事件分发。

Activity.dispatchTouchEvent()源码

/*** 创建人:帅次* 创建时间:2021/7/5* 功能:Activity.dispatchTouchEvent()*/
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {//在这里仅用于ACTION_DOWN的判断onUserInteraction();}//返回trueif (getWindow().superDispatchTouchEvent(ev)) {//Activity.dispatchTouchEvent()就返回true,则方法结束。//该点击事件停止往下传递&事件传递过程结束(让爷爷吃了嘿嘿)return true;}return onTouchEvent(ev);
}

Activity.onUserInteraction()源码

/*** 创建人:帅次* 创建时间:2021/7/6* 功能:该方法是用户交互,每当向Activity分派按键、触摸或轨迹球事件时调用。*/
public void onUserInteraction() {
}

Window.superDispatchTouchEvent()源码

/*** 创建人:帅次* 创建时间:2021/7/6* 功能:Window.superDispatchTouchEvent属于抽象方法。* 用于自定义窗口,如Dialog,传递触摸屏事件进一步向下视图层次结构。* 应用程序开发人员应该不需要实现或调用它。*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);

因为Window是抽象类,咱就继续挖,就找到了它的唯一实现类「PhoneWindow」

PhoneWindow.superDispatchTouchEvent()源码

// This is the top-level view of the window, containing the window decor.
//这是窗口的顶层View的实例对象。
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {//咱们继续往下看return mDecor.superDispatchTouchEvent(event);
}

DecorView.superDispatchTouchEvent()源码

public boolean superDispatchTouchEvent(MotionEvent event) {//super调用父类dispatchTouchEvent方法。那它的父类是谁呢?//DecorView extends FrameLayout、FrameLayout extends ViewGroup!//从上面看出DecorView 是ViewGroup的间接子类。//看到这里Activity.dispatchTouchEvent()也基本差不多了//如果ViewGroup.dispatchTouchEvent return true,//则Activity.dispatchTouchEvent()return true。//如果ViewGroup.dispatchTouchEvent return false,//则执行Activity.onTouchEvent(ev)。return super.dispatchTouchEvent(event);
}

Activity.onTouchEvent()源码

/*** 创建人:帅次* 创建时间:2021/7/6* 功能:当Touch事件未被其下的任何View消费时调用。*/
public boolean onTouchEvent(MotionEvent event) {//Window.shouldCloseOnTouch来判断是否消费。//那咱就继续看看Window.shouldCloseOnTouch是干嘛的if (mWindow.shouldCloseOnTouch(this, event)) {finish();//已经消费了该事件,则返回truereturn true;}//还没有消费返回false,默认返回falsereturn false;
}

Window.shouldCloseOnTouch()源码

//这里支持的最高版本maxTargetSdk = Build.VERSION_CODES.P(28),咱使用SDK30就没办法处理了。
//主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {final boolean isOutside =event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)|| event.getAction() == MotionEvent.ACTION_OUTSIDE;if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {//return true:说明事件在边界外,即 消费事件return true;}
//返回false:在边界内,即未消费(默认)return false;
}

Activity.onTouchEvent()到这里就基本结束了。后面源码完善在再补充。

Activity源码总结

当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent()),流程如下:

ViewGroup事件分发机制

从上面Activity的事件分发机制可知,在Activity.dispatchTouchEvent()实现了将事件从Activity->ViewGroup的传递,ViewGroup的事件分发机制从dispatchTouchEvent()开始。

在Activity.dispatchTouchEvent()中遗留了ViewGroup.dispatchTouchEvent()什么时候返回true/false在下面的源码分析中找出来。

ViewGroup.dispatchTouchEvent()源码

先从宏观角度,纵览整个 dispatch 的源码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {/*** 一、检查当前ViewGroup是否需要拦截事件* 1、如果事件为DOWN事件,则调用onInterceptTouchEvent进行拦截判断;* 2、mFirstTouchTarget!=null,代表已经有子View捕获了这个事件,子 View 的 dispatchTouchEvent 返回true就是代表捕获touch 件。*//*** 二、将事件分发给子View* 满足条件canceled和intercepted都为false,既不取消也不拦截* 1、actionMasked==MotionEvent.ACTION_DOWN表明事件主动分发的前提是事件为 DOWN 事件* 2、通过for循环,遍历当前ViewGroup下的所有子View;* 3、处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;* 4、调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,如果子 View 捕获事件成功,则将 View 赋值给 mFirstTouchTarget 。*//*** 三、根据mFirstTouchTarget再次分发事件* 3.1、mFirstTouchTarget为null,说明在上述的事件分发中并没有子 View 对事件进行了捕获操作。直接调用 dispatchTransformedTouchEvent 方法,并传入child为 null最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自身的 onTouchEvent 方法,进行处理touch事件。结论:如果没有子 View 捕获处理 touch 事件,ViewGroup会通过自身的onTouchEvent方法进行处理。* 3.2、mFirstTouchTarget 不为 null,说明在上述的事件分发中有子 View 对 touch 事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。*/
}

下面咱逐层分析:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {// 是否按下操作 , 最终的对外返回结果 , 该方法的最终返回值 boolean handled = false;/*onFilterTouchEventForSecurity以应用安全策略过滤触摸事件。/return true分派事件,return false删除事件。*/if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;final boolean intercepted;/*** 一、检查当前ViewGroup是否需要拦截事件* 1、如果事件为DOWN事件,则调用onInterceptTouchEvent进行拦截判断;* 2、mFirstTouchTarget!=null,代表已经有子View捕获了这个事件,子 View 的 dispatchTouchEvent 返回true就是代表捕获touch 件。*/if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);//恢复操作以防它被更改ev.setAction(action);} 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;}// Check for cancelation.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;final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0&& !isMouseEvent;TouchTarget newTouchTarget = null;boolean alreadyDispatchedToNewTouchTarget = false;/*** 二、将事件分发给子View* 满足条件canceled和intercepted都为false,既不取消也不拦截*/if (!canceled && !intercepted) {/*1、actionMasked==MotionEvent.ACTION_DOWN表明事件主动分发的前提是事件为 DOWN 事件*/if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {final int actionIndex = ev.getActionIndex(); // always 0 for downfinal int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;// Clean up earlier touch targets for this pointer id in case they// have become out of sync.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;//2、通过for循环,遍历当前ViewGroup下的所有子View;for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);//3、处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {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);/*4、调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,如果子 View 捕获事件成功,则将 View 赋值给 mFirstTouchTarget 。*/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();newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}}if (preorderedList != null) preorderedList.clear();}}}/*** 三、根据mFirstTouchTarget再次分发事件*///3.1mFirstTouchTarget为null,if (mFirstTouchTarget == null) {/*说明在上述的事件分发中并没有子 View 对事件进行了捕获操作。直接调用 dispatchTransformedTouchEvent 方法,并传入child为 null最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自身的 onTouchEvent 方法,进行处理touch事件。*//*结论:如果没有子 View 捕获处理 touch 事件,ViewGroup会通过自身的onTouchEvent方法进行处理。*/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) {handled = true;} else {final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;/*mFirstTouchTarget 不为 null,说明在上述的事件分发中有子 View 对 touch 事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。*/if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}}predecessor = target;target = next;}}}return handled;
}

ViewGroup.onInterceptTouchEvent()源码

/*** 创建人:帅次* 创建时间:2021/7/6* 前提ViewGroup.dispatchTouchEvent return super.dispatchTouchEvent(ev);* 功能:是否拦截事件 return true拦截,return false(默认)不拦截*/
public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getX(), ev.getY())) {return true;}return false;
}

ViewGroup.onTouchEvent()源码

ViewGroup是没有onTouchEvent()这个方法的,因为ViewGroup是View的子类,所以ViewGroup可以直接调用View的onTouchEvent()方法。这里就不做详细介绍,下面在View事件分发机制中一起解答。

ViweGroup源码总结

Android事件分发传递到Acitivity后,总是先传递到ViewGroup、再传递到View。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到ViewGroup)

View的事件分发机制

从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始。

View.dispatchTouchEvent()源码

public boolean dispatchTouchEvent(MotionEvent event) {/*一、(mViewFlags & ENABLED_MASK) == ENABLED1、该条件是判断当前点击的控件是否enable2、由于很多View默认enable,故该条件恒定为true(除非手动设置为false)*//*二、mOnTouchListener != null1、mOnTouchListener变量在View.setOnTouchListener()里赋值2、即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)*//*三、mOnTouchListener.onTouch(this, event)1、即回调控件注册Touch事件时的onTouch();2、需手动复写设置,具体如下(以View为例)view.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {MLog.logEvent("MyTouchTrueView.onTouch自行处理:",event);return true;// 1、若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束// 2、若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出if,执行onTouchEvent(event)// 3、onTouchEvent()源码分析 }});*/if ( (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener != null &&mOnTouchListener.onTouch(this, event)) {return true;}return onTouchEvent(event);
}

View.onTouchEvent()源码

public boolean onTouchEvent(MotionEvent event) {  ... // 仅展示关键代码// 若该控件可点击,则进入switch判断中if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  // 根据当前事件类型进行判断处理switch (event.getAction()) { // 抬起Viewcase MotionEvent.ACTION_UP:  performClick(); break;  // 按下case MotionEvent.ACTION_DOWN:  postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  break;  // 取消case MotionEvent.ACTION_CANCEL:  refreshDrawableState();  removeTapCallback();  break;// 滑动case MotionEvent.ACTION_MOVE:  final int x = (int) event.getX();  final int y = (int) event.getY();  int slop = mTouchSlop;  if ((x < 0 - slop) || (x >= getWidth() + slop) ||  (y < 0 - slop) || (y >= getHeight() + slop)) {  removeTapCallback();  if ((mPrivateFlags & PRESSED) != 0) {  removeLongPressCallback();  mPrivateFlags &= ~PRESSED;  refreshDrawableState();  }  }  break;  }  // 若该控件可点击,就一定返回truereturn true;  }  // 若该控件不可点击,就一定返回falsereturn false;
}public boolean performClick() {  if (mOnClickListener != null) {// 只要通过setOnClickListener()为控件View注册1个点击事件// 那么就会给mOnClickListener变量赋值(即不为空)// 则会往下回调onClick() & performClick()返回trueplaySoundEffect(SoundEffectConstants.CLICK);  mOnClickListener.onClick(this);  return true;  }  return false;  }

View源码总结

Android事件分发传递到Acitivity后,总是先传递到ViewGroup、再传递到View。流程总结如下:(假设已经经过了ViewGroup事件分发传递并传递到View)

总结

重点分析了 dispatchTouchEvent 的事件的流程机制,这一过程主要分 3 部分:

  • 1、判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;

  • 2、在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;

  • 3、最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。

然后介绍了整个事件分发中的几个特殊的点。

  • 1、DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;

  • 2、mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;

  • 3、CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。

以上就是本文的全部内容,希望对大家学习 Android 事件分发有所帮助和启发。

Android Touch事件分发(源码分析)相关推荐

  1. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  2. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  3. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  4. Android主流三方库源码分析(九、深入理解EventBus源码)

    一.EventBus使用流程概念 1.Android事件发布/订阅框架 2.事件传递既可用于Android四大组件间通信 3.EventBus的优点是代码简洁,使用简单,事件发布.订阅充分解耦 4.首 ...

  5. Android 系统(78)---《android framework常用api源码分析》之 app应用安装流程

    <android framework常用api源码分析>之 app应用安装流程 <android framework常用api源码分析>android生态在中国已经发展非常庞大 ...

  6. Android Touch事件分发—拦截—处理

    Android Touch事件分发(dispatchTouchEvent)-拦截(onInterceptTouchEvent)-处理(onTouchEvent) 转自:http://www.cnblo ...

  7. Android Camera 系统架构源码分析

    Android Camera 系统架构源码分析(1)---->Camera的初始化 Android Camera 系统架构源码分析(2)---->Camera的startPreview和s ...

  8. Android 11.0 Settings源码分析 - 主界面加载

    Android 11.0 Settings源码分析 - 主界面加载 本篇主要记录AndroidR Settings源码主界面加载流程,方便后续工作调试其流程. Settings代码路径: packag ...

  9. Android录音下————AudioRecord源码分析

    Android录音下----AudioRecord源码分析 文章目录 Android录音下----AudioRecord源码分析 一.概述 1.主要分析点 2.储备知识 二.getMinBufferS ...

最新文章

  1. CVPR | 让合成图像更真实,上交大提出基于域验证的图像和谐化
  2. linux 文件搜索 grep locate find
  3. Android 数字签名学习笔记
  4. 荣耀30s鸿蒙5g,荣耀赵明:荣耀30S将掀起5G手机购买狂潮
  5. UVALive 3026 Period (KMP算法简介)
  6. 由浅入深之Jq选择器(2)
  7. 单位矩阵的逆矩阵是它本身吗_矩阵运算、单位矩阵与逆矩阵(二)
  8. 超越杭州、北京居首、广州晋级第一梯队……国内城市算力大起底!
  9. onclick 源码_精读:手写React框架 解析Hooks源码
  10. AR科技贯穿里约奥运始终 腾讯QQ营销顺风车值了
  11. Win7下安装Flash低版本
  12. linux 启动禁用显卡驱动,Linux secure boot(安全启动)时添加Nvidia显卡驱动
  13. C#键盘钩子之局部钩子和全局钩子
  14. WIN2K XP 2K3 下红警不能联机的完美解决方案(转)
  15. STM32F103_study57_The punctual atoms(STM32 Port multiplexing and remapping configuration)
  16. AJAX聊天室实现原理 JQuery+PHP
  17. Excel技能树系列04:函数的套路
  18. MySQL数据库修改数据库名的三种方式
  19. Liferay的学习
  20. git 修改远端 commit 信息

热门文章

  1. bim学习—— 第6章 放置结构柱(可批量)
  2. 什么是外汇?怎么玩?炒外汇能赚钱吗?
  3. 常用的html单标签和标签都有哪些?
  4. 天下3服务器全部显示test,《天下》iOS品鉴 Test Flight 用户使用流程
  5. BZOJ 2246 [SDOI2011]迷宫探险 (记忆化搜索)
  6. gdb调试常用概念整理
  7. 模拟双色球机选的小程序
  8. 转:无效努力,解决不了系统困境
  9. Python的判及格与否
  10. java封装函数_关于java中函数功能的封装