通過事件分發之View事件處理和ViewGroup事件分發和處理源碼分析這兩篇的的理論知識分析,咱們已經大體的瞭解了事件的分發處理機制,可是這並不表明你就必定能寫好事件處理的代碼。java
既然咱們有了基本功,那麼本文就經過一個案例來逐步分析事件處理的代碼如何寫,事件衝突如何解決。git
爲了模擬實際狀況,我特地搞了一幅畫View各類嵌套的圖github
圖中有一個MyViewGroup
,它能夠左右滑動,本文就用它來說解事件處理的代碼如何寫。markdown
後面的分析須要你們有前面兩篇文章的基礎,請務必理解清楚,不然你可能會以爲我在講天書。框架
因爲咱們操做的目標是MyViewGroup
,所以我會把手指在MyViewGroup
內容區域內按下,至於按在哪裏,其實無所謂,甚至在TextView
上也行。此時系統會把ACTION_DOWN
事件通過Activity傳遞給ViewGroup0
,那麼問題來了ViewGroup0
會不會截斷事件呢?ide
若是ViewGroup0
截斷了ACTION_DOWN
事件,那麼它的全部子View在這個事件序列結束前,將沒法接收到任何事件,包括ACTION_DOWN
事件。MyViewGroup
就是ViewGroup0
的子View,很顯然咱們並不但願這樣的事情發生。若是真的發生從一開始就截斷ACTION_DOWN
這樣的事情,那父View控件的代碼寫的絕壁有問題。oop
事件序列是由
ACTION_DOWN
開始,由ACTION_UP
或者ACTION_CANCEL
結束,而且中間有0個或者多個ACTION_MOVE
組成。源碼分析
那麼有沒有截斷ACTION_DOWN
事件的狀況呢?固然有,ViewGroup
必須處於一個合理的狀態,而且有理由截斷ACTION_DOWN
事件。例如ViewPager
,當手指在屏幕快速劃事後,頁面還處於滑動狀態,此時若是手指再次按下,ViewPager
把這個ACTION_DOWN
事件當作是中止滑動當前滑動而且從新開始滑動的指示,所以它有理由截斷這個ACTION_DOWN
事件。post
那麼,ViewGroup0
在沒有任何合理狀態,而且尚未任何合理理由的狀況下,是毫不會截斷ACTION_DOWN
事件的,所以它會把這個事件傳遞給MyViewGroup
。測試
MyViewGroup
很高興接收到了第一個事件ACTION_DOWN
,按照剛纔講的規則,常規狀態下,是不截斷ACTION_DOWN
事件的,可是若是MyViewGroup
在滑動狀態中,而且手指已經離開屏幕,當再次按下手指的時候,我但願MyViewGroup
截斷ACTION_DOWN
事件的,所以onInterceptTouchEvent()
方法的事件處理的框架代碼應該這樣寫
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 1. 若是處於無狀態,默認不截斷 // 2. 若是處於滑動狀態,截斷事件 break; } return super.onInterceptTouchEvent(ev); } 複製代碼
如今討論的是事件處理的框架代碼如何寫,所以沒有具體的代碼。
你確定覺得ACTION_DOWN
事件就這樣處理完了是吧,機智的我早已看穿一切
MyViewGroup
是須要實現滑動特性的,那麼它就必需要能接收到ACTION_MOVE
事件。那麼ACTION_DOWN
事件要如何處理,才能確保這個事情呢?必須知足下面的一個條件
MyViewGroup
有一個子View處理了ACTION_DOWN
事件。MyViewGroup
本身處理ACTION_DOWN
事件。第一個條件呢,是最理想的狀況,由於MyViewGroup
在這種狀況下,不用處理ACTION_DOWN
事件就能夠接收到ACTION_MOVE
事件。
然而第一個條件,是不可控的,所以咱們要作好最壞的打算,那就是MyViewGroup
本身處理ACTION_DOWN
。所以,在onTouchEvent()
中處理ACTION_DOWN
事件要返回true
。
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 本身處理ACTIOND_DOWN,必須返回true return true; } return super.onTouchEvent(event); } 複製代碼
前面處理ACTION_DOWN
已經確保了ACTION_MOVE
能夠順利接收,根據前面列出的2個保證條件,那麼接收ACTION_MOVE
的狀況以下
MyViewGroup
有一個子View處理了ACTION_DOWN
,那麼ACTION_MOVE
將會在onInterceptTouchEvent()
中被接收。MyViewGroup
本身處理了ACTION_DOWN
,那麼ACTION_MOVE
將會在onTouchEvent()
中接收到。對於第一種狀況,其實有個限制條件,那就是子View必須容許
MyViewGroup
截斷事件,不然MyViewGroup
將收不到ACTION_MOVE
事件。若是出現這種狀況,那你得檢查子控件的代碼了是否寫的合理了。
首先討論第二種狀況,若是ACTION_MOVE
在onTouchEvent()
中接收到,那就表明MyViewGroup
要本身處理事件來滑動,所以返回true
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: // 本身處理ACTION_MOVE,返回true return true; } return super.onTouchEvent(event); } 複製代碼
如今來繼續看第一種狀況,ACTION_MOVE
在發送給處理了ACTION_DOWN
的子View前,須要經過MyViewGroup
的onInterceptTouchEvent()
方法,那麼MyViewGroup
要不要截斷ACTION_MOVE
事件呢?其實有不少種狀況,咱們來逐一分析可行性。
有人說,既然onInterceptTouchEvent()
會一直接收ACTION_MOVE
事件,那能夠不截斷就直接執行滑動。表面上看MyViewGroup
實現了滑動,可是在實際中可能遇到問題。假如子View也是一個滑動的控件,那麼在MyViewGroup
滑動的時候,因爲沒有截斷事件,所以子View同時也會根據本身的意願去滑動,這豈不是瞎搞嗎?又或者說子View在接收ACTION_MOVE
事件後,請求父View不容許截斷後續的事件,那麼MyViewGroup
後續就處理不了ACTION_MOVE
事件了。
通過上面的分析,有人可能會說,一不作二不休,那就直接截斷得了。我只能說,這位施主你太沖動!
若是直接粗暴的截斷,萬一趕上了不是徹底垂直滑動的手勢,MyViewGroup
卻在水平滑動,那豈不是尷尬了。
這時候,確定有人忍不了了,截斷也不是,不截斷也不是,你想鬧哪樣!咱們能夠變通下嘛,咱們要有條件的截斷,避免剛纔的尷尬狀況嘛,舉兩個經常使用的條件
那麼,在onInterceptTouchEvent()
方法中關因而否截斷ACTION_MOVE
的框架代碼能夠這樣寫
public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_MOVE: // 達到滑動標準就截斷,不然不截斷 // 滑動標準以下 // 1. 達到滑動的臨界距離 // 2. 判斷手勢是水平滑動 break; } return super.onInterceptTouchEvent(ev); } 複製代碼
咱們先來討論下,ACTION_UP
會在哪裏接收到
MyViewGroup
處理了ACTION_DOWN
,ACTION_UP
將會在onTouchEvent()
中接收到。MyViewGroup
在截斷ACTION_MOVE
以前,ACTION_UP
將會在onInterceptTouchEvent()
中接收到。MyViewGroup
截斷ACTION_MOVE
後,ACTION_UP
將會在onTouchEvent()
中接收到。第一種狀況,返回true
吧,由於畢竟是MyViewGroup
本身處理了ACTION_UP
事件。
第二種狀況,返回false
吧,由於此時MyViewGroup
尚未處理滑動事件呢。
第三種狀況,返回true
吧,由於畢竟是MyViewGroup
本身處理了ACTION_UP
事件。
從源碼角度看,對於
ACTION_UP
事件的處理的返回值,好像並不過重要。 可是返回true
仍是false
實際上是向父View代表一個種態度,那就是我究竟是不是處理了ACTION_UP
事件。
從前面文章分析可知,ACTION_CANCEL
是在MyViewGroup
的父View截斷了MyViewGroup
的ACTION_MOVE
事件後收到的,ACTION_CANCEL
接收的地方其實和ACTION_UP
是同樣,至因而處理仍是不處理,根據實際中有沒有作實質的動做來相應的返回true
或者false
。
前面咱們已經對每一個事件到底處不處理進行了分析,而且寫出了事件處理的框架,那麼接下來,咱們就能夠在這個框架之下,很放心地完成MyViewGroup
滑動特性的代碼了。
在處理ACTION_DOWN
的時候要作啥呢?固然是記錄手指按下時的座標。因爲ACTION_DOWN
必定會通過onInterceptTouchEvent()
,因此在這裏記錄按下座標
public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); float y = ev.getY(); switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 記錄手指按下的座標 mLastX = mStartX = x; mLastY = mStartY = y; // 1. 若是處於無狀態,默認不截斷 // 2. 若是處於滑動狀態,截斷事件 break; } return super.onInterceptTouchEvent(ev); } 複製代碼
mStartX
和mStartY
表示手指按下的座標,mLastX
和mLastY
表示最近一次事件的座標。
根據前面的分析,處理ACTION_MOVE
有狀況有以下幾種
若是MyViewGroup
存在一個子View處理了ACTION_DOWN
,
MyViewGroup
截斷ACTION_MOVE
以前,ACTION_MOVE
將會在onInterceptTouchEvent()
中接收。MyViewGroup
截斷ACTION_MOVE
以後,ACTION_MOVE
將會在onTouchEvent()
中接收。若是MyViewGroup
處理了ACTION_DOWN
,那麼ACTION_MOVE
將會在onTouchEvent()
中接收。
第一種狀況,根據前面的分析,咱們將在onInterceptTouchEvent()
根據條件來截斷ACTION_MOVE
事件。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); float y = ev.getY(); switch (ev.getActionMasked()) { case MotionEvent.ACTION_MOVE: // 計算從手指按下時滑動的距離 float distanceX = Math.abs(x - mStartX); float distanceY = Math.abs(y - mStartY); if (distanceX > mScaledTouchSlop && distanceX > 2 * distanceY) { // 設置拖拽狀態 setState(SCROLLING_STATE_DRAGGING); // 不容許父View截斷後續事件 requestDisallowIntercept(true); // 執行一次拖拽的滑動 performDrag(x); // 更新最新事件座標 mLastX = x; mLastY = y; // 截斷後續的事件 return true; } break; } return super.onInterceptTouchEvent(ev); } 複製代碼
根據咱們的分析,要達到截斷ACTION_MOVE
的標準才截斷後續的ACTION_MOVE
事件,從代碼中能夠看出這個標準有兩條
當咱們認爲這是一次有效的滑動的時候,就要截斷後續的ACTION_MOVE
事件,這就是代碼中看到的return true
的緣由。
然而事情尚未完,咱們還作了一些優化動做
第一步,設置拖拽狀態。這是由於在截斷後續的ACTION_MOVE
後,後續的ACTION_MOVE
事件就會分發給MyViewGroup
的onTouchEvent()
,而onTouchEvent()
也要處理其餘狀況的拖拽,所以須要這個狀態判斷值。
第二步,請求父View不容許截斷後續ACTION_MOVE
事件。由於MyViewGroup
立刻要執行以系列的滑動動做,若是父View此時截斷了事件那確定是不合適的,所以要通知父View不要搞事情。
第三步,執行一次滑動。可能不少人不理解爲什麼要在onInterceptTouchEvent()
中執行滑動動做,這個方法名義上只是用來判斷是否截斷事件的。
其實這裏是有緣由的,因爲要截斷後續的ACTION_MOVE
事件,那麼此次的ACTION_MOVE
事件是不會發送到MyViewGroup
的onTouchEvent()
中的,而是把這個ACTION_MOVE
事件變爲ACTION_CANCEL
事件發給處理了ACTION_DOWN
事件的子View。所以當前的ACTION_MOVE
若是不在onInterceptTouchEvent()
處理,那麼就會丟失這一次滑動處理。
截斷後續的ACTION_MOVE
後,MyViewGroup
的onTouchEvent()
會接收後續的ACTION_MOVE
,那麼在這裏要繼續執行滑動
public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: if (mState == SCROLLING_STATE_DRAGGING) { // 處於滑動狀態就繼續執行滑動 performDrag(x); mLastX = x; } return true; } return super.onTouchEvent(event); } 複製代碼
至此,處理ACTION_MOVE
的第一種狀況已經處理完畢,咱們如今來看下第二種狀況,那就是MyViewGroup
處理了ACTION_DOWN
,全部的ACTION_MOVE
事件都將交給MyViewGroup
的onTouchEvent()
處理。那麼此時MyViewGroup
尚未滑動,所以須要再次判斷是否達到滑動標準
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: if (mState == SCROLLING_STATE_DRAGGING) { // 處於滑動狀態就繼續執行滑動 performDrag(x); // 更新最新座標點 mLastX = x; } else { // 不處於滑動狀態,就再次檢測是否達滑動標準 float distanceX = Math.abs(x - mLastX); float distanceY = Math.abs(y - mLastY); if (distanceX > mScaledTouchSlop && distanceX > 2 * distanceY) { setState(SCROLLING_STATE_DRAGGING); requestDisallowIntercept(true); performDrag(x); mLastX = x; } } return true; } return super.onTouchEvent(event); } 複製代碼
對於ACTION_UP
事件,咱們先來預想下發生的狀況
ACTION_MOVE
事件以前,ACTION_UP
事件會先由onInterceptTouchEvent()
處理。ACTION_MOVE
事件以後,ACTION_UP
事件會由onTouchEvent()
處理。MyViewGroup
處理了ACTION_DOWN
事件,ACTION_UP
事件所有會由onTouchEvent()
處理。第一種狀況,因爲MyViewGroup
尚未產生滑動,所以不須要處理此種狀況下手指擡起事件。
第二種狀況,MyViewGroup
已經產生滑動,若是MyViewGroup
是一個像ViewPager
同樣的頁面式的滑動,那麼當手指擡起時,它須要進行一些頁面定位操做,也就是決定滑動到哪一個頁面。
第三種狀況,其實就是第一種狀況和第二種狀況的綜合版而已。
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: if (mState == SCROLLING_STATE_DRAGGING) { setState(SCROLLING_STATE_SETTING); // 使用Scroller進行定位操做 int contentWidth = getWidth() - getHorizontalPadding(); int scrollX = getScrollX(); int targetIndex = (scrollX + contentWidth / 2) / contentWidth; mScroller.startScroll(scrollX, 0, targetIndex * contentWidth - scrollX, 0); invalidate(); } return true; } return super.onTouchEvent(event); } 複製代碼
ACTION_CANCEL
這個事件比較特殊,按照正常流程看,是因爲父View截斷了MyViewGroup
的ACTION_MOVE
事件後,把ACTION_MOVE
變爲了ACTION_CANCEL
,而後發送給MyViewGroup
。
若是MyViewGroup
在進行滑動以前,會先請求父View不容許截斷它的事件,也就是說以後父View不可能截斷ACTION_MOVE
事件,也就是不可能發送ACTION_CANCEL
事件。
若是MyViewGroup
還沒開始滑動,那麼MyViewGroup
就可能會收到ACTION_CANCEL
事件,然而此時不用作任何處理動做,由於MyViewGroup
尚未滑動產生狀態呢。
這是一種正常狀況下的純理論分析,不排除異常狀況。
如今,咱們回過頭來處理MyViewGroup
截斷ACTION_DOWN
的狀況,前面咱們說過,若是手指擡起,MyViewGroup
仍是處於滑動狀態,在咱們這個例子中叫作定位狀態,那麼當手指按下時,就須要截斷事件,由於MyViewGroup
認爲這個時候的按下動做是爲了中止當前滑動,並用手指控制滑動
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); float y = ev.getY(); switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 重置狀態 setState(SCROLLING_STATE_IDLE); // 記錄手指按下的座標 mLastX = mStartX = x; mLastY = mStartY = y; // 1. 若是處於無狀態,默認不截斷 // 2. 若是處於滑動狀態,截斷事件 if (!mScroller.isFinished()) { // 中止定位動做 mScroller.abortAnimation(); // 設置拖拽狀態 setState(SCROLLING_STATE_DRAGGING); // 不容許父View截斷後續事件 requestDisallowIntercept(true); return true; } break; } return super.onInterceptTouchEvent(ev); } 複製代碼
當MyViewGroup
截斷ACTION_DOWN
事件後,那麼後續的的ACTION_MOVE
事件就由onTouchEvent()
來進行滑動處理,這個過程在前面已經實現。
本文先從理論上搭建了事件處理的框架,而後用一個簡單的例子實現了這個框架。若是你們在看本文的時候有任何疑問,請先參考前面兩篇文章的分析,若是仍是有疑問,歡迎在評論裏留言討論。
詳細源碼請參考github,實現的效果以下
爲了測試,我在第一個頁面放置了一個Button
,而後點擊Button
開始滑動,能夠看到Button
並無相應點擊事件。而後在第二個頁面返回第一個頁面時,只有滑動超過了一半的寬度,纔會自動滑動到第一頁面。