Android-UI事件传递就是这么个事儿

我们寻找的,也只不过是内心世界的片刻安宁, 以及,那样一场盛大的清欢。

正文

聊聊UI事件传递

Android UI事件传递.png

什么是UI事件?

  • 触摸屏幕中UI控件的那一刻即为事件发生
  • MotionEvent对象包含了所有的触摸事件,如触摸的位置、多指触摸等
  • MotionEvent描述了当前的操作类型,以下为常见类型(数字代表对应的值):
  • ACTION_DOWN = 0 按下
  • ACTION_UP = 1 抬起
  • ACTION_MOVE = 2 移动
  • ACTION_CANCEL = 3 动作取消
  • ACTION_OUTSIDE = 4 动作超出边界
  • ACTION_POINTER_DOWN = 5 已有一个点被按住,此时再按下一个点
  • ACTION_POINTER_UP = 6 多个点被按住,非最后放开的点都会调用

事件如何传递?

自定义的父布局和子布局,用来观察事件的变化(View1和Button1为自定义View和自定义Button,默认以自定义View1举例)

上图简略关系如下:


布局简略关系.png

× 之前一直以为事件是从子布局开始传递到父布,因为以直观的角度我们先碰到的是子布局得到错误的事件顺序:view1 --> ViewGroup2 --> ViewGroup1

后来才知道事件是从父布局传递到子布局,是由父布局判断点击位置上面有子布局然后向子布局传递。如果事件向子布局传递没有被拦截和消费,那么事件又会向父布局传递。正确的没有被拦截和消费的事件顺序:Activity --> ViewGroup1 --> ViewGroup2 --> View1 --> ViewGroup2 --> ViewGroup1 --> Activity

以下的Log为当手指对view1点击、滑动、抬起时,
发生的一系列事件传递(0.按下;1.抬起; 2.移动):
    E/MainActivity: ----------- dispatchTouchEvent = 0
    E/ViewGroup1: ------------- dispatchTouchEvent = 0
    E/ViewGroup1: ------------- onInterceptTouchEvent = 0
    E/ViewGroup2: ------------- dispatchTouchEvent = 0
    E/ViewGroup2: ------------- onInterceptTouchEvent = 0
    E/View1: ------------------ dispatchTouchEvent = 0
    E/View1: ------------------ onTouchEvent = 0
    E/ViewGroup2: ------------- onTouchEvent = 0
    E/ViewGroup1: ------------- onTouchEvent = 0
    E/MainActivity: ----------- onTouchEvent = 0
    E/MainActivity: ----------- dispatchTouchEvent = 2
    E/MainActivity: ----------- onTouchEvent = 2
    E/MainActivity: ----------- dispatchTouchEvent = 2
    E/MainActivity: ----------- onTouchEvent = 2
    E/MainActivity: ----------- dispatchTouchEvent = 2
    E/MainActivity: ----------- onTouchEvent = 2
    E/MainActivity: ----------- dispatchTouchEvent = 1
    E/MainActivity: ----------- onTouchEvent = 1
    E/MainActivity: ----------- dispatchTouchEvent = 1
    E/MainActivity: ----------- onTouchEvent = 1

| 观察|
| -------- | -----: | :----: |
|可以看出事件由外层大布局到内部子布局传进去,在从子布局传出去(Activity --> ViewGroup1 --> ViewGroup2 --> View1 --> ViewGroup2 --> ViewGroup1 --> Activity)|
|由此log还可以看出:当按下的事件没有被拦截,那么所有状态的事件都由Activity进行处理|

没有拦截事件时.png

如何拦截?

  • 通过dispatchTouchEvent对事件进行拦截,当返回值为true的时候拦截事件

  • 拦截后事件将不会传到子布局
    现在以ViewGroup1为例:
    让ViewGroup1中的dispatchTouchEvent直接返回true
    当手指对View1点击、移动、抬起时
    发生的一系列事件传递(0.按下;1.抬起; 2.移动)

E/MainActivity: ---------------- dispatchTouchEvent = 0
E/ViewGroup1: ------------------ dispatchTouchEvent = 0
E/MainActivity: ---------------- dispatchTouchEvent = 2
E/ViewGroup1: ------------------ dispatchTouchEvent = 2
E/MainActivity: ---------------- dispatchTouchEvent = 2
E/ViewGroup1: ------------------ dispatchTouchEvent = 2
E/MainActivity: ---------------- dispatchTouchEvent = 2
E/ViewGroup1: ------------------ dispatchTouchEvent = 2
E/MainActivity: ---------------- dispatchTouchEvent = 1
E/ViewGroup1: ------------------ dispatchTouchEvent = 1
```

| 观察|
| -------- | -----: | :----: |
|可以看出事件传递到ViewGroup1后被拦截,没有被任何布局消费|
|也就是说事件还没被消费就被拦截会导致触摸无效|
|我们可以在dispatchTouchEvent判断哪些情况需要拦截,哪些不需要拦截就放事件过去(以上直接返回了true拦截了所有情况的事件)|

拦截ViewGroup1的所有事件.png

如何获取?

  • 通过onInterceptTouchEvent获取事件,当返回值为true的时候获取事件

  • 获取事件后会调用onTouchEvent方法,调用这个方法后,如果我们设置了OnTouchListener,那么触摸监听将会被调用。
    现在以ViewGroup2为例:
    让ViewGroup2中的onInterceptTouchEvent直接返回true
    当手指对View1点击、移动、抬起时
    发生的一系列事件传递(0.按下;1.抬起; 2.移动)

E/MainActivity: ----------------  dispatchTouchEvent = 0
E/ViewGroup1: ------------------  dispatchTouchEvent = 0
E/ViewGroup1: ------------------  onInterceptTouchEvent = 0
E/ViewGroup2: ------------------  dispatchTouchEvent = 0
E/ViewGroup2: ------------------  onInterceptTouchEvent = 0
E/ViewGroup2: ------------------  onTouchEvent = 0
E/ViewGroup1: ------------------  onTouchEvent = 0
E/MainActivity: ----------------  onTouchEvent = 0
E/MainActivity: ----------------  dispatchTouchEvent = 2
E/MainActivity: ----------------  onTouchEvent = 2
E/MainActivity: ----------------  dispatchTouchEvent = 2
E/MainActivity: ----------------  onTouchEvent = 2
E/MainActivity: ----------------  dispatchTouchEvent = 2
E/MainActivity: ----------------  onTouchEvent = 2
E/MainActivity: ----------------  dispatchTouchEvent = 1
E/MainActivity: ----------------  onTouchEvent = 1
观察问题 原因 解决
哎呀呀~!为啥我获取到了的事件之后,移动和抬起手指的事件被MainActivity吃了!愤怒!! 原来onTouchEvent如果处理按下事件DOWN的时候没有返回true。如果onTouchEvent处理DOWN时候返回false,则表示没有消费事件,事件将会回到父布局,并且后续事件将不会再传递过来。 onTouchEvent方法中判断为按下DOWN事件的时候,返回true即下面要说的消费
当ViewGroup2事件获取到了,但没有消费.png

如何消费?

  • 上边已经提到过,就是获取事件遗留下来一个问题:获取到了按下事件,为啥没继续获取到后续的事件?就是因为按下时onTouchEvent没有返回true,导致事件从新回到父布局,也就是没有消费事件。

      现在接着以ViewGroup2为例:
      还是让ViewGroup2中的onInterceptTouchEvent直接返回true
      添加:在onTouchEvent方法中添加判断if (event.getAction() == MotionEvent.ACTION_DOWN) {return true;}
      当手指对View1点击、移动、抬起时
      发生的一系列事件传递(0.按下;1.抬起; 2.移动)
    

E/MainActivity: ---------------- dispatchTouchEvent = 0
E/ViewGroup1: ------------------ dispatchTouchEvent = 0
E/ViewGroup1: ------------------ onInterceptTouchEvent = 0
E/ViewGroup2: ------------------ dispatchTouchEvent = 0
E/ViewGroup2: ------------------ onInterceptTouchEvent = 0
E/ViewGroup2: ------------------ onTouchEvent = 0
E/MainActivity: ---------------- dispatchTouchEvent = 2
E/ViewGroup1: ------------------ dispatchTouchEvent = 2
E/ViewGroup1: ------------------ onInterceptTouchEvent = 2
E/ViewGroup2: ------------------ dispatchTouchEvent = 2
E/ViewGroup2: ------------------ onTouchEvent = 2
E/MainActivity: ---------------- onTouchEvent = 2
E/MainActivity: ---------------- dispatchTouchEvent = 2
E/ViewGroup1: ------------------ dispatchTouchEvent = 2
E/ViewGroup1: ------------------ onInterceptTouchEvent = 2
E/ViewGroup2: ------------------ dispatchTouchEvent = 2
E/ViewGroup2: ------------------ onTouchEvent = 2
E/MainActivity: ---------------- onTouchEvent = 2
E/MainActivity: ---------------- dispatchTouchEvent = 1
E/ViewGroup1: ------------------ dispatchTouchEvent = 1
E/ViewGroup1: ------------------ onInterceptTouchEvent = 1
E/ViewGroup2: ------------------ dispatchTouchEvent = 1
E/ViewGroup2: ------------------ onTouchEvent = 1
E/MainActivity: ---------------- onTouchEvent = 1



| `观察`|
|-|
|`由上边log可以看出,现在在ViewGroup2中的onTouchEvent的按下事件返回一个true后,按下事件并没有在传递回父布局中,使得后续事件都将能得到`|
|`可以看出当后续事件传递过来时,ViewGroup2已经没有再次调用onInterceptTouchEvent方法`|
|`我们只是将按下DOWN的事件返回true,所以除了按下事件其他移动或抬起的事件activity都也能获取到。当onTouchEvent不管三七二十一直接返回一个true时,activity就不会获取到事件`|

![当消费ViewGroup2的按下DOWN事件时.png](http://upload-images.jianshu.io/upload_images/1552955-078ecc8bed94af8d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


![当ViewGroup2中onTouchEvent直接返回true时.png](http://upload-images.jianshu.io/upload_images/1552955-fe80728c817d87c0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
### Button获取事件是怎么回事?
- 现在将View1换成Button1,其他的恢复最初状态,先来看看触摸的log

E/MainActivity: ------------- dispatchTouchEvent = 0
E/ViewGroup1: --------------- dispatchTouchEvent = 0
E/ViewGroup1: --------------- onInterceptTouchEvent = 0
E/ViewGroup2: --------------- dispatchTouchEvent = 0
E/ViewGroup2: --------------- onInterceptTouchEvent = 0
E/Button1: ------------------ dispatchTouchEvent = 0
E/Button1: ------------------ onTouchEvent = 0
E/MainActivity: ------------- dispatchTouchEvent = 2
E/ViewGroup1: --------------- dispatchTouchEvent = 2
E/ViewGroup1: --------------- onInterceptTouchEvent = 2
E/ViewGroup2: --------------- dispatchTouchEvent = 2
E/ViewGroup2: --------------- onInterceptTouchEvent = 2
E/Button1: ------------------ dispatchTouchEvent = 2
E/Button1: ------------------ onTouchEvent = 2
E/MainActivity: ------------- dispatchTouchEvent = 2
E/ViewGroup1: --------------- dispatchTouchEvent = 2
E/ViewGroup1: --------------- onInterceptTouchEvent = 2
E/ViewGroup2: --------------- dispatchTouchEvent = 2
E/ViewGroup2: --------------- onInterceptTouchEvent = 2
E/Button1: ------------------ dispatchTouchEvent = 2
E/Button1: ------------------ onTouchEvent = 2
E/MainActivity: ------------- dispatchTouchEvent = 1
E/ViewGroup1: --------------- dispatchTouchEvent = 1
E/ViewGroup1: --------------- onInterceptTouchEvent = 1
E/ViewGroup2: --------------- dispatchTouchEvent = 1
E/ViewGroup2: --------------- onInterceptTouchEvent = 1
E/Button1: ------------------ dispatchTouchEvent = 1
E/Button1: ------------------ onTouchEvent = 1


- 在来看看序列图


![Button获取触摸事件.png](http://upload-images.jianshu.io/upload_images/1552955-737a88ead364d2df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

- 这一看,这不是和上面那张图`当ViewGroup2中onTouchEvent直接返回true时`的效果一样的吗?也就是说button默认就是直接获取了事件,没有让事件返回主布局中。

- 等等还有一个!!!大家都知道布局有个属性**clickable**吧!当设置它的值为true时,使得这个布局事件如button所述!

- 更深入的理解的话这里博客已经介绍的很详细了
 -  [Android事件分发机制完全解析,带你从源码的角度彻底理解(上)](http://blog.csdn.net/guolin_blog/article/details/9097463)
 -  [Android事件分发机制完全解析,带你从源码的角度彻底理解(下)](http://blog.csdn.net/guolin_blog/article/details/9153747)

### 实际的应用
- 
来一个简单的应用

xml布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<CheckBox
android:id="@+id/checkbox_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lock Selection" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/selection_first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="First"/>
<RadioButton
android:id="@+id/selection_second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Second"/>
<RadioButton
android:id="@+id/selection_third"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Third"/>
</RadioGroup>
</LinearLayout>


activity代码

package com.examples.customtouch;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.CheckBox;

/**

  • Created by Dave Smith

  • Double Encore, Inc.

  • Date: 9/25/12

  • TouchListenerActivity
    */
    public class TouchListenerActivity extends Activity implements View.OnTouchListener {

    /* Views to display last seen touch event */
    CheckBox mLockBox;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.touch_listener);

     mLockBox = (CheckBox) findViewById(R.id.checkbox_lock);
    
     findViewById(R.id.selection_first).setOnTouchListener(this);
     findViewById(R.id.selection_second).setOnTouchListener(this);
     findViewById(R.id.selection_third).setOnTouchListener(this);
    

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
    /*
    * Consume the events here so the buttons cannot process them
    * if the CheckBox in the UI is checked
    */
    Log.e("TouchListenerActivity", getNameForEvent(event));
    return mLockBox.isChecked();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    Log.e("onTouchEvent", getNameForEvent(event));
    return super.onTouchEvent(event);
    }

    private String getNameForEvent(MotionEvent event) {
    String action = "";
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    action = "ACTION_DOWN";
    break;
    case MotionEvent.ACTION_CANCEL:
    action = "ACTION_CANCEL";
    break;
    case MotionEvent.ACTION_MOVE:
    action = "ACTION_MOVE";
    break;
    case MotionEvent.ACTION_UP:
    action = "ACTION_UP";
    break;
    default:
    return null;
    }

     return String.format("%s\n%.1f, %.1f", action, event.getX(), event.getY());
    

    }
    }



![效果图](http://upload-images.jianshu.io/upload_images/1552955-c3f471a317844513.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

|`状态`|`描述`|
|:-:|:-:|
|`当Lock Selection没有勾选时`|`下边的单选能正常选择`|
|`当Lock Selection勾选时`|`下边的单选无法点击`|

- 大家是否疑惑了,为什么复选框选中状态,ontouch返回了true反而不能点击了。为什么不是返回false无法点击,返回true时才能点击呢?其实这些控件默认可以点击的都是默认获取事件的,如上面说的button为什么获取事件一样,所以返回true和false和预想的结果相反。

#所用知识和资料
1. Android studio插件plantUml画序列图和类图
2. [PlantUML快速指南](http://archive.3zso.com/archives/plantuml-quickstart.html#sec-5-3) 和 [PlantUML官网](http://plantuml.com/classes.html)
3. [Android事件分发机制完全解析,带你从源码的角度彻底理解(上)](http://blog.csdn.net/guolin_blog/article/details/9097463)
[Android事件分发机制完全解析,带你从源码的角度彻底理解(下)](http://blog.csdn.net/guolin_blog/article/details/9153747)
4. [公共技术点之 View 事件传递](http://a.codekk.com/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8B%20View%20%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92)
5. [最后的那个例子来自于这儿](https://github.com/devunwired/custom-touch-examples)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容