转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/52416141

1. 概述

Android日常研发时,与View接触占领相当多的时间。而关于View的知识,主要集中在View的绘制和View对于点击事件的处理。关于View的绘制过程,能够查看一下这篇文章的介绍;关于View处理点击事件,可能有人会觉得在onTouchEvent()这种方法处理点击事件即可了,不错,详细的处理过程确实是在这种方法中,可是点击事件在View间是怎么分发的?怎么确定当前View想要处理点击事件?这些问题在本篇文章中都会一一解决。

点击事件的分发。在详细的View中是有细小区别的。比方说LinearLayout中事件的分发和RelativeLayout中事件的分发就有一些不同的地方。所以本篇只介绍一个通用的事件分发机制,不针对详细View的源码进行分析。

2. 点击事件类型

点击事件类型有好多种。參考MotionEvent类。在这里主要介绍四种常见的事件类型。

类型 说明
ACTION_DOWN 手指接触到屏幕事件
ACTION_UP 手指抬起离开屏幕事件
ACTION_MOVE 手指在屏幕上面滑动事件
ACTION_CANCEL 点击事件由于某种原因取消,比方说手指滑到屏幕外

一个正常完整的点击事件通常是从 ACTION_DOWN事件(手指接触到屏幕)開始。到ACTION_UP事件(手指抬起离开屏幕)结束,中间能够有滑动操作,例如以下所看到的。

ACTION_DOWN —> (ACTION_MOVE —> ACTION_MOVE —> ACTION_MOVE) —> ACTION_UP

注意手指滑动事件是一个连续的事件。在滑动过程中会一直触发,并非只触发一次。

3. 方法介绍

点击事件分发机制中,通常会由三个方法控制,并非上面所说的关注onTouchEvent()方法即可了。

  • dispatchTouchEvent()。事件的分发方法。一般由父布局调用,将点击事件传递到子View。返回true,代表事件被消费;返回false,表示事件未被消费,事件会继续传递下去。

  • onInterceptTouchEvent()。是否拦截点击事件,假设返回true,表示拦截事件,调用自身onTouchEvent()处理点击事件;假设返回false,不拦截点击事件,则将点击事件传递到子View。
  • onTouchEvent()。处理点击事件的详细方法

注意:View类和Activity类中唯独dispatchTouchEvent()和onTouchEvent()两个方法,并没有onInterceptTouchEvent()方法;上述三个方法在ViewGroup中都存在

注意:此文中”消费”一词的意思并非指调用了onTouchEvent()方法,而是指onTouchEvent()方法或者OnTouchListener()返回了true,表示事件被消费

4. 点击事件分发过程

4.1 Activity事件处理

最先获取点击事件的是Activity,也就是全部View获取到的点击事件都是由Activity传递下去的,Activity会调用顶层的ViewGroup的dispatchTouchEvent()方法。将事件分发给ViewGroup。看一下Activity这块的源码。

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

当接收到ACTION_DOWN事件时候,调用了自身的onUserInteraction()方法,这种方法是个空方法,然后通过调用顶层ViewGroup的dispatchTouchEvent ()方法将事件传递给ViewGroup。假设在ViewGroup中将事件消费了,就直接返回true。假设没有消费。终于会调用Activity的onTouchEvent()方法。也正如上面所说Activity中没有onInterceptTouchEvent()方法。

4.2 ViewGroup事件处理

ViewGroup获取到点击事件后处理过程会依据详细的ViewGroup有不同的操作,这个过程都有一些细小的区别,可是大体思路是不变的,用伪代码来描写叙述ViewGroup处理点击事件的过程。

public boolean dispatchTouchEvent(MotionEvent ev) {
// 假设拦截点击事件,不将该事件传递给子View,同一时候调用本身的onTouchEvent方法来处理事件,并返回布尔值标记是否消费该事件,
if (onInterceptTouchEvent(ev)) {
return onTouchEvent(ev);
}
// 假设不拦截点击事件,将事件传递给子View,并返回处理结果的布尔值标记是否消费该事件
if (isConsumed = child.dispatchTouchEvent(ev)){
return true;
}
// 假设子View也没有消费事件,事件又会从底层向上层传递。终于到activity
return onTouchEvent(ev);
}

ViewGroup拿到点击事件后,首先会调用onInterceptTouchEvent()方法来推断是否须要拦截该事件,防止该事件继续传递到子View。假设须要拦截该事件,则返回true,调用本身的onTouchEvent()方法处理该事件。假设不须要拦截该事件,调用子View的dispatchTouchEvent()方法,将事件传递给子View。假设存在多层ViewGroup嵌套。事件的传递过程也是相同的。

4.3 View事件处理

将事件传递给最底层的View后,比如TextView,看看内部是怎么处理的。源码太多,截取了比較重要的一段源码,例如以下。

public boolean dispatchTouchEvent(MotionEvent event) {
// 其它逻辑
.... // 过滤一些不必要的事件
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
} if (!result && onTouchEvent(event)) {
result = true;
}
} // 其它逻辑
.... return result;
}

贴这一段的源码主要是为了查看OnTouchListener相对于onToucheEvent()的优先级,能够看见。假设View设置了OnTouchListener,而且它消费了点击事件,就不会调用View的onToucheEvent()方法,顺便说一下OnClickListener会在onTouchEvent()方法内部被调用。整个调用的优先级例如以下。

OnTouchListener > onToucheEvent() > OnClickListener

4.4 点击事件分发小结

上面对Activity、ViewGroup、View对事件的处理过程进行了详细的介绍,主要集中于dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()三个方法,从以下这个列表来总结一下。

Touch事件相关方法 功能说明 Activity ViewGroup View
dispatchTouchEvent() 事件分发,返回true,代表事件已经被消费。返回false,代表事件未被消费。事件会回传给父布局处理。 有该方法 有改方法 有该方法
onInterceptTouchEvent() 事件拦截。返回true。代表当前控件要处理事件;返回false。代表当前控件不拦截事件。事件将会下发给子View 无该方法 有该方法 无该方法
onTouchEvent() 事件处理,返回true。代表事件被消费。返回false,代表事件未被消费 有该方法 有该方法 有该方法

5. Touch日志分析

新建一个Activity,复写里面的dispatchTouchEvent()onTouchEvent()方法,只加入日志,不做其它不论什么处理。

public class TouchDemoActivity extends AppCompatActivity {

    @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.dispatchTouchEvent(ev);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}

Activity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wang.demo.touch.view.TouchLayout
android:id="@+id/touch_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wang.demo.touch.view.TouchView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.wang.demo.touch.view.TouchLayout>
</LinearLayout>

TouchLayout继承LinearLayout,并复写了dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()三个方法,在这三个方法里面加入日志。

public class TouchLayout extends LinearLayout {

    // 构造方法

    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.dispatchTouchEvent(ev);
return result;
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onInterceptTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.onInterceptTouchEvent(ev);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}

TouchView直接继承View,并复写了dispatchTouchEvent()onTouchEvent()方法

,加入日志。

public class TouchView extends View {

    // 构造方法

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TouchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
} @Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.dispatchTouchEvent(event);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}

5.1 事件未被消费

上面代码中,都是调用控件的默认方法对事件处理,也就是说。事件并没有被我们消费。点击屏幕并滑动最后抬起手指,看一下日志输出。


图-1 事件未被消费-日志

标记出三个区域。

  • 第一个区域。是事件从Activity传递究竟层View的过程。在这个过程中,是没有控件对点击事件进行消费的。
  • 第二个区域,事件从上到下没有被消费,所以事件又会从底层View回传到ViewGroup而且终于会回传到Activity。

  • 第三个区域,假设控件对第一个获取的事件(ACTION_DOWN)没有进行处理的话,兴许的事件(ACTION_MOVE。ACTION_UP等等)就不会传递到这个控件,所以第三个区域的日志都在Activity这一层处理了。并没有向下层传递。

事件传递示图例如以下所看到的。


图-2 事件未被消费-事件传递图

5.2 事件被ViewGroup拦截且消费

改动一下TouchLayout中的onInterceptTouchEvent()方法让它返回true,表示拦截事件。改动后文件例如以下。

public class TouchLayout extends LinearLayout {

    // 构造函数

    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.dispatchTouchEvent(ev);
return result;
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onInterceptTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
// 返回true。拦截事件
return true;
} @Override
public boolean onTouchEvent(MotionEvent event) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
// 返回true。事件被消费
return true;
}
}

执行一下,日志例如以下。


图-3 事件被拦截并消费-日志

标记出两个区域。

  • 第一个区域,事件正常由Activity向下传递,到ViewGroup层后,事件被拦截,ViewGroup直接调用自身的onTouchEvent()方法消费该事件
  • 第二个区域。由于ViewGroup消费了最初的事件,所以兴许的事件直接传递到了该ViewGroup并进行处理

事件传递例如以下,黑色线是第一次事件传递过程,红色线为兴许事件传递过程。


图-4 事件被拦截并消费-事件传递图

5.3 事件由最底层的View消费

复原TouchLayout类,改动TouchView中的onTouchEvent()方法。让它返回true,表示事件被消费。

public class TouchView extends View {

    // 构造函数

    @Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.dispatchTouchEvent(event);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
return true;
}
}

执行日志例如以下。


图-5 事件被底层View消费-日志

从日志中能够看见,事件正常由Activity向下传递,终于传递究竟层View,并被消费。

这样的情况的事件传递例如以下。


图-6 事件被底层View消费-事件传递图

5.4 ViewGroup设置了OnTouchListener、OnClickListener

DemoMainActivity中设置TouchLayout的OnTouchListener。

public class TouchDemoActivity extends AppCompatActivity {

    @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch); findViewById(R.id.touch_layout).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout OnTouchListener, ev : " + MotionEvent.actionToString(event.getAction()));
}
return true;
}
}); findViewById(R.id.touch_layout).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(Constants.LOG_TAG_TOUCH, "layout OnClickListener");
}
});
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
} boolean result = super.dispatchTouchEvent(ev);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}

同一时候设置TouchView中的onTouchEvent()返回true,表示消费了该事件。

public class TouchView extends View {

    // 构造函数

    @Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.dispatchTouchEvent(event);
return result;
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
return true;
}
}

查看一下事件日志。


图-7 事件分发机制图

能够看见。尽管ViewGroup设置了OnTouchListener,可是事件是被最底层的View消费的。

我们再设置TouchView中的onTouchEvent()返回false,不让最底层的View消费事件。查看一下执行日志


图-8 事件分发机制图

从日志中看,确实是ViewGroup的OnTouchListener将事件消费了,可是为什么没有调用OnClickListenr呢?是由于OnTouchListener高于OnTouchEvent,而OnClickListener的调用时在OnTouchEvent内部。OnTouchListener将事件消费后,并不会调用OnTouchEvent。自然也不会调用OnClickListener了。

5.4 小结

从上面日志中,能够得出一些结论,帮助我们加深对事件分发的理解。

  • 事件尽管是从Activity向底层View传递,在不考虑ViewGroup拦截事件的情况下。最先处理事件(onTouchEvent)的是底层View。假设事件未被底层View消费。事件将会回传给上层的ViewGroup处理(onTouchEvent),若全部的ViewGroup都未消费事件。事件终于会回传到Activity由它做最后的处理(onTouchEvent)。
  • 事件在传递过程中,假设被ViewGroup拦截(onInterceptTouchEvent),该ViewGroup会优先处理该事件。

  • 底层的View或者ViewGroup假设将事件消费了,上层的ViewGroup的OnTouchListener、OnTouchEvetn。OnClickListener都不会被调用。

  • 在同一个View或者ViewGroup的事件处理中。OnTouchListener优先级最高,OnTouchEvent其次,OnClickListener最低。

6. 总结

文章大体介绍了Android的事件分发机制,并没有针对详细的源码进行解说,主要是不同的ViewGroup对于事件的处理在细节上有很多不同,可是在事件处理的大体思路上还是一致的。

而在平时中遇见关于事件处理的问题,去查看详细的View或者ViewGroup中对于事件的处理才是最快捷的解决这个问题的方式。

Android事件分发机制具体解释的更多相关文章

  1. Android 事件分发机制具体解释

    很多其它内容请參照我的个人网站: http://stackvoid.com/ 网上非常多关于Android事件分发机制的解释,大多数描写叙述的都不够清晰,没有吧来龙去脉搞清晰,本文将带你从Touch事 ...

  2. Android事件分发机制(上)

    Android事件分发机制这个问题不止一个人问过我,每次我的回答都显得模拟两可,是因为自己一直对这个没有很好的理解,趁现在比较闲对这个做一点总结 举个例子: 你当前有一个非常简单的项目,只有一个Act ...

  3. [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...

  4. Android事件分发机制源码分析

    Android事件分发机制源码分析 Android事件分发机制源码分析 Part1事件来源以及传递顺序 Activity分发事件源码 PhoneWindow分发事件源码 小结 Part2ViewGro ...

  5. 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...

  6. Android事件分发机制二:viewGroup与view对事件的处理

    前言 很高兴遇见你~ 在上一篇文章 Android事件分发机制一:事件是如何到达activity的? 中,我们讨论了触摸信息从屏幕产生到发送给具体 的view处理的整体流程,这里先来简单回顾一下: 触 ...

  7. 本以为精通Android事件分发机制,没想到被面试官问懵了

    文章中出现的源码均基于8.0 前言 事件分发机制不仅仅是核心知识点更是难点,并且还是View的一大难题滑动冲突解决方法的理论基础,因此掌握好View的事件分发机制是十分重要的. 一.基本认识 1. 事 ...

  8. Android事件分发机制(下)

    这篇文章继续讨论Android事件分发机制,首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子 ...

  9. android事件分发机制

    android事件分发机制,给控件设置ontouch监听事件,当ontouch返回true时,他就不会走onTouchEvent方法,要想走onTouchEvent方法只需要返回ontouch返回fa ...

随机推荐

  1. .Net 开源控件 NPlot使用小结

    NPlot是一款非常难得的.Net平台下的图表控件,能做各种曲线图,柱状图,饼图,散点图,股票图等,而且它免费又开源,使用起来也非常符合程序员的习惯.授权方式为BSD许可证. 下载链接: http:/ ...

  2. Android实战简易教程-第二十五枪(基于Baas的数据表查询下拉刷新和上拉载入实现!)

    上一节我们实现了数据表的载入,可是,当数据表数据非常多时.我们就要考虑数据的分页.这里我们选用了PullToRefreshListView控件,先看一下该控件的说明: 效果图:            ...

  3. 使用SqlBulkCopy进行批量数据插入

    Dim dt As DataTable = New DataTable() dt.Columns.Add("DtCostProductRuleGUID", GetType(Guid ...

  4. android notify

    notify http://examples.javacodegeeks.com/android/core/ui/notifications/android-notifications-example ...

  5. gomail发送附件

    采用github.com/go-gomail/gomail/ 的邮件功能,可以发送附件 以及html文档,下面是其给出的demo,测试通过. package main //cmd: go get go ...

  6. 父子margin塌陷

    1.使用padding 2.给父级使用border 3.给父级添加属性 overflow:hidden 4.浮动 5.定位{absolute,fixed} 6.伪元素代码 .parent:before ...

  7. 动态链接库DLL的创建生成及调用

    一.背景 最近在做CANTOUSB底层驱动的调用,是调用别人已经封装好的库,看不到别人写的源程序.程序中调用的是隐式调用即 x.h+x.lib+x.dll,其中DLL即是动态链接库(Dynamic L ...

  8. sql 高性能存储过程分页

    USE [Lyjjr] GO /****** Object: StoredProcedure [dbo].[P_ViewPage] Script Date: 05/29/2015 17:18:56 * ...

  9. Servlet的异常处理机制

    一 声明式异常处理   在web.xml中对声明对各种异常的处理方法. 通过 <error-page>元素来声明. 此元素的结构如下:                    +------ ...

  10. 【Codeforces Round #447 (Div. 2) A】QAQ

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] C语言程序练习题 [代码] #include <bits/stdc++.h> using namespace std; ...