1.处理点击事件的方法

View的层级

我们知道View的结构是树形的结构,View可以放在ViewGroup中,这个ViewGroup也可以放到另一个ViewGroup中,这样层层的嵌套就组成了View的层级。

什么是点击事件分发

当我们点击屏幕,就产生了触摸事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEvent在View的层级传递的过程就是点击事件分发。

点击事件分发的重要方法

点击事件有三个重要的方法它们分别是:

  • dispatchTouchEvent(MotionEvent ev):用来进行事件的分发
  • onInterceptTouchEvent(MotionEvent ev):用来进行事件的拦截,在dispatchTouchEvent()中调用,需要注意的是View没有提供该方法
  • onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent()方法中进行调用

为了了解这三个方法的关系,我们先来看看ViewGroup的dispatchTouchEvent()方法的部分源码:

  1. @Override
  2. public boolean dispatchTouchEvent(MotionEvent ev) {
  3. ...省略
  4. if (actionMasked == MotionEvent.ACTION_DOWN
  5. || mFirstTouchTarget != null) {
  6. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != ;
  7. if (!disallowIntercept) {
  8. intercepted = onInterceptTouchEvent(ev);
  9. ev.setAction(action); // restore action in case it was changed
  10. } else {
  11. intercepted = false;
  12. }
  13. } else {
  14. // There are no touch targets and this action is not an initial down
  15. // so this view group continues to intercept touches.
  16. intercepted = true;
  17. }
  18.  
  19. ...省略
  20. return handled;
  21. }

很明显在dispatchTouchEvent()方法中调用了onInterceptTouchEvent()方法来判断是否拦截事件,来看看onInterceptTouchEvent()方法:

  1. public boolean onInterceptTouchEvent(MotionEvent ev) {
  2. return false;
  3. }

onInterceptTouchEvent()方法默认返回false,不进行拦截,接着来看看dispatchTouchEvent()方法剩余的部分源码:

  1. public boolean dispatchTouchEvent(MotionEvent ev) {
  2. ...省略
  3. final View[] children = mChildren;
  4. for (int i = childrenCount - ; i >= ; i--) {
  5. final int childIndex = customOrder
  6. ? getChildDrawingOrder(childrenCount, i) : i;
  7. final View child = (preorderedList == null)
  8. ? children[childIndex] : preorderedList.get(childIndex);
  9.  
  10. // If there is a view that has accessibility focus we want it
  11. // to get the event first and if not handled we will perform a
  12. // normal dispatch. We may do a double iteration but this is
  13. // safer given the timeframe.
  14. if (childWithAccessibilityFocus != null) {
  15. if (childWithAccessibilityFocus != child) {
  16. continue;
  17. }
  18. childWithAccessibilityFocus = null;
  19. i = childrenCount - ;
  20. }
  21.  
  22. if (!canViewReceivePointerEvents(child)
  23. || !isTransformedTouchPointInView(x, y, child, null)) {
  24. ev.setTargetAccessibilityFocus(false);
  25. continue;
  26. }
  27.  
  28. newTouchTarget = getTouchTarget(child);
  29. if (newTouchTarget != null) {
  30. // Child is already receiving touch within its bounds.
  31. // Give it the new pointer in addition to the ones it is handling.
  32. newTouchTarget.pointerIdBits |= idBitsToAssign;
  33. break;
  34. }
  35.  
  36. resetCancelNextUpFlag(child);
  37. if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
  38. // Child wants to receive touch within its bounds.
  39. mLastTouchDownTime = ev.getDownTime();
  40. if (preorderedList != null) {
  41. // childIndex points into presorted list, find original index
  42. for (int j = ; j < childrenCount; j++) {
  43. if (children[childIndex] == mChildren[j]) {
  44. mLastTouchDownIndex = j;
  45. break;
  46. }
  47. }
  48. } else {
  49. mLastTouchDownIndex = childIndex;
  50. }
  51. mLastTouchDownX = ev.getX();
  52. mLastTouchDownY = ev.getY();
  53. newTouchTarget = addTouchTarget(child, idBitsToAssign);
  54. alreadyDispatchedToNewTouchTarget = true;
  55. break;
  56. }
  57.  
  58. // The accessibility focus didn't handle the event, so clear
  59. // the flag and do a normal dispatch to all children.
  60. ev.setTargetAccessibilityFocus(false);
  61. }
  62.  
  63. ...省略
  64.  
  65. }

我们看到了for循环,首先遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到则交由子元素来处理。接下来看看37行的dispatchTransformedTouchEvent()方法中实现了什么:

  1. private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
  2. View child, int desiredPointerIdBits) {
  3. final boolean handled;
  4.  
  5. // Canceling motions is a special case. We don't need to perform any transformations
  6. // or filtering. The important part is the action, not the contents.
  7. final int oldAction = event.getAction();
  8. if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
  9. event.setAction(MotionEvent.ACTION_CANCEL);
  10. if (child == null) {
  11. handled = dispatchTouchEvent(event);
  12. } else {
  13. handled = child.dispatchTouchEvent(event);
  14. }
  15. event.setAction(oldAction);
  16. return handled;
  17. }
  18. ...省略
  19. }

如果有子View则调用子View的dispatchTouchEvent(event)方法。如果ViewGroup没有子View则调用super.dispatchTouchEvent(event),ViewGroup是继承View的,我们再来看看View的dispatchTouchEvent(event):

  1. public boolean dispatchTouchEvent(MotionEvent event) {
  2. ...省略
  3. boolean result = false;
  4. if (onFilterTouchEventForSecurity(event)) {
  5. //noinspection SimplifiableIfStatement
  6. ListenerInfo li = mListenerInfo;
  7. if (li != null && li.mOnTouchListener != null
  8. && (mViewFlags & ENABLED_MASK) == ENABLED
  9. && li.mOnTouchListener.onTouch(this, event)) {
  10. result = true;
  11. }
  12.  
  13. if (!result && onTouchEvent(event)) {
  14. result = true;
  15. }
  16. }
  17. ...省略
  18. return result;
  19. }

我们看到如果OnTouchListener不为null并且onTouch()方法返回true,则表示事件被消费,就不会执行onTouchEvent(event),否则就会执行onTouchEvent(event)。再来看看onTouchEvent()方法的部分源码

  1. public boolean onTouchEvent(MotionEvent event) {
  2. ...省略
  3. final int action = event.getAction();
  4. if (((viewFlags & CLICKABLE) == CLICKABLE ||
  5. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
  6. (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
  7. switch (action) {
  8. case MotionEvent.ACTION_UP:
  9. boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != ;
  10. if ((mPrivateFlags & PFLAG_PRESSED) != || prepressed) {
  11. // take focus if we don't have it already and we should in
  12. // touch mode.
  13. boolean focusTaken = false;
  14.  
  15. if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  16. // This is a tap, so remove the longpress check
  17. removeLongPressCallback();
  18.  
  19. // Only perform take click actions if we were in the pressed state
  20. if (!focusTaken) {
  21. // Use a Runnable and post this rather than calling
  22. // performClick directly. This lets other visual state
  23. // of the view update before click actions start.
  24. if (mPerformClick == null) {
  25. mPerformClick = new PerformClick();
  26. }
  27. if (!post(mPerformClick)) {
  28. performClick();
  29. }
  30. }
  31. }
  32. ...省略
  33. }
  34. return true;
  35. }
  36. return false;
  37. }

上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一个为true,那么onTouchEvent就会返回true消耗这个事件。CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按点击,可以通过View的setClickable和setLongClickable方法来设置,也可以通过View的setOnClickListenter和setOnLongClickListener来设置,他们会自动将View的设置为CLICKABLE和LONG_CLICKABLE。
接着在ACTION_UP事件会调用performClick()方法:

  1. public boolean performClick() {
  2. final boolean result;
  3. final ListenerInfo li = mListenerInfo;
  4. if (li != null && li.mOnClickListener != null) {
  5. playSoundEffect(SoundEffectConstants.CLICK);
  6. li.mOnClickListener.onClick(this);
  7. result = true;
  8. } else {
  9. result = false;
  10. }
  11.  
  12. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  13. return result;
  14. }

如果View设置了点击事件OnClickListener,那么它的onClick()方法就会被执行。

2.点击事件分发的传递规则

看到这里我们就可以知道点击事件分发的这三个重要方法的关系,用伪代码来简单表示就是:

  1. public boolean dispatchTouchEvent(MotionEvent ev) {
  2. boolean result=false;
  3. if(onInterceptTouchEvent(ev)){
  4. result=onTouchEvent(ev);
  5.  
  6. }else{
  7. result=child.dispatchTouchEvent(ev);
  8. }
  9. return result;

点击事件由上而下的传递规则

当点击事件产生后会由Activity来处理在传递给Window再传递给顶层的ViewGroup,一般在事件传递中只考虑ViewGroup的onInterceptTouchEvent()方法,因为一般情况我们不会去重写dispatchTouchEvent()方法。
对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGroup的onInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理,如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则交给它的子元素的dispatchTouchEvent()来处理,如此的反复下去。如果传递给最底层的View,View是没有子View的,就会调用View的dispatchTouchEvent()方法,一般情况下最终会调用View的onTouchEvent()方法。

举个现实的例子,就是我们的应用产生了重大的bug,这个bug首先会汇报给技术总监那:

技术总监(顶层ViewGroup)→技术经理(中层ViewGroup)→工程师(底层View)
技术总监不拦截,把bug分给了技术经理,技术经理不拦截把bug分给了工程师,工程师没有下属只有自己处理了。
事件由上而下传递返回值规则为:true,拦截,不继续向下传递;false,不拦截,继续向下传递。

点击事件由下而上的传递规则

点击事件传给最底层的View,如果他的onTouchEvent()方法返回true,则事件由最底层的View消耗并处理了,如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()仍旧返回返回false,则继续传递给改父View的父View处理,如此的反复下去。

再返回我们现实的例子,工程师发现这个bug太难搞不定(onTouchEvent()返回false),他只能交给上级技术经理处理,如果技术经理也搞不定(onTouchEvent()返回false),那就把bug传给技术总监,技术总监一看bug很简单就解决了(onTouchEvent()返回true)。

事件由下而上传递返回值规则为:true,处理了,不继续向上传递;false,不处理,继续向上传递。

点击事件传递时的其他问题

  • 上面源码我们看到:如果我们设置了OnTouchListener并且onTouch()方法返回true,则onTouchEvent()方法不会被调用,否则则会调用onTouchEvent()方法,可见OnTouchListener的优先级要比onTouchEvent()要高。在OnTouchEvent()方法中,如果当前设置了OnClickListener则会执行它的onClick()方法。
  • View的OnTouchEvent()方法默认都会返回true,除非它是不可点击的也就是CLICKABLE和LONG_CLICKABLE都为false。

Android View体系(五)从源码解析View的事件分发机制的更多相关文章

  1. Android IntentService使用介绍以及源码解析

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 一.IntentService概述及使用举例 IntentService内部实现机制用到了HandlerThread,如果对HandlerThrea ...

  2. Cocos2dx源码赏析(3)之事件分发

    Cocos2dx源码赏析(3)之事件分发 这篇,继续从源码的角度赏析下Cocos2dx引擎的另一模块事件分发处理机制.引擎的版本是3.14.同时,也是学习总结的过程,希望通过这种方式来加深对Cocos ...

  3. 【Android应用开发】EasyDialog 源码解析

    示例源码下载 : http://download.csdn.net/detail/han1202012/9115227 EasyDialog 简介 : -- 作用 : 用于在界面进行一些介绍, 说明; ...

  4. 多线程与高并发(五)—— 源码解析 ReentrantLock

    一.前言 ReentrantLock 是基于 AQS 实现的同步框架,关于 AQS 的源码在 这篇文章 已经讲解过,ReentrantLock 的主要实现都依赖AQS,因此在阅读本文前应该先了解 AQ ...

  5. jquery源码解析:jQuery数据缓存机制详解2

    上一课主要讲了jQuery中的缓存机制Data构造方法的源码解析,这一课主要讲jQuery是如何利用Data对象实现有关缓存机制的静态方法和实例方法的.我们接下来,来看这几个静态方法和实例方法的源码解 ...

  6. jquery源码解析:jQuery数据缓存机制详解1

    jQuery中有三种添加数据的方法,$().attr(),$().prop(),$().data().但是前面两种是用来在元素上添加属性值的,只适合少量的数据,比如:title,class,name等 ...

  7. Android View体系(七)从源码解析View的measure流程

    前言 在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure.layout和draw.measure用来测量View的宽高,layout用来确定View的位置, ...

  8. Android View体系(八)从源码解析View的layout和draw流程

    前言 上一篇文章我们讲了View的measure的流程,接下来我们讲下View的layout和draw流程,如果你理解了View的measure的流程,那这篇文章自然就不在话下了. 1.View的la ...

  9. Android FM 模块学习之四 源码解析(1)

    Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE MicrosoftInternetExplorer4 前一章我们了解了FM手动调频,接下 ...

随机推荐

  1. Win10下音频设备无法播放音乐问题定位

    最近一直在调试音频设备,由于音频设备需要在不同的采样率下面转换,所以会经常导致我的win10无法播放和录音. 刚开始在网上搜了相关的知识,但是一直没找到有效的解决方案.后来,无奈之下,使用了微软的声音 ...

  2. HoloLens开发手记-配置开发环境 Install the tools

    随着Build 2016开发者大会的结束,HoloLens开发包也正式开放下载.Hololens没有独立的SDK,开发特性被集成到最新的Visual Studio Update 2中.如果你没有Hol ...

  3. python 变量进阶(理解)

    变量进阶(理解) 目标 变量的引用 可变和不可变类型 局部变量和全局变量 01. 变量的引用 变量 和 数据 都是保存在 内存 中的 在 Python 中 函数 的 参数传递 以及 返回值 都是靠 引 ...

  4. UFLDL 教程学习笔记(二)反向传导算法

    UFLDL(Unsupervised Feature Learning and Deep Learning)Tutorial 是由 Stanford 大学的 Andrew Ng 教授及其团队编写的一套 ...

  5. Python -- queue队列模块

    一 简单使用 --内置模块哦 import Queuemyqueue = Queue.Queue(maxsize = 10) Queue.Queue类即是一个队列的同步实现.队列长度可为无限或者有限. ...

  6. 源码编译安装lnmp环境

    一.源码编译安装步骤 首先说明源码安装的好处   速度快,可自定义路径 主要有三步:1.配置 进入源码安装包  ./configure --prefix=/uer/local/nginx  可指定参数 ...

  7. 从零开始学 Web 之 JavaScript 高级(一)原型,贪吃蛇案例

    大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:ht ...

  8. Jenkins之使用Pyinstaller构建Python应用程序

    目录 1. 极简概述 2. Jenkins配置 2.1 安装JDK 2.2 安装Jenkins 3. 安装Docker 4. 使用PyInstaller构建Python应用程序 4.1 Fork 一个 ...

  9. Rails项目防止时序攻击

    时序攻击属于侧信道攻击/旁路攻击(Side Channel Attack),侧信道攻击是指利用信道外的信息,比如加解密的速度/加解密时芯片引脚的电压/密文传输的流量和途径等进行攻击的方式,一个词形容就 ...

  10. JavaWeb学习 (二十七)————监听器(Listener)在开发中的应用

    监听器在JavaWeb开发中用得比较多,下面说一下监听器(Listener)在开发中的常见应用 一.统计当前在线人数 在JavaWeb应用开发中,有时候我们需要统计当前在线的用户数,此时就可以使用监听 ...