# 事件分發機制詳解
在上一篇文章 [事件分發機制原理][dispatch-touchevent-theory] 中簡要分析了一下事件分發機制的原理,原理是十分簡單的,一句話就能總結:**責任鏈模式,事件層層傳遞,直到被消費。** 雖然原理簡單,但是隨著 Android 不斷的發展,實際運用場景也越來越復雜,所以想要徹底玩轉事件分發機制還需要一定技巧,本篇事件分發機制詳解將帶大家了解 ...
> **你以為我接下來要講源碼?**
> 我就不按套路,所有的源碼都是為了適應具體的應用場景而寫的,只要能夠理解運用場景,理解源碼也就十分簡單了。所以本篇的核心問題是:**正確理解在實際場景中事件分發機制的作用。** 會涉及到源碼,但不是主角。
**注意:本文中所有源碼分析部分均基于 API23(Android 6.0) 版本,由于安卓系統源碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。**
## 常見事件
既然是事件分發,總要有事件才能分發吧,所以我們先了解一下常見的幾種事件。
根據面向對象思想,事件被封裝成 MotionEvent 對象,由于本篇重點不在于此,所以只會涉及到幾個與手指觸摸相關的常見事件:
| 事件 | 簡介 |
| ------------- | ------------------------- |
| ACTION_DOWN | 手指 **初次接觸到屏幕** 時觸發。 |
| ACTION_MOVE | 手指 **在屏幕上滑動** 時觸發,會會多次觸發。 |
| ACTION_UP | 手指 **離開屏幕** 時觸發。 |
| ACTION_CANCEL | 事件 **被上層攔截** 時觸發。 |
對于單指觸控來說,一次簡單的交互流程是這樣的:
**手指落下(ACTION_DOWN) -> 移動(ACTION_MOVE) -> 離開(ACTION_UP)**
> * 本次事例中 ACTION_MOVE 有多次觸發。
> * 如果僅僅是單擊(手指按下再抬起),不會觸發 ACTION_MOVE。

## 事件分發、攔截與消費
關于這一部分內容,上一篇文章 [事件分發機制原理][dispatch-touchevent-theory] 已經將流程整理的比較清楚了,本文會深入細節來研究這些內容。之所以分開講,是為了防止大家被細節所迷惑而忽略了整體邏輯。
> `√` 表示有該方法。
>
> `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`**。

下面我們來看一下實際測試結果:
> 手指按下,不移動,稍等片刻再抬起。
```shell
[Listener?]: onTouchListener ACTION_DOWN
[GcsView??]: onTouchEvent ACTION_DOWN
[Listener?]: onLongClickListener
[Listener?]: onTouchListener ACTION_UP
[GcsView??]: onTouchEvent ACTION_UP
[Listener?]: onClickListener
```
可以看到,測試結果也支持我們猜測的結論,因為長按 onLongClickListener 不需要 ACTION_UP 所以會在 ACTION_DOWN 之后就觸發。
接下來就看一下源碼是怎么設計的(省略了大量無關代碼):
```java
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false; // result 為返回值,主要作用是告訴調用者事件是否已經被消費。
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
/**
* 如果設置了OnTouchListener,并且當前 View 可點擊,就調用監聽器的 onTouch 方法,
* 如果 onTouch 方法返回值為 true,就設置 result 為 true。
*/
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/**
* 如果 result 為 false,則調用自身的 onTouchEvent。
* 如果 onTouchEvent 返回值為 true,則設置 result 為 true。
*/
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
```
> **如果覺得源碼還是太長,那么用偽代碼實現應當是這樣的(省略若干安全判斷),簡單粗暴:**
>
> ```java
> public boolean dispatchTouchEvent(MotionEvent event) {
> if (mOnTouchListener.onTouch(this, event)) {
> return true;
> } else if (onTouchEvent(event)) {
> return true;
> }
> return false;
> }
> ```
正當你沉迷在源碼的"精妙"邏輯的時候,你可能沒發現有兩個東西失蹤了,等回過神來,定睛一看,哎呦媽呀,**OnClick 和 OnLongClick 去哪里了?**
不要擔心,OnClick 和 OnLongClick 的具體調用位置在 **onTouchEvent** 中,看源碼(同樣省略大量無關代碼):
```java
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
// 檢查各種 clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
removeLongPressCallback(); // 移除長按
...
performClick(); // 檢查單擊
...
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0); // 檢測長按
...
break;
...
}
return true; // ??表示事件被消費
}
return false;
}
```
> **注意了,第一個重點要出現了(敲黑板)!**
>
> 
>
> **注意上面代碼中存在一個 `return true;` 并且是只要 View 可點擊就返回 true,就表示事件被消費了。**
>
> 舉個栗子: I have a **RelativeLayout**,I have a **View**,Ugh,**RelativeLayout - View**
>
> ```xml
> <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 的事件分發流程又是如何的呢?
上一篇文章 [事件分發機制原理][dispatch-touchevent-theory] 中我們了解到事件是通過ViewGroup一層一層傳遞的,最終傳遞給 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有權決定是否告訴要告訴 ChildView。在默認的情況下 ViewGroup 事件分發流程是這樣的。
* 1.判斷自身是否需要(詢問 onInterceptTouchEvent 是否攔截),如果需要,調用自己的 onTouchEvent。
* 2.自身不需要或者不確定,則詢問 ChildView ,一般來說是調用手指觸摸位置的 ChildView。
* 3.如果子 ChildView 不需要則調用自身的 onTouchEvent。
用偽代碼應該是這樣的:
```java
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默認狀態為沒有消費過
if (!onInterceptTouchEvent(ev)) { // 如果沒有攔截交給子View
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件沒有被消費,詢問自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}
```
**有人看到這里可能會有疑問,我看過源碼,ViewGroup 的 `dispatchTouchEvent` 可有二百多行呢,你弄這幾行就想忽悠我,別以為我讀書少。**
當然了,上述源碼是不完善的,還有很多問題是沒有解決的,例如:
##### 1. ViewGroup 中可能有多個 ChildView,如何判斷應該分配給哪一個?
這個很容易,就是把所有的 ChildView 遍歷一遍,如果手指觸摸的點在 ChildView 區域內就分發給這個View。
##### 2. 當該點的 ChildView 有重疊時應該如何分配?
當 ChildView 重疊時,**一般會分配給顯示在最上面的 ChildView**。
如何判斷哪個是顯示在最上面的呢?后面加載的一般會覆蓋掉之前的,所以**顯示在最上面的是最后加載的**。
如下:
```xml
<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="#BDDA66"
android:layout_width="200dp"
android:layout_height="200dp"/>
</RelativeLayout>
```

當手指點擊有重疊區域時,分如下幾種情況:
1. 只有 View1 可點擊時,事件將會分配給 View1,即使被 View2 遮擋,這一部分仍是 View1 的可點擊區域。
2. 只有 View2 可點擊時,事件將會分配給 View2。
3. View1 和 View2 均可點擊時,事件會分配給后加載的 View2,View2 將事件消費掉,View1接收不到事件。
**注意:**
* 上面說的是可點擊,可點擊包括很多種情況,只要你給View注冊了 `onClickListener、onLongClickListener、OnContextClickListener` 其中的任何一個監聽器或者設置了 `android:clickable="true"` 就代表這個 View 是可點擊的。
另外,某些 View 默認就是可點擊的,例如,Button,CheckBox 等。
* 給 View 注冊 `OnTouchListener` 不會影響 View 的可點擊狀態。即使給 View 注冊 `OnTouchListener` ,**只要不返回 `true` 就不會消費事件**。
##### 3. ViewGroup 和 ChildView 同時注冊了事件監聽器(onClick等),哪個會執行?
事件優先給 ChildView,會被 ChildView消費掉,ViewGroup 不會響應。
##### 4. 所有事件都應該被同一 View 消費
在上面的例子中我們分析后可以了解到,同一次點擊事件只能被一個 View 消費,這是為什呢?主要是為了防止事件響應混亂,如果再一次完整的事件中分別將不同的事件分配給了不同的 View 容易造成事件響應混亂。
> ( View 中 onClick 事件需要同時接收到 ACTION_DOWN 和 ACTION_UP 才能觸發,如果分配給了不同的 View,那么 onClick 將無法被正確觸發)。
**安卓為了保證所有的事件都是被一個 View 消費的,對第一次的事件( ACTION_DOWN )進行了特殊判斷,View 只有消費了 ACTION_DOWN 事件,才能接收到后續的事件(可點擊控件會默認消費所有事件),并且會將后續所有事件傳遞過來,不會再傳遞給其他 View,除非上層 View 進行了攔截。**
**如果上層 View 攔截了當前正在處理的事件,會收到一個 ACTION_CANCEL,表示當前事件已經結束,后續事件不會再傳遞過來。**
**源碼:**
> 其實如果能夠理解上面的內容,不看源碼也能非常順利的使用事件分發,但源碼中能挖掘出更多的內容。
```java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 調試用
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 判斷事件是否是針對可訪問的焦點視圖(很晚才添加的內容,個人猜測和屏幕輔助相關,方便盲人等使用設備)
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 處理第一次ACTION_DOWN.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 清除之前所有的狀態
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 檢查是否需要攔截.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 詢問是否攔截
ev.setAction(action); // 恢復操作,防止被更改
} else {
intercepted = false;
}
} else {
// 沒有目標來處理該事件,而且也不是一個新的事件事件(ACTION_DOWN), 進行攔截。
intercepted = true;
}
// 判斷事件是否是針對可訪問的焦點視圖
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 檢查事件是否被取消(ACTION_CANCEL).
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果沒有取消也沒有被攔截 (進入事件分發)
if (!canceled && !intercepted) {
// 如果事件是針對可訪問性焦點視圖,我們將其提供給具有可訪問性焦點的視圖。
// 如果它不處理它,我們清除該標志并像往常一樣將事件分派給所有的 ChildView。
// 我們檢測并避免保持這種狀態,因為這些事非常罕見。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清除此指針ID的早期觸摸目標,防止不同步。
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex); // 獲取觸摸位置坐標
final float y = ev.getY(actionIndex);
// 查找可以接受事件的 ChildView
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// ▼注意,從最后向前掃描
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// 如果有一個視圖具有可訪問性焦點,我們希望它首先獲取事件,
// 如果不處理,我們將執行正常的分派。
// 盡管這可能會分發兩次,但它能保證在給定的時間內更安全的執行。
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 檢查View是否允許接受事件(即處于顯示狀態(VISIBLE)或者正在播放動畫)
// 檢查觸摸位置是否在View區域內
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// getTouchTarget 中判斷了 child 是否包含在 mFirstTouchTarget 中
// 如果有返回 target,如果沒有返回 null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// ChildView 已經準備好接受在其區域內的事件。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break; // ??已經找到目標View,跳出循環
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 沒有找到 ChildView 接收事件
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 分發 TouchTarget
if (mFirstTouchTarget == null) {
// 沒有 TouchTarget,將當前 ViewGroup 當作普通的 View 處理。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 分發TouchTarget,如果我們已經分發過,則避免分配給新的目標。
// 如有必要,取消分發。
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 如果需要,更新指針的觸摸目標列表或取消。
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
```
## 核心要點
1. **事件分發原理: 責任鏈模式,事件層層傳遞,直到被消費。**
2. **View 的 `dispatchTouchEvent` 主要用于調度自身的監聽器和 onTouchEvent。**
3. **View的事件的調度順序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。**
4. **不論 View 自身是否注冊點擊事件,只要 View 是可點擊的就會消費事件。**
5. **事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。**
6. **ViewGroup 中可能有多個 ChildView 時,將事件分配給包含點擊位置的 ChildView。**
7. **ViewGroup 和 ChildView 同時注冊了事件監聽器(onClick等),由 ChildView 消費。**
8. **一次觸摸流程中產生事件應被同一 View 消費,全部接收或者全部拒絕。**
9. **只要接受 ACTION_DOWN 就意味著接受所有的事件,拒絕 ACTION_DOWN 則不會收到后續內容。**
10. **如果當前正在處理的事件被上層 View 攔截,會收到一個 ACTION_CANCEL,后續事件不會再傳遞過來**。
## 總結
本文啰嗦了這么多內容,但真正需要注意的就是核心要點中的幾個概念,只要能正確理解這些概念,相信理解事件分發機制將再也不是難題。
> 最后,個人推薦閱讀源碼的方法,先嘗試用自己的角度去分析,建立概念,然后看源碼進行驗證、對比,如果發現自己建立的概念有問題,就嘗試修正自己的概念,這樣比較容易理解原作者的意圖,也不容易被眾多的代碼所迷惑。
>
> 就像 ViewGroup 中的 dispatchTouchEvent 內容非常多,主要是為了應對實際的場景,里面有很多 安全判斷,處理多指觸控 等內容,這些如果不先建立概念就去看源碼很容易被這些細節問題所迷惑。
## 參考資料
[View ](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java)
[ViewGroup.java](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)
[Android Touch事件分發詳解](https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/Android%20Touch%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E8%AF%A6%E8%A7%A3.md)
[基于源碼來了解Android的事件分發機制](http://minjie.tech/2016/09/03/%E5%9F%BA%E4%BA%8E%E6%BA%90%E7%A0%81%E6%9D%A5%E4%BA%86%E8%A7%A3Android%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%9C%BA%E5%88%B6/)
## About Me
### 作者微博: <a href="http://weibo.com/GcsSloop" target="_blank">@GcsSloop</a>
<a href="http://www.gcssloop.com/info/about" target="_blank"><img src="http://ww4.sinaimg.cn/large/005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg" width="300" style="display:inline;" /></a>
[dispatch-touchevent-theory]: http://www.gcssloop.com/customview/dispatch-touchevent-theory "事件分發機制原理-GcsSloop"
- 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