自定義View進(jìn)階《十七》——手勢檢測

Android 手勢檢測,主要是 GestureDetector 相關(guān)內(nèi)容的用法和注意事項,本文依舊屬于事件處理這一體系,部分內(nèi)容會涉及到之前文章提及過的知識點,如果你沒看過之前的文章,可以到 自定義 View 系列 來查看這些內(nèi)容。

在開發(fā) Android 手機(jī)應(yīng)用過程中,可能需要對一些手勢作出響應(yīng),如:單擊、雙擊、長按、滑動、縮放等。這些都是很常用的手勢。就拿最簡單的雙擊來說吧,假如我們需要判斷一個控件是否被雙擊(即在較短的時間內(nèi)快速的點擊兩次),似乎是一個很容易的任務(wù),但仔細(xì)考慮起來,要處理的細(xì)節(jié)問題也有不少,例如:

  1. 記錄點擊次數(shù),為了判斷是否被點擊超過 1 次,所以必須記錄點擊次數(shù)。
  2. 記錄點擊時間,由于雙擊事件是較快速的點擊兩次,像點擊一次后,過來幾分鐘再點擊一次肯定不能算是雙擊事件,所以在記錄點擊次數(shù)的同時也要記錄上一次的點擊時間,我們可以設(shè)置本次點擊距離上一次時間超過一定時間(例如:超過100ms)就不識別為雙擊事件。
  3. 點擊狀態(tài)重置,在響應(yīng)雙擊事件,或者判斷不是雙擊事件的時候要重置計數(shù)器和上一次點擊時間。重置既可以在點擊的時候判斷并進(jìn)行重新設(shè)置,也可以使用定時器等超過一定時間后重置狀態(tài)。

這樣看起來,判斷一個雙擊事件就有這么多麻煩事情,更別其他的手勢了,雖然這些看起來都很簡單,但設(shè)計起來需要考慮的細(xì)節(jié)情況實在是太多了。

那么有沒有一種更好的方法來方便的檢測手勢呢?當(dāng)然有啦,因為這些手勢很常用,系統(tǒng)早就封裝了一些方法給我們用,接下來我們就看看它們是如何使用的。

GestureDetector

GestureDetector 可以使用 MotionEvents 檢測各種手勢和事件。GestureDetector.OnGestureListener 是一個回調(diào)方法,在發(fā)生特定的事件時會調(diào)用 Listener 中對應(yīng)的方法回調(diào)。這個類只能用于檢測觸摸事件的 MotionEvent,不能用于軌跡球事件。

  • 創(chuàng)建一個 GestureDetector 實例。
  • 在onTouchEvent(MotionEvent)方法中,確保調(diào)用 GestureDetector 實例的 onTouchEvent(MotionEvent)。回調(diào)中定義的方法將在事件發(fā)生時執(zhí)行。
  • 如果偵聽 onContextClick(MotionEvent),則必須在 View 的 onGenericMotionEvent(MotionEvent)中調(diào)用 GestureDetector OnGenericMotionEvent(MotionEvent)。

GestureDetector 本身的方法比較少,使用起來也非常簡單,下面讓我們先看一下它的簡單使用示例,分解開來大概需要三個步驟。

// 1.創(chuàng)建一個監(jiān)聽回調(diào)
SimpleOnGestureListener listener = new SimpleOnGestureListener() {
    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "雙擊666", Toast.LENGTH_SHORT).show();
        return super.onDoubleTap(e);
    }
};

// 2.創(chuàng)建一個檢測器
final GestureDetector detector = new GestureDetector(this, listener);

// 3.給監(jiān)聽器設(shè)置數(shù)據(jù)源
view.setOnTouchListener(new View.OnTouchListener() {
    @Override public boolean onTouch(View v, MotionEvent event) {
        return detector.onTouchEvent(event);
    }
});

接下來我們先了解一下 GestureDetector 里面都有哪些內(nèi)容。

1. 構(gòu)造函數(shù)

GestureDetector 一共有 5 種構(gòu)造函數(shù),但有 2 種被廢棄了,1 種是重復(fù)的,所以我們只需要關(guān)注其中的 2 種構(gòu)造函數(shù)即可,如下:


第 1 種構(gòu)造函數(shù)里面需要傳遞兩個參數(shù),上下文(Context) 和 手勢監(jiān)聽器(OnGestureListener),這個很容易理解,就不再過多敘述,上面的例子中使用的就是這一種。

第 2 種構(gòu)造函數(shù)則需要多傳遞一個 Handler 作為參數(shù),這個有什么作用呢?其實作用也非常簡單,這個 Handler 主要是為了給 GestureDetector 提供一個 Looper。
在通常情況下是不需這個 Handler 的,因為它會在內(nèi)部自動創(chuàng)建一個 Handler 用于處理數(shù)據(jù),如果你在主線程中創(chuàng)建 GestureDetector,那么它內(nèi)部創(chuàng)建的 Handler 會自動獲得主線程的 Looper,然而如果你在一個沒有創(chuàng)建 Looper 的子線程中創(chuàng)建 GestureDetector 則需要傳遞一個帶有 Looper 的 Handler 給它,否則就會因為無法獲取到 Looper 導(dǎo)致創(chuàng)建失敗。

第 2 種構(gòu)造函數(shù)使用方式如下(下面是兩種在子線程中創(chuàng)建 GestureDetector 的方法):

// 方式一、在主線程創(chuàng)建 Handler
final Handler handler = new Handler();
new Thread(new Runnable() {
    @Override public void run() {
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它代碼 ...
    }
}).start();

// 方式二、在子線程創(chuàng)建 Handler,并且指定 Looper
new Thread(new Runnable() {
    @Override public void run() {
        final Handler handler = new Handler(Looper.getMainLooper());
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它代碼 ...
    }
}).start();

當(dāng)然了,使用其它創(chuàng)建 Handler 的方式也是可以的,重點傳遞的 Handler 一定要有 Looper,敲黑板,重點是 Handler 中的 Looper。假如子線程準(zhǔn)備了 Looper 那么可以直接使用第 1 種構(gòu)造函數(shù)進(jìn)行創(chuàng)建,如下:

new Thread(new Runnable() {
    @Override public void run() {
        Looper.prepare(); // <- 重點在這里
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener());
        // ... 省略其它代碼 ...
    }
}).start();

2.手勢監(jiān)聽器

既然是手勢檢測,自然要在對應(yīng)的手勢出現(xiàn)的時候通知調(diào)用者,最合適的自然是事件監(jiān)聽器模式。目前 GestureDetecotr 有四種監(jiān)聽器。

2.1 OnContextClickListener

由于 OnContextClickListener 主要是用于檢測外部設(shè)備按鈕的,關(guān)于它需要注意一點,如果偵聽 onContextClick(MotionEvent),則必須在 View 的 onGenericMotionEvent(MotionEvent)中調(diào)用 GestureDetector 的 OnGenericMotionEvent(MotionEvent)。

由于目前我們用到這個監(jiān)聽器的場景并不多,所以也就不展開介紹了,重點關(guān)注后面幾個監(jiān)聽器。

2.2 OnDoubleTapListener

這個很明顯就是用于檢測雙擊事件的,它有三個回調(diào)接口,分別是 onDoubleTap、onDoubleTapEvent 和 onSingleTapConfirmed。

2.2.1 onDoubleTap 與 onSingleTapConfirmed

如果你只想監(jiān)聽雙擊事件,那么只用關(guān)注 onDoubleTap 就行了,如果你同時要監(jiān)聽單擊事件則需要關(guān)注 onSingleTapConfirmed 這個回調(diào)函數(shù)。
有人可能會有疑問,監(jiān)聽單擊事件為什么要使用 onSingleTapConfirmed,使用 OnClickListener 不行嗎?從理論上是可行的,但是我并不推薦這樣使用,主要有兩個原因:

  1. 它們兩個是存在一定沖突的,如果你看過 事件分發(fā)機(jī)制詳解 就會知道,如果想要兩者同時被觸發(fā),則 setOnTouchListener 不能消費事件,如果 onTouchListener 消費了事件,就可能導(dǎo)致 OnClick 無法正常觸發(fā)。
  2. 需要同時監(jiān)聽單擊和雙擊,則說明單擊和雙擊后響應(yīng)邏輯不同,然而使用 OnClickListener 會在雙擊事件發(fā)生時觸發(fā)兩次,這顯然不是我們想要的結(jié)果。而使用 onSingleTapConfirmed 就不用考慮那么多了,你完全可以把它當(dāng)成單擊事件來看待,而且在雙擊事件發(fā)生時,onSingleTapConfirmed 不會被調(diào)用,這樣就不會引發(fā)沖突。

如果你需要同時監(jiān)聽兩種點擊事件可以這樣寫:

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

關(guān)于 onSingleTapConfirmed 原理也非常簡單,這一個回調(diào)函數(shù)在單擊事件發(fā)生后300ms后觸發(fā)(注意,不是立即觸發(fā)的),只有在確定不會有后續(xù)的事件后,既當(dāng)前事件肯定是單擊事件才觸發(fā) onSingleTapConfirmed,所以在進(jìn)行點擊操作時,onDoubleTap 和 onSingleTapConfirmed 只會有一個被觸發(fā),也就不存在沖突了。

2.2.2 onDoubleTapEvent

有些細(xì)心的小伙伴可能注意到還有一個 onDoubleTapEvent 回調(diào)函數(shù),它是干什么的呢?它在雙擊事件確定發(fā)生時會對第二次按下產(chǎn)生的 MotionEvent 信息進(jìn)行回調(diào)。
至于為什么要存在這樣的回調(diào),就要涉及到另一個比較細(xì)致的問題了,那就是 onDoubleTap 的觸發(fā)時間,如果你在這些函數(shù)被調(diào)用時打印一條日志,那么你會看到這樣的信息:

GCS-LOG: onDoubleTap
GCS-LOG: onDoubleTapEvent - down
GCS-LOG: onDoubleTapEvent - move
GCS-LOG: onDoubleTapEvent - move
GCS-LOG: onDoubleTapEvent - up

通過觀察這些信息你會發(fā)現(xiàn)它們的調(diào)用順序非常有趣,首先是 onDoubleTap 被觸發(fā),之后依次觸發(fā) onDoubleTapEvent 的 down、move、up 等信息,為什么說它們有趣呢?是因為這樣的調(diào)用順序會引發(fā)兩種猜想,第一種猜想是 onDoubleTap 是在第二次手指抬起(up)后觸發(fā)的,而 onDoubleTapEvent 是一種延時回調(diào)。第二種猜想則是 onDoubleTap 在第二次手指按下(dowm)時觸發(fā),onDoubleTapEvent 是一種實時回調(diào)。
通過測試和觀察源碼發(fā)現(xiàn)第二種猜想是正確的,因為第二次按下手指時,即便不抬起也會觸發(fā) onDoubleTap 和 onDoubleTapEvent 的 down,而且源碼中邏輯也表明 onDoubleTapEvent 是一種實時回調(diào)。
這就引發(fā)了另一個問題,雙擊的觸發(fā)時間,雖然這是一個細(xì)微到很難讓人注意到的問題,假如說我們想要在第二次按下抬起后才判定這是一個雙擊操作,觸發(fā)后續(xù)的內(nèi)容,則不能使用 onDoubleTap 了,需要使用 onDoubleTapEvent 來進(jìn)行更細(xì)微的控制,如下:

final GestureDetector detector = new GestureDetector(MainActivity.this, new GestureDetector.SimpleOnGestureListener() {
    @Override public boolean onDoubleTap(MotionEvent e) {
        Logger.e("第二次按下時觸發(fā)");
        return super.onDoubleTap(e);
    }

    @Override public boolean onDoubleTapEvent(MotionEvent e) {
        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_UP:
                Logger.e("第二次抬起時觸發(fā)");
                break;
        }
        return super.onDoubleTapEvent(e);
    }
});

如果你不需要控制這么細(xì)微的話,忽略即可。

2.3 OnGestureListener

這個是手勢檢測中較為核心的一個部分了,主要檢測以下類型事件:按下(Down)、 一扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊抬起(SingleTapUp)。

2.3.1 onDown
@Override public boolean onDown(MotionEvent e) {
    return true;
}

看過前面的文章應(yīng)該知道,down 在事件分發(fā)體系中是一個較為特殊的事件,為了保證事件被唯一的 View 消費,哪個 View 消費了 down 事件,后續(xù)的內(nèi)容就會傳遞給該 View。如果我們想讓一個 View 能夠接收到事件,有兩種做法

  1. 讓該 View 可以點擊,因為可點擊狀態(tài)會默認(rèn)消費 down 事件。
  2. 手動消費掉 down 事件。
    由于圖片、文本等一些控件默認(rèn)是不可點擊的,所以我們要么聲明它們的 clickable 為 true,要么在發(fā)生 down 事件是返回 true。所以 onDown 在這里的作用就很明顯了,就是為了保證讓該控件能擁有消費事件的能力,以接受后續(xù)的事件。
2.3.2 onFling

Failing 中文直接翻譯過來就是一扔、拋、甩,最常見的場景就是在 ListView 或者 RecyclerView 上快速滑動時手指抬起后它還會滾動一段時間才會停止。onFling 就是檢測這種手勢的。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
        velocityY) {
    return super.onFling(e1, e2, velocityX, velocityY);
}

在 onFling 的回調(diào)中共有四個參數(shù),分別是:


我們可以通過 e1 和 e2 獲取到手指按下和抬起時的坐標(biāo)、時間等相關(guān)信息,通過 velocityX 和 velocityY 獲取到在這段時間內(nèi)的運動速度,單位是像素/秒(即 1 秒內(nèi)滑動的像素距離)。

這個我們自己用到的地方比較少,但是也可以幫助我們簡單的做出一些有趣的效果,例如下面的這種彈球效果,會根據(jù)滑動的力度和方向產(chǎn)生不同的彈跳效果。



其實這種原理非常簡單,簡化之后如下:

  1. 記錄 velocityX 和 velocityY 作為初始速度,之后不斷讓速度衰減,直至為零。
  2. 根據(jù)速度和當(dāng)前小球的位置計算一段時間后的位置,并在該位置重新繪制小球。
  3. 判斷小球邊緣是否碰觸控件邊界,如果碰觸了邊界則讓速度反向。

根據(jù)這三條基本的邏輯就可以做出比較像的彈球效果,具體的Demo可以看這里

2.3.3 onLongPress

這個是檢測長按事件的,即手指按下后不抬起,在一段時間后會觸發(fā)該事件。

@Override 
public void onLongPress(MotionEvent e) {
}
2.3.4 onScroll

onScroll 就是監(jiān)聽滾動事件的,它看起來和 onFaling 比較像,不同的是,onSrcoll 后兩個參數(shù)不是速度,而是滾動的距離。

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 
        distanceY) {
    return super.onScroll(e1, e2, distanceX, distanceY);
}
2.3.5 onShowPress

它是用戶按下時的一種回調(diào),主要作用是給用戶提供一種視覺反饋,可以在監(jiān)聽到這種事件時可以讓控件換一種顏色,或者產(chǎn)生一些變化,告訴用戶他的動作已經(jīng)被識別。
不過這個消息和 onSingleTapConfirmed 類似,也是一種延時回調(diào),延遲時間是 180 ms,假如用戶手指按下后立即抬起或者事件立即被攔截,時間沒有超過 180 ms的話,這條消息會被 remove 掉,也就不會觸發(fā)這個回調(diào)。

@Override 
public void onShowPress(MotionEvent e) {
}
2.3.6 onSingleTapUp
@Override 
public boolean onSingleTapUp(MotionEvent e) {
    return super.onSingleTapUp(e);
}

這個也很容易理解,就是用戶單擊抬起時的回調(diào),但是它和上面的 onSingleTapConfirmed 之間有何不同呢?和 onClick 又有何不同呢?

單擊事件觸發(fā):

GCS: onSingleTapUp
GCS: onClick
GCS: onSingleTapConfirmed


雙擊事件觸發(fā):

GCS: onSingleTapUp
GCS: onClick
GCS: onDoubleTap // <- 雙擊
GCS: onClick

可以看出來這三個事件還是有所不同的,根據(jù)自己實際需要進(jìn)行使用即可

2.4 SimpleOnGestureListener

這個里面并沒有什么內(nèi)容,只是對上面三種 Listener 的空實現(xiàn),在上面的例子中使用的基本都是這監(jiān)聽器。因為它用起來更方便一點。
這主要是 GestureDetector 構(gòu)造函數(shù)的設(shè)計問題,以只監(jiān)聽 OnDoubleTapListener 為例,如果想要使用 OnDoubleTapListener 接口則需要這樣進(jìn)行設(shè)置:

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

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

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

既然都已經(jīng)創(chuàng)建 SimpleOnGestureListener 了,再創(chuàng)建一個 OnDoubleTapListener 顯然十分浪費,如果構(gòu)造函數(shù)不使用 SimpleOnGestureListener,而是使用 OnGestureListener 的話,會多出幾個無用的空實現(xiàn),顯然很浪費,所以在一般情況下,老老實實的使用 SimpleOnGestureListener 就好了。

3. 相關(guān)方法

除了各類監(jiān)聽器之外,與 GestureDetector 相關(guān)的方法其實并不多,只有幾個,下面來簡單介紹一下。


結(jié)語

關(guān)于手勢檢測部分的 GestureDetector 相關(guān)內(nèi)容基本就這么多了,其實手勢檢測還有一個 ScaleGestureDetector 也是為手勢檢測服務(wù)的,限于篇幅,本次就講這么多吧。

其實手勢檢測輔助類 GestureDetector 本身并不是很復(fù)雜,帶上注釋等內(nèi)容才不到1000行,感興趣的可以自己研究一下實現(xiàn)方式。
參考資料
文檔 · GestureDetector
源碼 · GestureDetector

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內(nèi)容