終於到了 View 這一關卡了,以前也有實踐過自定義 View:圓弧刻度溫度進度條,可是對於 View 底層的東西沒什麼瞭解,只是會用而已,抱着「知其然知其因此然」的心態,不少時候都會先去嘗試使用,而後纔來究其緣由。此次會分兩個部分來敘述本篇:事件分發機制、滑動衝突;本身自己對源碼也不熟悉,因此本篇主要是理論概述,儘可能不出現源碼的東西。java
首先要聲明這裏用來分析的對象是 MotionEvent,即點擊事件。git
所謂點擊事件的事件分發其實就是對 MotionEvent 事件的分發過程,即當一個 MotionEvent 產生了之後,系統須要把這個事件傳遞給一個具體的 View,而這個傳遞過程就是分發機制。github
瞭解了分發機制後就來了解另外一個概念,同一個事件序列:從手指接觸屏幕的那一刻起到手指離開屏幕的那一刻結束,在這個過程當中所產生的一系列事件就叫同一個事件序列。這個事件序列以 down 事件開始,中間含有 n 個 move 事件,最終以 up 事件結束。面試
知道什麼是同一個事件序列對後面的分析有很大的幫助,由於後續不少都是針對同一個事件序列來進行分析的。接下來看看點擊事件的分發過程當中三個很重要的方法:spa
public boolean dispatchTouchEvent(MotionEvent event)code
public boolean onInterceptTouchEvent(MotionEvent event)對象
public boolean onTouchEvent(MotionEvent event)事件
這三個方法的關係就以下面的僞代碼:get
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}複製代碼
經過這個僞代碼能夠很清晰的知道這三個方法之間的關係。它們的傳遞規則:對於一個根 ViewGroup 來講,點擊事件產生後,首先會傳遞給 ViewGroup 自己,此時 ViewGroup 的 dispatchTouchEvent() 方法就會被調用,接着會調用 onInterceptTouchEvent() 方法,若是 onInterceptTouchEvent() 方法返回 true 就表示 ViewGroup 要攔截當前事件;接着再調用 onTouchEvent() 方法來處理該事件;可是若是 onInterceptTouchEvent() 方法返回 false 就表示 ViewGroup 不攔截當前事件,此時 ViewGroup 的 onTouchEvent() 方法就不會被調用,而是調用子 View 的 dispatchTouchEvent() 方法,如此反覆直到事件最終被處理。源碼
一般來講那三個方法的執行流程就如上所說的,可是還會有一些比較特殊的狀況,好比設置 OnTouchListener、OnClickListener。
當一個 View 須要處理事件時,若是 View 設置了 OnTouchListener,那麼 OnTouchListener 中的 onTouch() 方法就會被調用。若是 onTouch() 方法返回 false,則當前 View 的 onTouchEvent() 方法就會被調用;若是返回 true,當前 View 的 onTouchEvent() 方法將不會被調用。
在 onTouchEvent() 方法中,若是有設置 OnClickListener,那麼 onClick() 方法是必定會被調用的。
曾經見過一道面試題,詳細描述記不清了,大概意思是這樣的:onTouch() 和 onTouchEvent() 誰先執行 ?有評論者說若是對 View 進行過比較深刻的瞭解是接觸不到這些的。
那麼經過前面幾段敘述,對事件分發機制應該有個比較清晰的理解了,還有如下幾點須要注意:
在界面中只要內外兩層同時能夠滑動,這個時候就會產生滑動衝突。常見的滑動衝突場景能夠簡單的分爲如下三種:
- 外部滑動方向和內部滑動方向不一致;
- 外部滑動方向和內部滑動方向一致;
- 上面兩種狀況的嵌套。
場景一: 主要是 ViewPager 和 Fragment 配合使用所組成的頁面滑動效果。在這種效果中能夠經過 ViewPager 提供的左右滑動來切換頁面,而每一個頁面內部每每又是一個 RecyclerView。按說這種效果是會出現滑動衝突的,但卻不用咱們去處理,緣由是 ViewPager 內部已經處理了滑動衝突,因此咱們使用的時候無須關注這個問題。
場景二: 若是在一個 ScrollView 中嵌套一個 RecyclerView,那麼內外兩層都在同一個方向能夠滑動,當手指開始滑動的時候就會出現問題,由於系統不知道咱們想要滑動的是哪一層。同理,若是內外兩層均可以在左右方向滑動也會出現這種狀況。
場景三: 場景三是場景一和場景二兩種狀況的嵌套,這種狀況更爲複雜。
比較常見的滑動衝突就是上面那三種,那麼該怎麼來解決呢?其實能夠根據滑動類型是水平滑動仍是豎直滑動來判斷由誰來攔截事件。至於如何判斷滑動類型就有比較多的方式了:能夠依據滑動路徑和水平方向所造成的夾角;也能夠依據水平方向和豎直方向上的距離差來判斷 … 。
滑動類型已經肯定了,接下來就是肯定滑動的接收者,到底是誰來響應這個滑動類型?下面介紹兩種具體的解決方法:
所謂外部攔截就是指點擊事件都先通過父容器的攔截處理,若是父容器須要該事件就攔截,不然就不攔截而是交給子容器。外部攔截須要重寫父容器的 onInterceptTouchEvent() 方法,在內部作響應的攔截便可。這種方法的僞代碼以下:
public boolean oonInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器須要當前點擊事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}複製代碼
在 onInterceptTouchEvent() 方法中,首先是 ACTION_DOWN 事件,父容器必須返回 false,即不攔截 ACTION_DOWN 事件,這是由於一旦父容器攔截了 ACTION_DOWN 事件,那麼後續的 ACTION_MOVE 和 ACTION_UP 事件都會直接交由父容器處理,此時事件就不能傳遞給子元素了;其次是 ACTION_MOVE 事件,這個事件能夠根據須要來決定是否攔截,若是父容器須要攔截就返回 true,不然就返回 false;最後是 ACTION_UP 事件,這裏必須返回 false,由於 ACTION_UP 事件自己沒有太多意義。
內部攔截是指父容器默認不攔截任何事件,全部的事件都傳遞給子元素,若是子元素須要此事件就直接消耗處理掉,不然就返回給父容器進行處理。這種方法和外部攔截恰好相反,須要配合 requestDisallowInterceptTouchEvent() 方法才能正常工做,此時須要重寫子元素的 dispatchTouchEvent() 方法。僞代碼以下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotinEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器須要此類點擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}複製代碼
使用內部攔截除了須要對子元素進行修改之外,父元素也要修改成攔截除了 ACTION_DOWN 之外的其餘事件。事件父元素所作的修改以下:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}複製代碼
爲何父元素不能攔截 ACTION_DOWN 事件呢?那是由於經過 requestDisallowInterceptTouchEvent() 方法會自動設置一個 FLAG_DISALLOW_INTERCEPT 標記,該標記會致使父元素沒法攔截除了 ACTION_DOWN 之外的其餘點擊事件,而 ACTION_DOWN 事件會重置 FLAG_DISALLOW_INTERCEPT 標記,使之無效。
目前所理解的 View 事件分發機制和滑動衝突就這麼多了,只是一些理論概述,接下來還須要好好的實踐一番才能進一步掌握,同時也才能發現一些細節上的問題。