专栏名称: 开发者全社区
分享和推送Java/Android方向的技术和文章,让你成为这方面的大牛,让你每天都成长一点。同时,我们也会邀请BAT的大牛分享原创!
目录
相关文章推荐
开发者全社区  ·  广州员工奴性事件 ·  12 小时前  
开发者全社区  ·  大S走了,两任老公争遗产式撕逼表演 ·  23 小时前  
开发者全社区  ·  恒大歌舞团团长嫁人了 ·  昨天  
鸿洋  ·  ActivityTaskManagerSer ... ·  昨天  
鸿洋  ·  关于 2025 副业探索,DeepSeek ... ·  2 天前  
51好读  ›  专栏  ›  开发者全社区

安卓自定义View进阶-事件分发机制详解

开发者全社区  · 公众号  · android  · 2017-04-28 07:31

正文

相关阅读:

读Android源码之事件分发机制最全总结

Android 事件分发机制详解,在上一篇文章 事件分发机制原理 中简要分析了一下事件分发机制的原理,原理是十分简单的,一句话就能总结: 责任链模式,事件层层传递,直到被消费。 虽然原理简单,但是随着 Android 不断的发展,实际运用场景也越来越复杂,所以想要彻底玩转事件分发机制还需要一定技巧,本篇事件分发机制详解将带大家了解 …

你以为我接下来要讲源码?
我就不按套路,所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。所以本篇的核心问题是: 正确理解在实际场景中事件分发机制的作用。 会涉及到源码,但不是主角。

注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。

常见事件

既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。

根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:

事件 简介
ACTION_DOWN 手指 初次接触到屏幕 时触发。
ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
ACTION_UP 手指 离开屏幕 时触发。
ACTION_CANCEL 事件 被上层拦截 时触发。

对于单指触控来说,一次简单的交互流程是这样的:

手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)

  • 本次事例中 ACTION_MOVE 有多次触发。

  • 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。

事件分发、拦截与消费

关于这一部分内容,上一篇文章 事件分发机制原理 已经将流程整理的比较清楚了,本文会深入细节来研究这些内容。之所以分开讲,是为了防止大家被细节所迷惑而忽略了整体逻辑。

表示有该方法。

X 表示没有该方法。

类型 相关方法 ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent X
事件消费 onTouchEvent

View 相关

dispatchTouchEvent 是事件分发机制中的核心,所有的事件调度都归它管。不过我细看表格, ViewGroup 有 dispatchTouchEvent 也就算了,毕竟人家有一堆 ChildView 需要管理,但为啥 View 也有?这就引出了我们的第一个疑问。

Q: 为什么 View 会有 dispatchTouchEvent ?

A: 我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent ,所以 View 也会有事件分发。

相信看到这里很多小伙伴会产生第二个疑问,View 有这么多事件监听器,到底哪个先执行?

Q: 与 View 事件相关的各个方法调用顺序是怎样的?

A: 如果不去看源码,想一下让自己设计会怎样?

  • 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)

  • 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)

  • 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)

  • View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)

所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener

下面我们来看一下实际测试结果:

手指按下,不移动,稍等片刻再抬起。

可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。

接下来就看一下源码是怎么设计的(省略了大量无关代码):

如果觉得源码还是太长,那么用伪代码实现应当是这样的(省略若干安全判断),简单粗暴:

正当你沉迷在源码的”精妙”逻辑的时候,你可能没发现有两个东西失踪了,等回过神来,定睛一看,哎呦妈呀, OnClick 和 OnLongClick 去哪里了?

不要担心,OnClick 和 OnLongClick 的具体调用位置在 onTouchEvent 中,看源码(同样省略大量无关代码):

注意了,第一个重点要出现了(敲黑板)!

注意上面代码中存在一个 return true; 并且是只要 View 可点击就返回 true,就表示事件被消费了。

举个栗子: I have a RelativeLayout ,I have a View ,Ugh, RelativeLayout - View

RelativeLayout
    android:background="#CCC"




    

    android:id="@+id/layout"
    android:onClick="myClick"
    android:layout_width="200dp"
    android:layout_height="200dp">
    View
        android:clickable="true"
        android:layout_width="200dp"
        android:layout_height="200dp" />
RelativeLayout>

现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件 myClick ,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 android:clickable="true" 正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

关于 View 的事件分发先说这么多,下面我们来看一下 ViewGroup 的事件分发。

ViewGroup 相关

ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。

VIewGroup 的事件分发流程又是如何的呢?

上一篇文章 事件分发机制原理 中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。

  • 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。

  • 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。

  • 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。

用伪代码应该是这样的:

有人看到这里可能会有疑问,我看过源码,ViewGroup 的 dispatchTouchEvent 可有二百多行呢,你弄这几行就想忽悠我,别以为我读书少。

当然了,上述源码是不完善的,还有很多问题是没有解决的,例如:

1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?

这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。

2. 当该点的 ChildView 有重叠时应该如何分配?

当 ChildView 重叠时, 一般会分配给显示在最上面的 ChildView
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以 显示在最上面的是最后加载的

如下:

RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" 
    android:id="@+id/activity_main"
    android:layout_width="match_parent" 
    android:layout_height




    
="match_parent"
    tools:context="com.gcssloop.viewtest.MainActivity">
    View
        android:id="@+id/view1"
        android:background="#E4A07B"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
    View
        android:id="@+id/view2"
        android:layout_margin="100dp"
        android:background






请到「今天看啥」查看全文