1. 程式人生 > 實用技巧 >View事件傳遞機制

View事件傳遞機制

理解事件傳遞的基本邏輯,對於工作過程中解決滑動事件衝突非常有幫助。比如我們此時有一個父控制元件ViewPager,這個ViewPager其中一個Item是ScrollView,此時會發生什麼問題呢?當ViewPager滑動到ScrollView這個條目的時候,再左右滑動,發現ViewPager再也左右滑動不了了。這是為什麼呢?我們結合圖6一起來分析一下。


  1.我們都知道ViewPager是能夠橫向滑動的控制元件,而ScrollView是縱向滑動的控制元件,當Down事件產生的時候,此時會由ViewPager傳遞給ScrollView,ViewPager沒有對Down事件攔截,ScrollView也不會對這個Down事件進行攔截,所以事件就會傳遞給ScrollView的孩子,也就是類似於圖6中的子View,子View如果沒有對Down事件響應,那麼最後會到ScrollView中的onTouchEvent,而ScrollView的onTouchEvent對於這個Down事件返回了true,代表ScrollView消費了這個Down事件。


  2.接下來開始滑動手指,產生一系列的Move事件。Move事件也是由ViewPager傳遞給ScrollView。由於Down事件是被ScrollView的onTouchEvent中消費的,所以Move事件就不會傳遞給ScrollView的子控制元件了。一系列的Move事件也是在ScrollView的onTouchEvent中被執行。


  3.最後的Up事件也是由ScrollView中的onTouchEvent消費。


  從上述1至3的步驟中,我們看出來無論是Down事件、Move事件還是Up事件,最後全部都是被ScrollView所消費。從頭到尾ViewPager的onTouchEvent都沒有得到執行。而ViewPager之所以能夠左右滑動,正是因為ViewPager的onTouchEvent裡面的程式碼邏輯產生的效果。ViewPager的onTouchEvent沒有執行,這個ViewPager當然就不能夠左右滑動了。所以解決上述問題,就是在於如何讓ViewPager中的onTouchEvent方法執行。

我們可以自定義一個MyViewPager繼承ViewPager,重寫onInterceptTouchEvent方法,如果我們在onInterceptTouchEvent方法中直接野蠻地return一個true,此時就代表無論是Down事件、Move事件,還是Up事件,全部都攔截下來了,攔截在MyViewPager中,我們可以認為是圖6中的ViewGroupB,既然攔截下來了所有事件,那麼所有事件就會傳遞到MyViewPager的onTouchEvent,所以此時,這個MyViewPager一定可以左右滑動。

  但是,由此會引發另外一個問題,就是這個ScrollView不能上下滑動了。這又是為什麼呢?因為ScrollView能夠上下滑動的程式碼邏輯在ScrollView中的onTouchEvent方法內,而此時事件又全部被MyViewPager攔截了下來,ScrollView完全得不到事件,onTouchEvent方法得不到執行,自然不能上下滑動。所以我們需要修改MyViewPager中的onInterceptTouchEvent的邏輯。


  ViewPager只對左右滑動感興趣,而ScrollView對上下滑動這個動作感興趣,所以我們只需要在MyViewPager的onInterceptTouchEvent中,根據多個Move事件,判斷是左右滑動還是上下滑動,如果是左右滑動,return true將事件攔截下來,如果是上下滑動,return false將事件傳遞給ScrollView,這樣就能解決問題了。
所以,對於Down事件,我們一般都不進行攔截,判斷是否攔截得根據一些列的Move事件才能得出具體的條件是否成立。
Cancel事件的產生:

  剛才我們說了事件一般有三個,Down、Move、Up,這三個事件比較好理解。其實還有一種事件就是Cancel事件。它代表什麼含義呢?
還是回到圖6,如果一個Down事件產生了,這個Down事件從ViewGroupA傳遞到ViewGroupB,最終到達子View,被子View的onTouchEvent消費,return了true,那麼此時Down事件就終止了。接下來後續的Move事件也會從ViewGroupA傳遞給ViewGroupB,也就是說ViewGroupA和ViewGroupB會比子View更先拿到Move事件,那既然ViewGroupA和ViewGroupB比子View更先拿到Move事件,那麼他們當中的任何一個都有可能在某一個Move事件中,把這個Move事件給攔截下來,一旦Move事件被攔截下來了,子View肯定就拿不到這個Move事件了,不過,此時子View會產生一個新的事件,就是Cancel事件。


  所以一個正常的事件序列是 Down→Move→Up,這樣才被認為是一個正常的事件序列。如果一個View響應的Down事件,可是卻被沒有正常結尾,Move事件或者Up事件被攔截了,此時非正常結尾的情況就會給子View產生一個新的事件Cancel。


  子控制元件可以影響父控制元件是否攔截的行為
  子控制元件是可以干預父控制元件是否攔截事件的結果。通過在子View中dispatchTouchEvent中增加一行程式碼即可。getParent().requestDisallowInterceptTouchEvent(true);這行程式碼就可以請求父控制元件不要攔截事件。


  很多人可能不太明白這句話的意思,既然事件一定是先到達父控制元件,然後才到達子View,那也就是getParent().requestDisallowInterceptTouchEvent(true);這句話是在父控制元件是否攔截判斷結束之後才呼叫,怎麼能改變父控制元件是否攔截的結果呢,這裡存在一個執行先後順序的疑惑。
  

  其實是這樣的,getParent().requestDisallowInterceptTouchEvent(true);達到的效果不是修改父控制元件對本次事件是否攔截的結果,而影響的是後續事件。比如子View在Down事件中呼叫了getParent().requestDisallowInterceptTouchEvent(true);這行程式碼,那麼在後續Move事件、Up事件產生到達父控制元件的時候,父控制元件就不會再攔截了。所以getParent().requestDisallowInterceptTouchEvent(true);只會影響Move事件和Up事件,影響不到Down事件。

內容引自如下

深入淺出解析Android事件傳遞機制

事件傳遞順序
硬體 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View
事件傳遞中主要的三個方法dispatchTouchEvent() onInterceptTouchEvent() 和 onTouchEvent()


##View的滑動衝突

當我們內外兩層View都可以滑動時候,就會產生滑動衝突。滑動衝突有兩種形式,內外兩層滑動方向不一致和滑動一致。不管是哪種形式,我們只需要根據我們的邏輯考慮什麼時候需要外層View處理滑動,什麼時候需要內層View處理滑動即可。

滑動衝突處理的方式有兩種,外部攔截法和內部攔截法。

####外部攔截法
外部攔截法是父View根據需要對事件進行攔截。邏輯處理放在父View的onInterceptTouchEvent方法中。我們只需要重寫父View的onInterceptTouchEvent方法,並根據邏輯需要做相應的攔截即可。

根據業務邏輯需要,在ACTION_MOVE方法中進行判斷,如果需要父View處理則返回true,否則返回false,事件分發給子View去處理。
ACTION_DOWN 一定返回false,不要攔截它,否則根據View事件分發機制,後續ACTION_MOVE 與 ACTION_UP事件都將預設交給父View去處理
ACTION_UP也需要返回false,如果返回true,並且滑動事件交給子View處理,那麼子View將接收不到ACTION_UP事件,子View的onClick事件也無法觸發
外部攔截法子View不需要做任何處理

####內部攔截法
內部攔截法父View攔截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中呼叫getParent().requestDisallowInterceptTouchEvent(true)方法接管事件並在ACTION_MOVE中根據業務邏輯決定事件是否教給父View處理。如需交給父View處理則呼叫requestDisallowInterceptTouchEvent(false)方法。內部攔截法不符合事件分發流程,是通過子VIew反向控制父View攔截。虛擬碼如下:

/**
* 內部攔截法
* 父View需攔截除DOWN以外的其他事件
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
} else {
return true;
}
}

//子View.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 MotionEvent.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);
}

//父View.onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent event) {

int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
上述程式碼是內部攔截的典型程式碼,當面對不同的滑動策略時只需要修改裡面的條件即可,其他不需要做改動而且也不能有改動。

父元素要預設攔截除了ACTION_DOWN以外的其他事件
子元素呼叫parent.requestDisallowInterceptTouchEvent(false/true)來控制父元素是否攔截事件
父元素不能攔截ACTION_DOWN因為它不受FLAG_DISALLOW_INTERCEPT標誌位控制,一旦父容器攔截ACTION_DOWN那麼所有的事件都不會傳遞給子View

內容引自如下:

Android View的事件傳遞及滑動衝突

1.事件傳遞優先順序:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。

標題
標題
標題



另外參考文章

圖解Android View的事件傳遞