热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

AndroidView事件分发机制详解

本文主要介绍AndroidView事件分发机制,这里整理了相关资料并详细介绍了view分发机制的知识及简单示例代码,有兴趣的小伙伴可以参考下

Android开发,触控无处不在。对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑。View事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了。我以前也看过很多人写的这方面的文章,不是说的太啰嗦就是太模糊,还有一些在细节上写的也有争议,故再次重新整理一下这块内容,十分钟让你搞明白View事件的分发机制。

说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和这三个方法与n个ViewGroup和View堆叠在一起的问题,再复杂的结构都能拆分成1个ViewGroup+1个View。

其实ViewGroup和View都是大同小异,View只是没有了子容器,自然不存在拦截问题,dispatch也很简单,所以弄明白了ViewGroup其实就懂的差不多了。

三个关键方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup处理事件分发的发起者,View/ViewGroup接收到触控事件最先调起的就是这个方法,然后在该方法中判断是否处理拦截或是将事件分发给子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup专用,通过该方法可以达到控件事件的分发方向,一般可以在该方法中判断将事件给ViewGroup独吞或是它继续传递给子容器,是处理事件冲突的最佳地点

public boolean onTouchEvent(MotionEvent event)

触控事件的真正处理者,最后每个事件都会在这里被处理

核心问题

时间分发机制的难点在哪,我觉得难的地方以下几点:三个方法调用规则,确定处理事件的对象以及事件冲突的解决方法。

事件传递规则

一般一次点击会有一系列的MotionEvent,可以简单分为:down->move->….->move->up,当一次event分发到ViewGroup时,上述三个方法之间的 ViewGroup中调用顺序可以用一段简单代码表示

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);

public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isCOnsumed= false;
  if(onInterceptTouchEvent(ev)){
   isCousumed = this.onTouchEvent(ev);
  }else{
   isCOnsumed= childView.dispatchTouchEvent(ev);
  }
  return isConsumed;
}

返回结果true表示事件被处理了,返回false表示没有处理。上面的代码通俗易懂,看起来也很简单,一句话就能概括,ViewGroup收到事件后调用dispatch,在dispatch中先检查是否要拦截,若拦截则ViewGroup吃掉事件,否则交给有处理能力的子容器处理。

不过,简单归简单,写成这样只是为了方便理解,ViewGroup的事件处理流程当然没这么简单,这里忽略了很多细节问题,接下来继续补充。回到上面说的,一系列事件我们经常处理的一般都是一个down,多个move和一个up,光靠上面的伪代码是没办法把这些问题都给完美解决,直接来看ViewGroup的dispatchTouchEvent。

onInterceptTouchEvent调用条件

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代表的含义,intercepted == false表示父容器ViewGroup暂时不拦截事件,事件有机会传给子View处理,返回true表示父容器直接拦截了该系列事件,后续不会再传递给子View了。子View想获取事件只能让该值为false

onInterceptTouchEvent调用返回false(返回false才能传递给子View,对应到上面伪代码的else中的内容,叫事件传递到子容器需要满足的内容更好理解一些)需要满足两个条件中的任意一个就有可能触发(当然只是有可能):

一个是在down的时候,另一个就是mFirstTouchTarget!=null,那mFirstTouchTarget何时不为空,有兴趣的同学可以看ViewGroup中的addTouchTarget这个方法的调用时机,mFirstTouchTarget就是在这里赋值的,源码太长我就不贴了。

mFirstTouchTarget是用来保存ViewGroup中消费了ACTION_DOWN事件的子View,即在上面伪代码中child.dispatchTouchEvent(ev)在ACTION_DOWN的时候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不会为null(这个赋值过程只发生在ACTION_DOWN里,如果子ViewACTION__DOWN不给它赋值后面序列的事件就不会再),反之,若无子View处理,该对象即为null。当然,满足了上述两个条件还不行,必须还要满足!disallowIntercept。

disallowIntercept这个变量很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT这个标记影响,这个值可以被ViewGroup的子View设置,ViewGroup的子View如果调用了requestDisallowInterceptTouchEvent这个方法,会改变FLAG_DISALLOW_INTERCEPT,导致disallowIntercept这个值就是ture了,这种情况会跳过intercept,导致拦截失效。

但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。

所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。

上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。

再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:

if (!canceled && !intercepted) {
....
if (actiOnMasked== MotionEvent.ACTION_DOWN
            || (split && actiOnMasked== MotionEvent.ACTION_POINTER_DOWN)
            || actiOnMasked== MotionEvent.ACTION_HOVER_MOVE) {
 ....
 for(child : childList){
   if(!child satisfied condition....){
     continue;
   }
   newTouchTarget = addTouchTarget(child, idBitsToAssign);//在这里给mFirstTouchTarget赋值
 }

 }
}

可以在这里看到intercepted为false在ACTION_DOWN里才能给上面说过的mFirstTouchTarget赋值,只有mFirstTouchTarget不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。

mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器ViewGroup处理了,真正处理事件传递的方法是dispatchTransformedTouchEvent,再看源码:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
      View child, int desiredPointerIdBits) {
   final boolean handled;

    // Canceling motions is a special case. We don't need to perform any transformations
    // or filtering. The important part is the action, not the contents.
    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;
    }

}

看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。

那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。

所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:

if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
      } else {
     ...
     dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)
   }

这就是为什么mFirstTouchTarget能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。

这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。

孩子是谁的

继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码

if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
   continue;
}

ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isCOnsume== true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isCOnsume== false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。

伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。

一些没说到但也很重要的点

上面解释的东西都很简单,是从一个ViewGroup+一个View开始的,事件分发的执行者是ViewGroup,子容器也只有一个View,但实际开发中当然没这么简单,不过不要怕,再复杂的情况也能够拆分成这种模式的,只不过层次多了一些递归复杂了一些而已,原理还是一样的。

顺带补充几点:

从用户点击屏幕开始触发一个系列的点击事件时,事件真正的传递流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到达ViewGroup之前还有一个DecorView,事件是从Activity传过来的,但这些东西其实和ViewGroup的原理是一样的,Activity能看做一个大的ViewGroup,当它的DecorView包含的所有子View没有人能够消耗事件的时候(这样说有漏洞,大家懂我的意思就行了)最后还是会交给Activity处理。

事件冲突解决可以按照上面的原理在几个point中进行处理。最容易想到的处理的时机是在onInterceptTouchEvent里,比如当一个竖直方向滑动的ViewGroup里嵌套一个横向滑动的ViewGroup,可以在这里的ACTION_MOVE里来判断后续事件应该传递给谁处理,当然,也可以根据上面说的标记位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent来控制事件的流向,这都是比较容易想到的,不过看过别的大神,通过分享MotionEvent的方法来控制事件的流向,即在父容器中保存MotionEvent并在适当的时机传入子View自定义的事件处理方法来分享事件,也是可行的。

任何View只要拒绝了一系列事件中的ACTION_DOWN(返回false),则后续事件都不会再传递过来了。但如果拒绝了其他的事件,后续事件还是可以传过来的,比如View某次ACTION_MOVE没处理,这个没处理的事件最后会被Activity消耗掉(而不是View的父容器),但后续的事件还是会继续传给该View。

合理的利用ACTION_CANCEL能够控制一个系列事件的生命周期,让事件处理更加灵活。

理解事件分发的机制只要明白上面的原理基本就够用了,github上很多牛逼的大神写的各种炫酷的自定义控件的事分发根据这些也能够看明白,当然还有很多扩展的东西和更深入的内容由于篇幅的关系在这里就不罗嗦了,更重要的还是去看源码吧。
最后送各位一句经典:纸上得来终觉浅,绝知此事要躬行!

以上就是对Android View事件分发机制的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!


推荐阅读
  • 精选Unity开源项目:UniRx实现响应式编程
    本文介绍了Unity中的响应式编程框架——UniRx,探讨了其在解决异步编程难题中的应用及优势。 ... [详细]
  • ECharts 基础使用指南
    本文档提供了一个简单的 ECharts 使用示例,帮助初学者快速了解如何在网页中集成和使用 ECharts 创建图表。更多详细信息请参阅官方文档:https://www.echartsjs.com/zh/tutorial.html#5%20分钟上手%20ECharts ... [详细]
  • 目录介绍01.CoordinatorLayout滑动抖动问题描述02.滑动抖动问题分析03.自定义AppBarLayout.Behavior说明04.CoordinatorLayo ... [详细]
  • 本问题涉及对一个非负整数数组执行加一操作。数组以最高位数字在前的方式存储,每个数组元素仅包含一位数字。假设该整数没有前导零,除非该整数为0。 ... [详细]
  • 本文提供了关于如何在 Java 中使用 `com.amazonaws.services.kinesis.model.StreamDescription.getRetentionPeriodHours()` 方法的详细说明,并附带了多个实际代码示例。 ... [详细]
  • Alluxio 1.5.0 版本发布:增强功能与优化
    Alluxio 1.5.0 开源版本引入了多项新特性和改进,旨在提升数据访问速度和系统互操作性。 ... [详细]
  • 深入解析:主流开源分布式文件系统综述
    本文详细探讨了几款主流的开源分布式文件系统,包括HDFS、MooseFS、Lustre、GlusterFS和CephFS,重点分析了它们的元数据管理和数据一致性机制,旨在为读者提供深入的技术见解。 ... [详细]
  • 持续集成概述与实践指南
    本文探讨了持续集成(CI)的基本概念、目的及其在现代软件开发中的应用。通过实例分析,帮助读者理解如何有效实施持续集成,提高软件开发效率。 ... [详细]
  • 纵向|发生_ListView和EditText使用解决方案 ... [详细]
  • 深入解析Apache SkyWalking CVE-2020-9483 SQL注入漏洞
    本文详细探讨了Apache SkyWalking中的SQL注入漏洞(CVE-2020-9483),特别是其影响范围、漏洞原因及修复方法。Apache SkyWalking是一款强大的应用性能管理工具,广泛应用于微服务架构中。然而,该漏洞使得未经授权的攻击者能够通过特定的GraphQL接口执行恶意SQL查询,从而获取敏感信息。 ... [详细]
  • 本文介绍了软件测试项目的实际操作过程,包括各角色的职责分配、项目启动、测试流程及测试人员的主要任务,旨在为从事软件测试工作的技术人员提供指导。 ... [详细]
  • 本文详细介绍了使用NumPy和TensorFlow实现的逻辑回归算法。通过具体代码示例,解释了数据加载、模型训练及分类预测的过程。 ... [详细]
  • 如何创建个性化Ubuntu Live CD
    随着Ubuntu频繁的版本更新,越来越多的用户开始探索创建个性化Ubuntu Live CD的方法。这不仅能满足特定的硬件和软件需求,还能极大地提升用户体验。本文将详细介绍使用Distroshare Ubuntu Imager创建定制化Ubuntu Live CD的过程。 ... [详细]
  • StackEngine,一家位于德克萨斯州奥斯汀的新兴科技公司,近期发布了一套全新的工具集,旨在首次实现Docker生产环境应用的统一管理和自动化。此外,该公司还透露已从多家知名风险投资机构获得了初步的资金支持。 ... [详细]
  • 利用HTML5 Canvas高效构建电信网络拓扑图
    电信网络拓扑图在实际应用中具有很高的实用价值。本文介绍了一个基于HTML5 Canvas的电信网络拓扑图项目,不仅实现了基本的图形展示功能,还加入了自动布局和属性栏功能,使项目更加完善。此Demo经过细微调整即可直接应用于实际项目中。 ... [详细]
author-avatar
大姑娘苍之瑜
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有