1. 程式人生 > >看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!

看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!

引言

在 android 開發過程中,我們經常需要對一些手勢,如:單擊、雙擊、長按、滑動、縮放等,進行監測。這時也就引出了手勢監測的概念,所謂的手勢監測,說白了就是對於 GestureDetector 的用法的使用和注意要點的學習。注:由於縮放手勢獨有的複雜性,我打算後期將其單獨拿出來歸納總結。

像網上其他將手勢監聽的部落格一樣,本文將以雙擊事件為引子,逐步展開探討 Android 手勢監聽,你需要知道的點點滴滴,還是那句話:看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!


雙擊 666

對於一個 Android 新手而言,如果需要你實現一個雙擊功能,我們一般會怎麼想呢?

May Be

  1. 首先我們重寫 onTouchEvent 方法
  2. 當第一次點選後,咱們先判斷是否為需要監聽的控制元件
  3. 如果是則 new 一個執行緒,開始倒計時(如 1s)
  4. 如果在這個倒計時的期間,再次呼叫了點選事件
  5. 判斷成功、發生雙擊事件⌚️

But
這實在是太複雜了,你又要控制時間,又要判斷控制元件等等等等。所以,我們因該如何解決呢?手勢監聽的使用


GestureDetector 使用

我的理解是 GestureDetector 是 Android 中,專門用來進行手勢監聽的一個物件,在他的監聽器中,我們通過傳入 MotionEvents 物件,就可以在各種事件的回撥方法中各種手勢進行監測。舉個例子: GestureDetector 的 OnGestureListener 就是一種回撥方法,就是說在獲得了傳入的這個 MotionEvents 物件之後,進行了處理,我們通過重寫了其中的各種方法(單擊事件、雙擊事件等等),就可以監聽到單擊,雙擊,滑動等事件,然後直接在這些方法內部進行處理。

使用方法

  1. 首先,建立一個 SimpleOnGestureListener 回撥方法物件,並對其中各個方法進行重寫
  2. 根據這個 listener 物件,例項化出 GestureDetector 物件
  3. 對目標控制元件重寫 setOnTouchListener 方法,並在其中呼叫 detector 物件的 onTouchEvent 方法即可

簡單易懂,一分鐘搞定

    @Override
    protected void onResume() {
        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return detector.onTouchEvent(event);
            }
        });
        super.onResume();
    }

    private void iniGestureListener(){
        GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                MyToast.makeToast(GestureDetectorActivity.this, "double  click up!");
                return super.onDoubleTap(e);
            }

        detector = new GestureDetector(GestureDetectorActivity.this, listener);
    }

執行結果


Looper 註冊

如果哪位好奇的老鐵,嘗試著線上程中建立這個 detector 物件(比如下面這種)。那麼執行時就可能出現程式崩潰的情況,這是為什麼呢?

        new Thread(){
            @Override
            public void run() {
                super.run();
                detector = new GestureDetector(GestureDetectorActivity.this, listener);
            }
        }.start();

其實在 GestureDetector 被例項化時,內部會自動建立一個 Handler 用於處理資料,所以如果你在主執行緒中建立 GestureDetector,那麼這個 GestureDetector 內部建立的 Handler 會自動獲得主執行緒的 Looper。也是因此:如果你在一個沒有建立 Looper 的子執行緒中建立 GestureDetector 則需要傳遞一個帶有 Looper 的 Handler 給它,否則就會因為無法獲取到 ==Looper== 導致建立失敗。

解決

既然問題出現了,那要怎麼解決呢。既然是缺少活動中的 Looper ,那麼將活動中的 ==Looper== 傳入就是。觀察 ==detector== 的構造方法,發現其一共有種方法,其中我們常用的方法有兩種,首先是我們在主執行緒中用的那種,另外一種就是我們現在要用的,在子執行緒中,能傳入 Looper 的 構造方法:
| public GestureDetector(Context context, OnGestureListener listener) |
|--|
| GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler) |

方案一
傳入一個保有 Looper 物件的 Hander

        new Thread(){
            @Override
            public void run() {
                super.run();
                detector = new GestureDetector(GestureDetectorActivity.this, listener, new Handler(Looper.getMainLooper()));
            }
        }.start();

方案二
跟方法已一樣,只是把 Hander 拿出來單獨建立罷了

        new Thread(){
            @Override
            public void run() {
                super.run();
                Handler handler = new Handler(Looper.getMainLooper());
                detector = new GestureDetector(GestureDetectorActivity.this, listener, handler);
            }
        }.start();

方案三
在主執行緒中建立 Hander ,這樣就不用在建立 Hander 時,傳入主執行緒的 Looper

        final Handler handler = new Handler();
        new Thread(){
            @Override
            public void run() {
                super.run();
                detector = new GestureDetector(GestureDetectorActivity.this, listener, handler);
            }
        }.start();

方案四
和上面幾個方法一樣,只不過在子線稱裡提前準備好 Lopper ,這樣子線稱就和主執行緒一樣了

        new Thread(){
            @Override
            public void run() {
                super.run();
                Looper.prepare();
                detector = new GestureDetector(GestureDetectorActivity.this, listener);
            }
        }.start();

重點:手勢監聽

剛剛我們已經通過雙擊效果,講過 onDoubleTapEvent 了,那麼 GestureDetecotr 還有哪些厲害的回撥方法呢?

  1. OnDoubleTapListener :也就是雙擊事件,雙擊事件除了 onDoubleTapEvent 這個回撥方法之外,還有 SingleTapConfirmed 和 DoubleTap 這兩個回撥方法
  2. OnGestureListener :這裡集合了眾多手勢的監聽器:主要有:按下(Down)、 扔(Fling)、長按(LongPress)、滾動(Scroll)、觸控反饋(ShowPress) 和 單擊擡起(SingleTapUp)
  3. SimpleOnGestureListener :上述介面的空實現,用的頻率比較多

OnDoubleTapListener

我們先來講講 OnDoubleTapListener,大家可能要問:剛剛不是已經講過雙擊事件監聽了嗎,這裡又來不是浪費時間?廢話不說,讓我詳細介紹下這類的方法:

單擊回撥 SingleTapConfirmed

有人就會很好奇,對於單擊事件的回撥,直接去用 onClickListener 不就好了麼,幹嘛要用 SingleTapConfirmed 呢?

首先,這兩個方法是衝突的,這裡就涉及到了事件分發機制,這點我後期會專門給大家總結下,這裡就不詳解了。

其二,更具 onClickListener 的機制,我們不難發現,如果是用 onClickListener 的話,當我們雙擊時,我們也會呼叫單擊事件,也就是單擊了兩次,這明顯是不符合我們意圖的。那麼該如何呼叫呢?very easy !

        final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                MyToast.makeToast(GestureDetectorActivity.this, "single  click!");
                return super.onSingleTapConfirmed(e);
            }

            ...
        };

DoubleTap 與 onDoubleTapEvent

我打算把這兩個方法放在一起將,一則他兩都屬於雙擊的範疇,二則他兩有著極高相似和細微卻重要的區別。

大家可以嘗試著在 onTouchEvent 和 DoubleTap 中,對點選的 Down move 和 up 進行列印,你就會發現,對於 DoubleTap 而言,它是在第二次點選按下是,發生的回撥,而對於 onDoubleTapEvent 而言,則是在第二次點選後,手指擡起離開了螢幕時,發生的回撥。這就是他兩最重要的區別。

    final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "double  click down!");
            return super.onDoubleTap(e);
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            switch (e.getActionMasked()){
                case MotionEvent.ACTION_UP:
                    MyToast.makeToast(GestureDetectorActivity.this, "double  click up!");
                    break;
            }
            return super.onDoubleTapEvent(e);
        }
    };

所以,有了這兩個方法,我們就可以更具目的性的滿足兩種需求。
講到這裡,單擊雙擊事件就告一段落了,下面我們進入 OnGestureListener 的學習


OnGestureListener

這可以說是整個手勢監測中,最核心的部分了,前面都是引入,現在才是正題,這裡我主要向大家介紹一下手勢:

  1. 按下(Down)
  2. 一扔(Fling)
  3. 長按(LongPress)
  4. 滾動(Scroll)
  5. 觸控反饋(ShowPress)
  6. 單擊擡起(SingleTapUp)

onDown

onDown 事件很好理解,他在一個 View 被按下時執行。也正是如此,要想能執行 onDown ,首先要保證這個 View 是可以點選的,也就是 onClickable 的值為 true 。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){

        @Override
        public boolean onDown(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onDown");
            // 後續事件
            return super.onDown(e);
        }
    };

onFling

對於 onFling 我個人感覺這是個最常用的方法,就像它的名字,翻譯過來是拖、拽、扔的意思。舉個例子 RecyclerView 或者 ListView 我們都有用過,當我們快速上拉後會滾動一定距離停止,我們可愛的 onFling 就是用於檢測這種手勢的。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            mSpeedX = velocityX;
            mSpeedY = velocityY;
            handler.postDelayed(runnable, 30);
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };

從程式碼中,我們不難發現:該方法有四個引數

引數 意義
e1 手指按下時的 Event。
e2 手指擡起時的 Event。
velocityX 在 X 軸上的運動速度(畫素/秒)。
velocityY 在 Y 軸上的運動速度(畫素/秒)。

通過前兩個 MotionEvent 引數,我們可以獲得點擊發生的位置等,通過後兩個 float 引數,我們可以獲得手指滑動的速度。

具體使用其實還是蠻多的,比如我們可以想象下臺球遊戲,球杆擊球后,就有這樣一個初速度遞減的效果。

onLongPress

onLongPress 很簡單,就是長按事件的回撥,比如說長按複製,長按彈窗等等,它不但應用廣泛,同時使用也非常簡單,這裡就不嘮叨了

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){

        @Override
        public void onLongPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onLongPress");
            // 後續工作
            super.onLongPress(e);
        }
    };

onScroll

onScroll 方法和 onFling 很像,唯一的區別在於,onFling 的引數是滑動的速度,而 onScroll 的後兩個引數則是滑動的距離:

引數 意義
e1 手指按下時的 MotionEvent
e2 手指擡起時的 MotionEvent
distanceX 在 X 軸上劃過的距離
distanceY 在 Y 軸上劃過的距離
    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            MyToast.makeToast(GestureDetectorActivity.this, "onScroll X = " + 
                    distanceX + " Y = " + distanceY);
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
    };

onShowPress

這個方法我其實覺得作用不是很大,因為它是在 View 被點選(按下)是呼叫,其作用是給使用者一個視覺反饋,讓使用者知道我這個控制元件被點選了,這樣的效果我們完全可以用 Material design 的 ripple 實現,或者直接 drawable 寫個背景也行。

如果說它有什麼特別指出的話,它是一種延時回撥,延遲時間是 180 ms。也就是說使用者手指按下後,如果立即擡起或者事件立即被攔截,時間沒有超過 180 ms的話,這條訊息會被 remove 掉,也就不會觸發這個回撥。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public void onShowPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onShowPress");// >150ms 時呼叫
            super.onShowPress(e);
        }
    };

onSingleTapUp

對於 onSingleTapUp 網上有很多分析,但我覺得過於複雜了,其實這東西很簡單。舉個例子你就懂了:

之前我們講過雙擊事件,那好 onSingleTapUp 就是在 雙擊事件的第一次點選時回撥。也就是說但你點選了一個控制元件時(雙擊第一下),這個回撥馬上會被呼叫,然後迅速點第二下(雙擊事件的第二下),則其不會被呼叫。

型別 觸發次數 摘要
onSingleTapUp 1 在雙擊的第一次擡起時觸發
onSingleTapConfirmed 0 雙擊發生時不會觸發。
onClick 2 在雙擊事件時觸發兩次。

它和 onSingleTapConfirmed 的區別也就很明顯了,onSingleTapConfirmed 在發生雙擊時,會回撥兩次,而 onSingleTapUp 只會在雙擊的的第一次回撥。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onSingleTapUp(MotionEvent e) {// 雙擊第一次擡起觸發,第二次不觸發
            Log.d("onSingleTapUp", "onSingleTapUp");// >150ms 時呼叫
            return super.onSingleTapUp(e);
        }
    };

SimpleOnGestureListener

SimpleOnGestureListener 中包含了以上所有方法的空實現,之所以在文末再一次提及他,主要是想講下它的方便之處。

我們以監聽 OnDoubleTapListener 為例,如果想要使用 OnDoubleTapListener 介面則需要這樣進行設定:

GestureDetector detector = new GestureDetector(this, new GestureDetector
        .SimpleOnGestureListener());
detector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
    @Override public boolean onSingleTapConfirmed(MotionEvent e) {
        Toast.makeText(MainActivity.this, "onSingleTapConfirmed", Toast.LENGTH_SHORT).show();
        return false;
    }

    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_SHORT).show();
        return false;
    }

    @Override public boolean onDoubleTapEvent(MotionEvent e) {
        Toast.makeText(MainActivity.this,"onDoubleTapEvent",Toast.LENGTH_SHORT).show();
        return false;
    }
});

我們不難發現一個問題,既然在 GestureDetector 例項化時,已經例項化了一個 SimpleOnGestureListener 了,那麼在捨近求遠的去使用 OnGestureListener 的話,會多出幾個無用的空實現,顯然很浪費,所以在一般情況下,乖乖的使用 SimpleOnGestureListener 就好了。

最後

由於手勢監聽的方法有點多,大家一時難以記住,所以我打算把所有方法,在 SimpleOnGestureListener 中重寫一遍,方便大家進行查閱、記憶:

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "single click!");
            return super.onSingleTapConfirmed(e);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "double click down!");
            return super.onDoubleTap(e);
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            switch (e.getActionMasked()){
                case MotionEvent.ACTION_UP:
                    MyToast.makeToast(GestureDetectorActivity.this, "double click up!");
                    break;
            }
            return super.onDoubleTapEvent(e);
        }

        @Override
        public boolean onDown(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onDown");
            return super.onDown(e);
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            mSpeedX = velocityX;
            mSpeedY = velocityY;
            handler.postDelayed(runnable, 30);
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        @Override
        public void onShowPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onShowPress");// >150ms 時呼叫
            super.onShowPress(e);
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {// 雙擊第一次擡起觸發,第二次不觸發
            Log.d("onSingleTapUp", "onSingleTapUp");// >150ms 時呼叫
            return super.onSingleTapUp(e);
        }

        @Override
        public void onLongPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onLongPress");
            // 後續工作
            super.onLongPress(e);
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            MyToast.makeToast(GestureDetectorActivity.this, "onScroll X = " +
                    distanceX + " Y = " + distanceY);
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
        
    };

本篇部落格,是我對我學習過程的總結,所以其中難免有疏漏,希望大家能在評論區中指出,萬分感謝。

同時,如果大家有任何疑問,也可以在評論區中留言、討論,這個搓衣板跪不跪,你們說了算!