1. 程式人生 > >講講Android事件攔截機制

講講Android事件攔截機制

簡介

什麼是觸控事件?顧名思義,觸控事件就是捕獲觸控式螢幕幕後產生的事件。當點選一個按鈕時,通常會產生兩個或者三個事件——按鈕按下,這是事件一,如果滑動幾下,這是事件二,當手擡起,這是事件三。所以在Android中特意為觸控事件封裝了一個類MotionEvent,如果重寫onTouchEvent()方法,就會發現該方法的引數就是這樣的一個MotionEvent,在一般重寫觸控相關的方法中,引數一般都含有MotionEvent,可見它的重要性。

那麼MotionEvent到底是什麼東東呢,它包含了幾種型別。

  • Action_Down:手指剛接觸螢幕
  • Action_Move:手指在螢幕上移動
  • Action_Up:手指從螢幕上鬆開的一瞬間

在正常情況下,一次手指觸控式螢幕幕的行為會觸發一系列點選事件,考慮如下幾種情況:

  • 點選屏幕後離開鬆開,事件序列為Down->Up
  • 點選螢幕滑動一會再鬆開,事件序列為Down->Move->......>Move->Up

那麼,在MotionEvent裡面封裝了不少好東西,比如觸控點的座標,可以通過event.getX()方法和event.getRawX(),這兩者區別也很簡單,getX()返回的是相對於當前View左上角的x座標,getRawY()返回是相對於手機螢幕左上角的x座標,同理,y座標也是可以獲取的,getY()和getRawY()方法,MotionEvent

獲得點選事件的型別,可以通過不同的Action來進行區分,並實現不同的邏輯。

例子

如此看來,觸控事件還是簡單的,其實就是一個動作型別加座標而已。但是我們知道,Android的View結構是樹形結構,也就是說,View可以放在ViewGroup裡面,通過不同的組合來實現不同的樣式,那麼如果View放在ViewGroup裡面,這個ViewGroup又巢狀在另一個ViewGroup裡面,甚至還有可能繼續巢狀,一層層的疊加起來呢,我們先看一個例子,是通過一個按鈕點選的。

XML檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/mylayout">
    <Button
        android:id="@+id/my_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="click test"/>
</LinearLayout>

Activity檔案

public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
    private LinearLayout mLayout;
    private Button mButton;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        mLayout = (LinearLayout) this.findViewById(R.id.mylayout);
        mButton = (Button) this.findViewById(R.id.my_btn);

        mLayout.setOnTouchListener(this);
        mButton.setOnTouchListener(this);

        mLayout.setOnClickListener(this);
        mButton.setOnClickListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
        return false;
    }

    @Override
    public void onClick(View v) {
        Log.i(null, "OnClickListener--onClick--"+v);
    }
}

以上程式碼很簡單,Activity中有一個LinearLayout(ViewGroup的子類,ViewGroup是View的子類)佈局,佈局中包含一個按鈕(View的子類),然後分別對這兩個控制元件設定了Touch與Click的監聽事件,具體執行結果如下:
1,當穩穩的點選Button時

點選Button

2,當穩穩的點選除過Button以外的其他地方時: 
點選Button其他地方

3,當收指點選Button時按在Button上晃動了一下鬆開後
點選Button晃動幾下

我們看下onTouch和onClick,從引數都能看出來onTouch比onClick強大靈活,畢竟多了一個event引數。這樣onTouch裡就可以處理ACTION_DOWN、ACTION_UP、ACTION_MOVE等等的各種觸控。現在來分析下上面的列印結果;在1中,當我們點選Button時會先觸發onTouch事件(之所以列印action為0,1各一次是因為按下擡起兩個觸控動作被觸發)然後才觸發onClick事件;在2中也同理類似1;在3中會發現onTouch被多次調運後才調運onClick,是因為手指晃動了,所以觸發了ACTION_DOWN->ACTION_MOVE…->ACTION_UP。

onTouch會有一個返回值,而且在上面返回了false。你可能會疑惑這個返回值有啥效果?那就驗證一下吧,我們將上面的onTouch返回值改為ture。如下:

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
        return true;
    }

顯示結果:
onTouch返回True

此時onTouch返回true,則onClick不會被調運了。

好了,經過這個簡單的例項驗證你可以總結髮現:

  1. Android控制元件的Listener事件觸發順序是先觸發onTouch,其次onClick。
  2. 如果控制元件的onTouch返回true將會阻止事件繼續傳遞,返回false事件會繼續傳遞。

    事件流程

看上面的例子是不是有點困惑,為何OnTouch返回True,onClick就不執行,事件傳遞就中斷,在這裡需要引進一個場景,這樣解釋起來就更形象生動。

首先,請想象一下生活中常見的場景:假如你所在的公司,有一個總經理,級別最高,它下面有個部長,級別次之,最底層就是幹活的你,沒有級別。現在總經理有一個任務,總經理將這個業務佈置給部長,部長又把任務安排給你,當你完成這個任務時,就把任務反饋給部長,部長覺得這個任務完成的不錯,於是就簽了他的名字反饋給總經理,總經理看了也覺得不錯,就也簽了名字交給董事會,這樣,一個任務就順利完成了。這其實就是一個典型的事件攔截機制。

在這裡我們先定義三個類:
一個總經理—MyViewGroupA,最外層的ViewGroup

一個部長—MyViewGroupB,中間的ViewGroup

一個你—MyView,在最底層

根據以上的場景,我們可以繪製以下流程圖:
流程圖
從圖中,我們可以看到在ViewGroup中,比View多了一個方法—onInterceptTouchEvent()方法,這個是幹嘛用的呢,是用來進行事件攔截的,如果被攔截,事件就不會往下傳遞了,不攔截則繼續。

如果我們稍微改動下,如果總經理(MyViewGroupA)發現這個任務太簡單,覺得自己就可以完成,完全沒必要再找下屬,因此MyViewGroupA就使用了onInterceptTouchEvent()方法把事件給攔截了,此時流程圖:
流程圖A
我們可以看到,事件就傳遞到MyVewGroupA這裡就不繼續傳遞下去了,就直接返回。

如果我們再改動下,總經理(MyViewGroupA)委託給部長(MyViewGroupB),部長覺得自己就可以完成,完全沒必要再找下屬,因此MyViewGroupB就使用了onInterceptTouchEvent()方法把事件給攔截了,此時流程圖:
流程圖B
我們可以看到,MyViewGroupB攔截後,就不繼續傳遞了,同理如果,到乾貨的我們上(MyView),也直接返回True的話,事件也是不會繼續傳遞的,如圖:
流程圖C

原始碼

分析Android View事件傳遞機制之前有必要先看下原始碼的一些關係,如下是幾個繼承關係圖: 

原始碼1

原始碼2
看了官方這個繼承圖是不是明白了上面例子中說的LinearLayout是ViewGroup的子類,ViewGroup是View的子類,Button是View的子類關係呢?其實,在Android中所有的控制元件無非都是ViewGroup或者View的子類,說高尚點就是所有控制元件都是View的子類。

1,從View的dispatchTouchEvent方法說起

在Android中你只要觸控控制元件首先都會觸發控制元件的dispatchTouchEvent方法(其實這個方法一般都沒在具體的控制元件類中,而在他的父類View中),所以我們先來看下View的dispatchTouchEvent方法,如下:

  public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

dispatchTouchEvent的程式碼有點長,但可以挑幾個重點講講,if (onFilterTouchEventForSecurity(event))語句判斷當前View是否沒被遮住等,然後定義ListenerInfo區域性變數,ListenerInfo是View的靜態內部類,用來定義一堆關於View的XXXListener等方法;接著if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句就是重點,首先li物件自然不會為null,li.mOnTouchListener呢?你會發現ListenerInfo的mOnTouchListener成員是在哪兒賦值的呢?怎麼確認他是不是null呢?通過在View類裡搜尋可以看到:

/**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

li.mOnTouchListener是不是null取決於控制元件(View)是否設定setOnTouchListener監聽,在上面的例項中我們是設定過Button的setOnTouchListener方法的,所以也不為null,接著通過位與運算確定控制元件(View)是不是ENABLED 的,預設控制元件都是ENABLED 的,接著判斷onTouch的返回值是不是true。通過如上判斷之後如果都為true則設定預設為false的result為true,那麼接下來的if (!result && onTouchEvent(event))就不會執行,最終dispatchTouchEvent也會返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句有一個為false則if (!result && onTouchEvent(event))就會執行,如果onTouchEvent(event)返回false則dispatchTouchEvent返回false,否則返回true。

這下再看前面的例項部分明白了吧?控制元件觸控就會調運dispatchTouchEvent方法,而在dispatchTouchEvent中先執行的是onTouch方法,所以驗證了例項結論總結中的onTouch優先於onClick執行道理。如果控制元件是ENABLE且在onTouch方法裡返回了true則dispatchTouchEvent方法也返回true,不會再繼續往下執行;反之,onTouch返回false則會繼續向下執行onTouchEvent方法,且dispatchTouchEvent的返回值與onTouchEvent返回值相同

2,dispatchTouchEvent總結

在View的觸控式螢幕傳遞機制中通過分析dispatchTouchEvent方法原始碼我們會得出如下基本結論:

  1. 觸控控制元件(View)首先執行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。
  3. 如果控制元件(View)的onTouch返回false或者mOnTouchListener為null(控制元件沒有設定setOnTouchListener方法)或者控制元件不是enable的情況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回一樣。
  4. 如果控制元件不是enable的設定了onTouch方法也不會執行,只能通過重寫控制元件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回一樣。
  5. 如果控制元件(View)是enable且onTouch返回true情況下,dispatchTouchEvent直接返回true,不會呼叫onTouchEvent方法。

3,onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

首先地6到14行可以看出,如果控制元件(View)是disenable狀態,同時是可以clickable的則onTouchEvent直接消費事件返回true,反之如果控制元件(View)是disenable狀態,同時是disclickable的則onTouchEvent直接false。多說一句,關於控制元件的enable或者clickable屬性可以通過java或者xml直接設定,每個view都有這些屬性。

接著22行可以看見,如果一個控制元件是enable且disclickable則onTouchEvent直接返回false了;反之,如果一個控制元件是enable且clickable則繼續進入過於一個event的switch判斷中,然後最終onTouchEvent都返回了true。switch的ACTION_DOWN與ACTION_MOVE都進行了一些必要的設定與置位,接著到手擡起來ACTION_UP時你會發現,首先判斷了是否按下過,同時是不是可以得到焦點,然後嘗試獲取焦點,然後判斷如果不是longPressed則通過post在UI Thread中執行一個PerformClick的Runnable,也就是performClick方法。具體如下:

 public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

這個方法也是先定義一個ListenerInfo的變數然後賦值,接著判斷li.mOnClickListener是不是為null,決定執行不執行onClick。你指定現在已經很機智了,和onTouch一樣,搜一下mOnClickListener在哪賦值的唄,結果發現:

public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

控制元件只要監聽了onClick方法則mOnClickListener就不為null,而且有意思的是如果調運setOnClickListener方法設定監聽且控制元件是disclickable的情況下預設會幫設定為clickable。

4,onTouchEvent小結

  1. onTouchEvent方法中會在ACTION_UP分支中觸發onClick的監聽。
  2. 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action。

小結

通過以上總結,Android中的事件攔截機制,其實跟我們生活中的上下級委託任務很像,領導可以處理掉,也可以下發給下屬員工處理,如果員工處理的好,領導才敢給你下發任務,如果你處理不好,則領導也不敢把任務交給你,這就像在中途把下發的任務的中途攔截掉了。通過流程和原始碼的分析,相信大家能比較容易瞭解事件的分發、攔截、處理事件的流程。在弄清楚順序機制之後,再配合原始碼看,你會更加深入的理解,為什麼流程會是這樣的,最先對流程有一個大致的認識之後,再去理解,這樣就不會一頭霧摸不著頭腦,進而會有更大的學習樂趣,畢竟在學習過程中,保持好奇心是很重要的。

閱讀擴充套件