Android UI事件傳遞就是這麼個事兒

咱們尋找的,也只不過是心裏世界的片刻安寧,
以及,那樣一場盛大的清歡。

正文 ###聊聊UI事件傳遞html

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舉例)

上圖簡略關係以下: android

佈局簡略關係.png

× 以前一直覺得事件是從子佈局開始傳遞到父布,由於以直觀的角度咱們先碰到的是子佈局獲得錯誤的事件順序:view1 --> ViewGroup2 --> ViewGroup1git

後來才知道事件是從父佈局傳遞到子佈局,是由父佈局判斷點擊位置上面有子佈局而後向子佈局傳遞。若是事件向子佈局傳遞沒有被攔截和消費,那麼事件又會向父佈局傳遞。正確的沒有被攔截和消費的事件順序:Activity --> ViewGroup1 --> ViewGroup2 --> View1 --> ViewGroup2 --> ViewGroup1 --> Activitygithub

如下的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的時候攔截事件bash

  • 攔截後事件將不會傳到子佈局 如今以ViewGroup1爲例: 讓ViewGroup1中的dispatchTouchEvent直接返回true 當手指對View1點擊、移動、擡起時 發生的一系列事件傳遞(0.按下;1.擡起; 2.移動)app

    複製代碼

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 ```ide

觀察
能夠看出事件傳遞到ViewGroup1後被攔截,沒有被任何佈局消費
也就是說事件還沒被消費就被攔截會致使觸摸無效
咱們能夠在dispatchTouchEvent判斷哪些狀況須要攔截,哪些不須要攔截就放事件過去(以上直接返回了true攔截了全部狀況的事件)

攔截ViewGroup1的全部事件.png

如何獲取?

  • 經過onInterceptTouchEvent獲取事件,當返回值爲true的時候獲取事件佈局

  • 獲取事件後會調用onTouchEvent方法,調用這個方法後,若是咱們設置了OnTouchListener,那麼觸摸監聽將會被調用。 如今以ViewGroup2爲例: 讓ViewGroup2中的onInterceptTouchEvent直接返回true 當手指對View1點擊、移動、擡起時 發生的一系列事件傳遞(0.按下;1.擡起; 2.移動)ui

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,致使事件重新回到父佈局,也就是沒有消費事件。this

    如今接着以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佈局
複製代碼

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爲何獲取事件同樣,因此返回truefalse和預想的結果相反。

#所用知識和資料
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)
複製代碼
相關文章
相關標籤/搜索