手把手教你如何寫事件處理的代碼

通過事件分發之View事件處理ViewGroup事件分發和處理源碼分析這兩篇的的理論知識分析,咱們已經大體的瞭解了事件的分發處理機制,可是這並不表明你就必定能寫好事件處理的代碼。java

既然咱們有了基本功,那麼本文就經過一個案例來逐步分析事件處理的代碼如何寫,事件衝突如何解決。git

剖析事件分發的過程

爲了模擬實際狀況,我特地搞了一幅畫View各類嵌套的圖github

View嵌套圖

圖中有一個MyViewGroup,它能夠左右滑動,本文就用它來說解事件處理的代碼如何寫。markdown

後面的分析須要你們有前面兩篇文章的基礎,請務必理解清楚,不然你可能會以爲我在講天書。框架

ACTION_DOWN

因爲咱們操做的目標是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事件要如何處理,才能確保這個事情呢?必須知足下面的一個條件

  1. MyViewGroup有一個子View處理了ACTION_DOWN事件。
  2. 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_MOVE

前面處理ACTION_DOWN已經確保了ACTION_MOVE能夠順利接收,根據前面列出的2個保證條件,那麼接收ACTION_MOVE的狀況以下

  1. MyViewGroup有一個子View處理了ACTION_DOWN,那麼ACTION_MOVE將會在onInterceptTouchEvent()中被接收。
  2. MyViewGroup本身處理了ACTION_DOWN,那麼ACTION_MOVE將會在onTouchEvent()中接收到。

對於第一種狀況,其實有個限制條件,那就是子View必須容許MyViewGroup截斷事件,不然MyViewGroup將收不到ACTION_MOVE事件。若是出現這種狀況,那你得檢查子控件的代碼了是否寫的合理了。

首先討論第二種狀況,若是ACTION_MOVEonTouchEvent()中接收到,那就表明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前,須要經過MyViewGrouponInterceptTouchEvent()方法,那麼MyViewGroup要不要截斷ACTION_MOVE事件呢?其實有不少種狀況,咱們來逐一分析可行性。

有人說,既然onInterceptTouchEvent()會一直接收ACTION_MOVE事件,那能夠不截斷就直接執行滑動。表面上看MyViewGroup實現了滑動,可是在實際中可能遇到問題。假如子View也是一個滑動的控件,那麼在MyViewGroup滑動的時候,因爲沒有截斷事件,所以子View同時也會根據本身的意願去滑動,這豈不是瞎搞嗎?又或者說子View在接收ACTION_MOVE事件後,請求父View不容許截斷後續的事件,那麼MyViewGroup後續就處理不了ACTION_MOVE事件了。

通過上面的分析,有人可能會說,一不作二不休,那就直接截斷得了。我只能說,這位施主你太沖動!

反思

若是直接粗暴的截斷,萬一趕上了不是徹底垂直滑動的手勢,MyViewGroup卻在水平滑動,那豈不是尷尬了。

這時候,確定有人忍不了了,截斷也不是,不截斷也不是,你想鬧哪樣!咱們能夠變通下嘛,咱們要有條件的截斷,避免剛纔的尷尬狀況嘛,舉兩個經常使用的條件

  1. 達到滑動的臨界點
  2. 判斷手勢是水平滑動仍是垂直滑動

那麼,在onInterceptTouchEvent()方法中關因而否截斷ACTION_MOVE的框架代碼能夠這樣寫

public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 達到滑動標準就截斷,不然不截斷
                // 滑動標準以下
                // 1. 達到滑動的臨界距離
                // 2. 判斷手勢是水平滑動
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
複製代碼

ACTION_UP

咱們先來討論下,ACTION_UP會在哪裏接收到

  1. MyViewGroup處理了ACTION_DOWNACTION_UP將會在onTouchEvent()中接收到。
  2. MyViewGroup在截斷ACTION_MOVE以前,ACTION_UP將會在onInterceptTouchEvent()中接收到。
  3. 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

從前面文章分析可知,ACTION_CANCEL是在MyViewGroup的父View截斷了MyViewGroupACTION_MOVE事件後收到的,ACTION_CANCEL接收的地方其實和ACTION_UP是同樣,至因而處理仍是不處理,根據實際中有沒有作實質的動做來相應的返回true或者false

完成案例代碼

前面咱們已經對每一個事件到底處不處理進行了分析,而且寫出了事件處理的框架,那麼接下來,咱們就能夠在這個框架之下,很放心地完成MyViewGroup滑動特性的代碼了。

ACTION_DOWN

在處理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);
    }
複製代碼

mStartXmStartY表示手指按下的座標,mLastXmLastY表示最近一次事件的座標。

ACTION_MOVE

根據前面的分析,處理ACTION_MOVE有狀況有以下幾種

  1. 若是MyViewGroup存在一個子View處理了ACTION_DOWN

    1. MyViewGroup截斷ACTION_MOVE以前,ACTION_MOVE將會在onInterceptTouchEvent()中接收。
    2. MyViewGroup截斷ACTION_MOVE以後,ACTION_MOVE將會在onTouchEvent()中接收。
  2. 若是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事件,從代碼中能夠看出這個標準有兩條

  1. 水平滑動的距離要大於一個臨界值。
  2. 水平滑動的距離要大於兩倍的垂直滑動距離,這樣就排除了一些不標準的手勢。

當咱們認爲這是一次有效的滑動的時候,就要截斷後續的ACTION_MOVE事件,這就是代碼中看到的return true的緣由。

然而事情尚未完,咱們還作了一些優化動做

第一步,設置拖拽狀態。這是由於在截斷後續的ACTION_MOVE後,後續的ACTION_MOVE事件就會分發給MyViewGrouponTouchEvent(),而onTouchEvent()也要處理其餘狀況的拖拽,所以須要這個狀態判斷值。

第二步,請求父View不容許截斷後續ACTION_MOVE事件。由於MyViewGroup立刻要執行以系列的滑動動做,若是父View此時截斷了事件那確定是不合適的,所以要通知父View不要搞事情。

第三步,執行一次滑動。可能不少人不理解爲什麼要在onInterceptTouchEvent()中執行滑動動做,這個方法名義上只是用來判斷是否截斷事件的。

其實這裏是有緣由的,因爲要截斷後續的ACTION_MOVE事件,那麼此次的ACTION_MOVE事件是不會發送到MyViewGrouponTouchEvent()中的,而是把這個ACTION_MOVE事件變爲ACTION_CANCEL事件發給處理了ACTION_DOWN事件的子View。所以當前的ACTION_MOVE若是不在onInterceptTouchEvent()處理,那麼就會丟失這一次滑動處理。

截斷後續的ACTION_MOVE後,MyViewGrouponTouchEvent()會接收後續的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事件都將交給MyViewGrouponTouchEvent()處理。那麼此時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_UP事件,咱們先來預想下發生的狀況

  1. 沒有截斷ACTION_MOVE事件以前,ACTION_UP事件會先由onInterceptTouchEvent()處理。
  2. 截斷ACTION_MOVE事件以後,ACTION_UP事件會由onTouchEvent()處理。
  3. 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

ACTION_CANCEL這個事件比較特殊,按照正常流程看,是因爲父View截斷了MyViewGroupACTION_MOVE事件後,把ACTION_MOVE變爲了ACTION_CANCEL,而後發送給MyViewGroup

若是MyViewGroup在進行滑動以前,會先請求父View不容許截斷它的事件,也就是說以後父View不可能截斷ACTION_MOVE事件,也就是不可能發送ACTION_CANCEL事件。

若是MyViewGroup還沒開始滑動,那麼MyViewGroup就可能會收到ACTION_CANCEL事件,然而此時不用作任何處理動做,由於MyViewGroup尚未滑動產生狀態呢。

這是一種正常狀況下的純理論分析,不排除異常狀況。

截斷ACTION_DOWN

如今,咱們回過頭來處理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並無相應點擊事件。而後在第二個頁面返回第一個頁面時,只有滑動超過了一半的寬度,纔會自動滑動到第一頁面。

相關文章
相關標籤/搜索