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

閱讀擴展

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard代碼混淆
3,講講Handler+Looper+MessageQueue關系
4,Android圖片加載庫理解
5,談談Android運行時權限理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大組件之 " Activity "
10,Android 四大組件之" Service "
11,Android 四大組件之“ BroadcastReceiver "
12,Android 四大組件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命周期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 性能優化
23,Android 消息機制
24,Android Bitmap相關
25,Android 線程和線程池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸摸事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 插件化思考
32,開發人員必備技能——單元測試


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM