### 初探View事件
前言View的事件分發和滑動沖突處理是老生常談的知識了,因為最近擼了一個[仿QQ側滑刪除](https://github.com/qdxxxx/SwipeMenuContainer "optional title"),所以對該View事件有了更深入的總結。老鐵們是時候走一波[star](https://github.com/qdxxxx/SwipeMenuContainer "optional title")了。?
我們常說的View事件是指: 從手指親密接觸屏幕的那一刻到手指離開屏幕的這個過程,該事件序列以down事件為起點,move事件為過程,up事件為終點。?
一次down-move-up這一個事件過程我們稱為一個事件序列。所以我們今天研究的對象就是MotionEvent。
### 事件分發
#### 理論知識
* public boolean dispatchTouchEvent(MotionEvent ev)?
用來分發事件,即事件序列的大門,如果事件傳遞到當前View的`onTouchEvent`或者是子View的`dispatchTouchEvent`,即該方法被調用了。?
return true:?表示消耗了當前事件,有可能是當前View的`onTouchEvent`或者是子View的`dispatchTouchEvent`消費了,事件終止,不再傳遞。?
return false:?調用父ViewGroup或則Activity的`onTouchEvent`。 (不再往下傳)。①另外如果不消耗ACTION_DOWN事件,那么down,move,up事件都與該View無關,交由父類處理(父類的`onTouchEvent`方法)?
return super.dispatherTouchEvent:?則繼續往下(子View)傳遞,或者是調用當前View的onTouchEvent方法;
* public boolean onInterceptTouchEvent(MotionEvent ev)?
在`dispatchTouchEvent`內部調用,顧名思義就是判斷是否攔截某個事件。(注:ViewGroup才有的方法,View因為沒有子View了,所以不需要也沒有該方法)?
return true:?ViewGroup將該事件攔截,交給自己的`onTouchEvent`處理。②而且這一個事件序列(當前和其它事件)都只能由該ViewGroup處理,并且不會再調用該`onInterceptTouchEvent`方法去詢問是否攔截。?
return false:?繼續傳遞給子元素的`dispatchTouchEvent`處理。?
return super.dispatherTouchEvent:?事件默認不會被攔截。
* public boolean onTouchEvent(MotionEvent ev)?
在`dispatchTouchEvent`內部調用?
return true:?事件消費,當前事件終止。?
return false:?交給父View的`onTouchEvent`。?
return super.dispatherTouchEvent:?默認處理事件的邏輯和返回 false 時相同。
其實上面的關系可以用以下代碼簡單描述。
~~~
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;//是否消費事件
if(onInterceptTouchEvent(ev)){//是否攔截事件
consume = onTouchEvent(ev);//攔截了,交給自己的View處理
}else{
consume = child.dispatchTouchEvent(ev);//不攔截,就交給子View處理
}
return consume;//true:消費事件,終止。false:交給父onTouchEvent處理。并不再往下傳遞當前事件。
}
~~~
有圖有真相

有點類似[責任鏈設計模式](http://blog.csdn.net/qian520ao/article/details/73558275 "optional title")
#### 實戰講解
##### 驗證View的事件分發
* 創建CustomViewGroup繼承FrameLayout
* 創建CustomView繼承View
xml
~~~
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="qdx.viewtouchevent.MainActivity">
//最外層為activity(白色背景)
<qdx.viewtouchevent.CustomViewGroup
android:layout_width="300dp"
android:layout_height="400dp"
android:layout_gravity="right"
android:background="#84bf96">
//CustomViewGroup(綠色背景)包含CustomView(黃色背景)
<qdx.viewtouchevent.CustomView
android:layout_width="150dp"
android:layout_height="300dp"
android:layout_gravity="right"
android:background="#f2eada" />
</qdx.viewtouchevent.CustomViewGroup>
</FrameLayout>
~~~

如上圖所示,down事件由activity->ViewGroup->View,因為View并沒有處理down事件,所以事件消費情況為false,并且最后由View->ViewGroup->activity傳遞。?
##### 驗證不消耗ACTION_DOWN事件
我們再來驗證①另外如果不消耗ACTION_DOWN事件,那么down,move,up事件系列都與該View無關,交由父類處理(父類的`onTouchEvent`方法)?
根據上面文字描述,因為我們的`CustomViewGroup`和`CustomView`都沒有去處理任何事件,即當前序列的所有事件都return false,所以我們也無法接收/處理其他事件(move,up)

我們再將customView設置為可點擊狀態,即消費touch事件。`setClickable(true);`

##### 驗證 ViewGroup事件攔截
viewGroup將事件攔截后,②而且這一個事件序列(當前和其它事件)都只能由該ViewGroup處理,并且不會再調用該`onInterceptTouchEvent`方法去詢問是否攔截。

通過上面的幾個驗證,我們越來越接近真相,用通俗的話來解釋就是:
> 老板發現BUG解決,一開始是由上級往下級問話。([類似責任鏈設計模式](http://blog.csdn.net/qian520ao/article/details/73558275 "optional title"))
>
> 例如突然間出現了BUG,老板問小組A有沒有空處理一下BUG(即分發ACTION_DOWN),小組A說沒時間(return false),那么老板就不會把這個序列的BUG(ACTION_MOVE和ACTION_UP)交給小組A。如果再次出現BUG,老板還會再次詢問小組A。①
>
> 如果你舉手攬了這個BUG(即攔截),那么這一事件的BUG都交由你解決,并且相同序列的BUG老板不會問話,直接找你處理。②
#### 源碼分析ViewGroup
源碼分析這一塊主要還是基于《Android開發藝術探索》這本書的引導和理解做出的總結。PS:這本書性價比很高,涵蓋知識面廣。
##### Activity的事件分發
Activity的事件分發還關系到View的繪制和加載機制,等待下一篇來更詳細認識這個知識點。
~~~
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//最終獲取到頂級View(ViewGroup)分發事件
//(getWindow().getDecorView().findViewById(android.R.id.Content)).getChildAt(0)
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果所有的View都沒有處理事件,則由Activity親自出馬
return onTouchEvent(ev);
}
~~~
##### ViewGroup的事件攔截
~~~
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//清除FLAG_DISALLOW_INTERCEPT,并且設置mFirstTouchTarget為null
resetTouchState(){
if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
......
};
}
final boolean intercepted;//ViewGroup是否攔截事件
//mFirstTouchTarget是ViewGroup中處理事件(return true)的子View
//如果沒有子View處理則mFirstTouchTarget=null,ViewGroup自己處理
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent
ev.setAction(action);
} else {
intercepted = false;
//如果子類設置requestDisallowInterceptTouchEvent(true)
//ViewGroup將無法攔截MotionEvent.ACTION_DOWN以外的事件
}
} else {
intercepted = true;
//actionMasked != MotionEvent.ACTION_DOWN并且沒有子View處理事件,則將事件攔截
//并且不會再調用onInterceptTouchEvent詢問是否攔截
}
......
......
}
~~~
我們將上面的結論再次寫下來,方便對照。?
①另外如果不消耗ACTION_DOWN事件,那么down,move,up事件都與該View無關,交由父類處理(父類的`onTouchEvent`方法)(dispatchTouchEvent)?
②而且這一個事件序列(當前和其它事件)都只能由該ViewGroup處理,并且不會再調用該`onInterceptTouchEvent`方法去詢問是否攔截。(onInterceptTouchEvent return true)
* 首先我們分析上面第21行代碼: ViewGroup在兩種情況下會攔截事件(ACTION_DOWN || mFirstTouchTarget != null)所以反過來也就是說?I : 當ACTION_MOVE和ACTION_UP事件到來時,如果沒有子元素處理事件(mFirstTouchTarget==null),則ViewGroup的onInterceptTouchEvent不會再被調用,而且同一序列中的其它事件都會默認交給它處理(第34行 intercepted=true);與上面所說的①②呼應。
* 緊接著22行: ViewGroup`disallowIntercept`(不攔截)的判定是`FLAG_DISALLOW_INTERCEPT`標記位,這個標記是通過子View`requestDisallowInterceptTouchEvent`方法設置的。所以我們可以得出這么一個結論II : 當子View處理了ACTION_DOWN事件(mFirstTouchTarget =該子View),而且設置了FLAG_DISALLOW_INTERCEPT標記位,那么ViewGroup將無法攔截除了ACTION_DOWN以外的其它事件。(在11行代碼ACTION_DOWN時清除了FLAG_DISALLOW_INTERCEPT標記位,所以ViewGroup無論如何都可以選擇是否攔截處理ACTION_DOWN)
上面變著花樣的又一次驗證了①②個知識點,不得不說read the fuck source code讓我們可以找到一個處理滑動沖突的方法:子View處理DOWN事件并且設置`FLAG_DISALLOW_INTERCEPT`標記位,就可以不讓ViewGroup攔截DOWN以外的事件。
##### ViewGroup的事件分發
~~~
public boolean dispatchTouchEvent(MotionEvent ev) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
......
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))
{
ev.setTargetAccessibilityFocus(false);
//如果子View沒有播放動畫,而且點擊事件的坐標在子View的區域內,繼續下面的判斷
continue;
}
//判斷是否有子View處理了事件
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
//如果已經有子View處理了事件,即mFirstTouchTarget!=null,終止循環。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//點擊dispatchTransformedTouchEvent代碼發現其執行方法實際為
//return child.dispatchTouchEvent(event); (因為child!=null)
//所以如果有子View處理了事件,我們就進行下一步:賦值
......
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//addTouchTarget方法里完成了對mFirstTouchTarget的賦值
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
......
if (child == null) {
//如果沒有子View處理事件,就自己處理
handled = super.dispatchTouchEvent(event);
} else {
//有子View,調用子View的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(event);
......
return handled;
}
~~~
上面為ViewGroup對事件的分發,主要有2點
1. 如果有子View,則調用子View的dispatchTouchEvent方法判斷是否處理了事件,如果處理了便賦值mFirstTouchTarget,賦值成功則跳出循環。
2. ViewGroup的事件分發最終還是調用View的`dispatchTouchEvent`方法,具體如上代碼所述。
至此View的事件分發機制已經演練完畢,如果事件分發機制理解深入的話,那么處理滑動沖突便是手到擒來了。
### View的滑動沖突
關于View的滑動沖突我們就開門見山吧,因為上述的事件分發已經有足夠的理論知識了,我們可以單刀赴會了。


針對上圖,這個是比較普遍的滑動沖突事件,我們先拿它來開刀。
好記性不如爛筆頭,我們再次把結論搬到戰場上?
①另外如果不消耗ACTION_DOWN事件,那么down,move,up事件都與該View無關,交由父類處理(父類的`onTouchEvent`方法)(dispatchTouchEvent)?
②而且這一個事件序列(當前和其它事件)都只能由該ViewGroup處理,并且不會再調用該`onInterceptTouchEvent`方法去詢問是否攔截。(onInterceptTouchEvent return true)
I : 當ACTION_MOVE和ACTION_UP事件到來時,如果沒有子元素處理事件(mFirstTouchTarget==null),則ViewGroup的onInterceptTouchEvent不會再被調用,而且同一序列中的其它事件都會默認交給它處理(第34行 intercepted=true);
#### 外部攔截
外部攔截顧名思義就是由父ViewGroup對事件攔截處理(所以重寫`onInterceptTouchEvent`方法即可),子View只能眼巴巴的處理父View“吃剩”的事件。主要有以下幾點。
* 父類不能攔截ACTION_DOWN,也就是說必須返回false,根據上述①②和?I?可得。
* 父類在ACTION_MOVE的時候根據需求,判斷是否攔截。
* ACTION_UP事件建議返回false或者`super.onInterceptTouchEvent`,因為如果已經攔截的話,那么并不會調用`onInterceptTouchEvent`方法再次詢問。如果不攔截,而且返回true,子View可能就無法觸發onClick等相關事件。
ViewGroup : 需要重寫`onInterceptTouchEvent`,判斷是否攔截即可。?
但是有一種情況:用戶正在水平滑動(事件已攔截給ViewGroup),但是水平滑動停止前用戶再進行豎直滑動,下面代碼我用`isSolve`進行簡單的處理。
~~~
private boolean isIntercept;
private boolean isSolve;//是否完成了攔截判斷,如果決定攔截,那么同系列事件就不能設置為不攔截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
return false;//down的時候攔截后,就只能交給自己處理了
case MotionEvent.ACTION_MOVE:
if (!isSolve) {//是否已經決定攔截/不攔截?
isIntercept = (Math.abs(ev.getX() - mPointGapF.x) > Math.abs(ev.getY() - mPointGapF.y)*2);//如果是左右滑動,且水平角度小于30°,就攔截
isSolve = true;
}
return isIntercept;//如果是左右滑動,就攔截
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
scrollBy((int) (mPointGapF.x - ev.getX()), 0);
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
break;
}
return super.onTouchEvent(ev);
}
~~~
子View : 和子View沒有多大關系,只需要處理自身的移動操作即可。
~~~
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
scrollBy(0, (int) (mPointGapF.y - ev.getY()));
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
break;
}
return true;
}
~~~


#### 內部攔截
II : 當子View處理了ACTION_DOWN事件(mFirstTouchTarget =該子View),而且設置了FLAG_DISALLOW_INTERCEPT標記位,那么ViewGroup將無法攔截除了ACTION_DOWN以外的其它事件。
ViewGroup : 只需在`onInterceptTouchEvent`MotionEvent.ACTION_DOWN時候不攔截,其他時候都需要攔截,否則父類的`onTouchEvent`就不能處理任何事件了。
~~~
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;//down的時候攔截后,就只能交給自己處理了
}
return true;//如果不攔截,父類的onTouchEvent方法就無事件可以處理。
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
scrollBy((int) (mPointGapF.x - ev.getX()), 0);
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
break;
}
return super.onTouchEvent(ev);
}
~~~
子View : 需要在ACTION_DOWN事件設置getParent().requestDisallowInterceptTouchEvent(true),并且在ACTION_MOVE的時候通過判斷是否禁止父類的攔截。
~~~
private boolean isSolve;
private boolean isIntercept;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
isSolve = false;
mPointGapF.x = ev.getX();
mPointGapF.y = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (!isSolve) {
isSolve = true;
isIntercept = (Math.abs(ev.getX() - mPointGapF.x) < Math.abs(ev.getY() - mPointGapF.y) * 2);
getParent().requestDisallowInterceptTouchEvent(isIntercept);
}
break;
}
return super.dispatchTouchEvent(ev);
}
~~~
最終的效果圖和外部攔截的效果一致,這里就不再次貼出來了。?
### 總結
通過理論和實戰,更清晰的了解了事件的分發機制,從而這些理論知識使得我們更有效的處理滑動沖突事件,所以以后只要再遇見滑動沖突事件,再次鞏固View的事件分發,萬變不離其宗,定能手到擒來解決這一問題!
- 0-發現
- AndroidInterview-Q-A
- Android能讓你少走彎路的干貨整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待辦任務
- 待補充列表
- 0-未分類
- AndroidView事件分發與滑動沖突處理
- Spannable
- 事件分發機制詳解
- 1-Java
- 1-Java-01基礎
- 未歸檔
- 你應該知道的JDK知識
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默認方法
- 圖解HashMap(1)
- 圖解HashMap(2)
- 2-Android
- 2-Android-1-基礎
- View繪制流程
- 事件分發
- AndroidView的事件分發機制和滑動沖突解決
- 自定義View基礎
- 1-安卓自定義View基礎-坐標系
- 2-安卓自定義View基礎-角度弧度
- 3-安卓自定義View基礎-顏色
- 自定義View進階
- 1-安卓自定義View進階-分類和流程
- 10-安卓自定義View進階-Matrix詳解
- 11-安卓自定義View進階-MatrixCamera
- 12-安卓自定義View進階-事件分發機制原理
- 13-安卓自定義View進階-事件分發機制詳解
- 14-安卓自定義View進階-MotionEvent詳解
- 15-安卓自定義View進階-特殊形狀控件事件處理方案
- 16-安卓自定義View進階-多點觸控詳解
- 17-安卓自定義View進階-手勢檢測GestureDetector
- 2-安卓自定義View進階-繪制基本圖形
- 3-安卓自定義View進階-畫布操作
- 4-安卓自定義View進階-圖片文字
- 5-安卓自定義View進階-Path基本操作
- 6-安卓自定義View進階-貝塞爾曲線
- 7-安卓自定義View進階-Path完結篇偽
- 8-安卓自定義View進階-Path玩出花樣PathMeasure
- 9-安卓自定義View進階-Matrix原理
- 通用類介紹
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2圖文完全教程
- Dagger2最清晰的使用教程
- Dagger2讓你愛不釋手-終結篇
- Dagger2讓你愛不釋手-重點概念講解、融合篇
- dagger2讓你愛不釋手:基礎依賴注入框架篇
- 閱讀筆記
- Glide
- Google推薦的圖片加載庫Glide:最新版使用指南(含新特性)
- rxjava
- 這可能是最好的RxJava2.x入門教程完結版
- 這可能是最好的RxJava2.x入門教程(一)
- 這可能是最好的RxJava2.x入門教程(三)
- 這可能是最好的RxJava2.x入門教程(二)
- 這可能是最好的RxJava2.x入門教程(五)
- 這可能是最好的RxJava2.x入門教程(四)
- 2-Android-3-優化
- 優化概況
- 各種優化
- Android端秒開優化
- apk大小優化
- 內存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一鍵分析Android的BugReport
- 版本控制
- git
- git章節簡述
- 2-Android-5-源碼
- HandlerThread 源碼分析
- IntentService的使用和源碼分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常見圖片壓縮方式
- 3-Kotlin
- Kotlin使用筆記1-草稿
- Kotlin使用筆記2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和總結
- gradle使用筆記
- Nexus私服搭建
- 編譯提速最佳實踐
- 7-設計模式與架構
- 組件化
- 組件化探索(OKR)
- 1-參考列表
- 2-1-組件化概述
- 2-2-gradle配置
- 2-3-代碼編寫
- 2-4-常見問題
- 2-9-值得一讀
- 8-數據結構與算法
- 0臨時文件
- 漢諾塔
- 8-數據-1數據結構
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比較
- 遲到一年HashMap解讀
- 8-數據-2算法
- 1個就夠了
- Java常用排序算法(必須掌握的8大排序算法)
- 常用排序算法總結(性能+代碼)
- 必須知道的八大種排序算法(java實現)
- 9-職業
- 閱讀
- 書單
- 面試
- 面試-01-java
- Java面試題全集駱昊(上)
- Java面試題全集駱昊(下)
- Java面試題全集駱昊(中)
- 面試-02-android
- 40道Android面試題
- 面試-03-開源源碼
- Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程
- 面試-07-設計模式
- 面試-08-算法
- 面試-09-其他
- SUMMARY
- 版權說明
- temp111