Android手勢監(jiān)聽GestureDetector和ScaleGestureDetector

在Android開發(fā)中,可能需要實現(xiàn)一些手勢監(jiān)聽相關(guān)的功能,如:單擊、雙擊、長按、滑動、縮放等。這些都是很常用的手勢。手勢監(jiān)聽,還是遵循事件分發(fā)和處理的原理。

GestureDetector

首先我們來簡單了解下,實現(xiàn)雙擊手勢監(jiān)聽需要注意的細(xì)節(jié)
1 記錄點擊事件的時間,雙擊事件是指快速點擊兩次觸發(fā)的事件,記錄點擊發(fā)生的時間,就可以判斷兩次點擊發(fā)生的間隔,如果太長肯定不能被視為雙擊事件。
2 記錄點擊事件的次數(shù),雙擊事件包含兩次點擊,所以需要判斷是否已經(jīng)有過一次點擊。
3 點擊狀態(tài)重置,在判斷雙擊事件時,不管是否滿足觸發(fā)條件。都要將點擊事件的計數(shù)器和上一次點擊的時間重置。如果觸發(fā)了雙擊事件,那計數(shù)器次數(shù)要歸零。如果未觸發(fā),計數(shù)器應(yīng)該以本次點擊作為雙擊事件的第一次點擊,重新加入后續(xù)判斷。
這是我們自定義實現(xiàn)雙擊事件監(jiān)聽的思路。

Android系統(tǒng)也封裝了GestureDetector,用來實現(xiàn)手勢監(jiān)聽。它使用事件分發(fā)機制中的MotionEvents來監(jiān)測各種手勢和事件。
GestureDetector類中包含三個監(jiān)聽器接口,OnGestureListener,OnDoubleTapListener和OnContextClickListener。

  • OnContextClickListener,它是在Android6.0(API 23)才添加的一個選項,是用于檢測外部設(shè)備上的按鈕是否按下的,例如藍(lán)牙觸控筆上的按鈕,一般情況下,忽略即可。
  • OnDoubleTapListener,用來監(jiān)聽雙擊事件。有三個回調(diào)方法:onDoubleTap,onDoubleTapEvent,onSingleTapConfirm。
  • OnGestureListener,手勢檢測,主要有以下類型事件:按下(Down)、 一扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊抬起(SingleTapUp)。
  • SimpleOnGestureListener,是包含了以上三個接口的所有監(jiān)聽事件的實現(xiàn)類,它是一個空實現(xiàn),實際使用中,我們需要繼承這個類,并重新需要監(jiān)聽的方法。在創(chuàng)建GestureDetector對象時,可以直接傳SimpleOnGestureListener對象,就不用再去單獨設(shè)置OnDoubleTapListener和OnContextClickListener。
    創(chuàng)建GestureDetector一共有5個構(gòu)造函數(shù),其中有兩個已經(jīng)廢棄,還有一個是重復(fù)。主要值得關(guān)注的是兩個
GestureDetector(Context context, GestureDetector.OnGestureListener listener)
GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler)

第二種相比第一種,多了一個Handler參數(shù),這個Handler對象主要是為了給GestureDetector提供一個Looper。
如果我們是在主線程中創(chuàng)建GestureDetector對象,那么就用第一個構(gòu)造函數(shù)即可,因為此時GestureDetector對象會在內(nèi)部自動創(chuàng)建一個Handler對對象,這個Handler對象會獲取主線程的Looper。然后如果是在一個沒有創(chuàng)建Looper的子線程中創(chuàng)建GestureDetector對象,它內(nèi)部自動創(chuàng)建的Handler,無法獲取到當(dāng)前線程的Looper就會導(dǎo)致創(chuàng)建失敗。

Can't create handler inside thread that has not called Looper.prepare()

如果要在子線程中創(chuàng)建GestureDetector實例,有兩種方式
一種是將主線程中創(chuàng)建的Handler對象作為上面第二個構(gòu)造方法的參數(shù),創(chuàng)建GestureDetector的實例

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

另一種是在用第一個構(gòu)造方法創(chuàng)建之前,將線程實例化為Looper。

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

OnDoubleTapListener

OnDoubleTapListener有三個回調(diào)方法,onDoubleTap,onDoubleTapEvent與onSimgleTapConfirmed。
onDoubleTap和onDOubleTapEvent的區(qū)別。如下圖所示。onDoubleTap是在雙擊事件的第二次點擊事件序列的ACTION_DOWN事件中觸發(fā)的。而onDoubleTapEvent事件是在第二次點擊事件序列的每一個事件中都會觸發(fā)。


image.png

onSingleTapConfirmed和onClick的區(qū)別。這兩個回調(diào)方法都是監(jiān)聽單擊事件。區(qū)別在于,onCLick的回調(diào)沒有延遲,而且是在點擊事件序列的ACTION_UP事件觸發(fā)。onSingleTapConfirmed是由點擊事件序列的ACTION_DOWN事件觸發(fā),并且有300ms的延遲,主要是為了確認(rèn)有沒有第二次點擊,是不是雙擊事件。


image.png
  • 需要同時監(jiān)聽單擊和雙擊,則說明單擊和雙擊后響應(yīng)邏輯不同,然而使用 OnClickListener 會在雙擊事件發(fā)生時觸發(fā)兩次,這顯然不是我們想要的結(jié)果。而使用 onSingleTapConfirmed 就不用考慮那么多了,你完全可以把它當(dāng)成單擊事件來看待,而且在雙擊事件發(fā)生時,onSingleTapConfirmed 不會被調(diào)用,這樣就不會引發(fā)沖突。
  • 如果是在子線程中創(chuàng)建的GestureDetector對象,而且關(guān)聯(lián)的Looper不是主線程的Looper,將無法觸發(fā)onSingleTapConfirmed方法。
  • 如果控件設(shè)置了onTouchListner,并且onTouch方法返回true,則表示onTouchEvent方法不會觸發(fā),自然onCLick方法也不會觸發(fā),但是onSIngleTapConfirmed還是可以觸發(fā)。
imageview.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("onTouch","MotionEvent = "+event.getAction());
                detector.onTouchEvent(event);
                scaleDetector.onTouchEvent(event);
                return true;
            }
        });
image.png

OnGestureListener

這個是手勢檢測中較為核心的一個部分,主要檢測以下類型事件:按下(Down)、 一扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊抬起(SingleTapUp)。
onDown
監(jiān)聽ACTION_DOWN事件。這個方法的特殊意義在于,在事件分發(fā)機制中,通常同一個事件序列中的ACTION_DOWN事件被哪個控件處理(消費)了,那該事件序列的后續(xù)事件也由這個控件來處理(消費)。而如果onTown方法返回true,即表示ACTION_DOWN事件被消費掉了。這樣的用途是,讓一些默認(rèn)不可點擊的控件如ImageView和TextView,具備了消費事件序列的能力。
onFling
Fling 中文直接翻譯過來就是一扔、拋、甩,最常見的場景就是在 ListView 或者 RecyclerView 上快速滑動時手指抬起后它還會滾動一段時間才會停止。onFling 就是檢測這種手勢的。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
        velocityY) {
    return super.onFling(e1, e2, velocityX, velocityY);
}
  • e1,fling手勢事件序列開始時的ACTION_DOWN事件
  • e2,fling手勢事件序列當(dāng)前的ACTION_MOVE事件
  • velocityX,fling手勢當(dāng)前在水平方向上的移動速度,單位是每秒多少像素。
  • velocityY,fling手勢當(dāng)前在垂直方向上的移動速度,單位是每秒多少像素。
    onLongPress
    長按事件監(jiān)聽,比較簡單。
    onScroll
    監(jiān)聽滾動事件。和onFling比較像。不同的是,onScroll方法的后面兩個參數(shù)不是速度,而是滾動的距離。
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 
        distanceY) {
    return super.onScroll(e1, e2, distanceX, distanceY);
}

onShowPress
產(chǎn)生ACTION_DOWN但是沒有產(chǎn)生ACTION_MOVE和ACTION_UP的情況下觸發(fā)。用途是在用戶按下時,可以產(chǎn)生視覺反饋。如改變控件的背景色或邊框顏色。

@Override 
public void onShowPress(MotionEvent e) {
}

不過這個監(jiān)聽和 onSingleTapConfirmed 類似,也是一種延時回調(diào),延遲時間是 180 ms,假如用戶手指按下后立即抬起或者事件立即被攔截,時間沒有超過 180 ms的話,也就不會觸發(fā)這個回調(diào)。
onSingleTapUp
這個也很容易理解,就是用戶單擊抬起時的回調(diào)。當(dāng)同時監(jiān)聽onSIngleTapUp、onClick、OnSingleTapConfirmed時,他們的觸發(fā)順序的:onSIngleTapUp->onClick->onSingleTapConfirmed。值得注意的是,雙擊事件的第二次點擊的ACTION_UP事件不會觸發(fā)onSingleTapUp。

ScaleGestureDetector

縮放手勢需要用到的機會比較少,它最常見于以下的一些應(yīng)用場景中,例如:圖片瀏覽,圖片編輯(貼圖效果)、網(wǎng)頁縮放、地圖、文本閱讀(通過縮放手勢調(diào)整文字大小)等。縮放手勢相對比較簡單,網(wǎng)絡(luò)上也能查到不少非官方實現(xiàn)的縮放手勢計算方案,但部分非官方的方案確實有所局限,例如只支持兩個手指的計算,在出現(xiàn)超過兩個手指時,只計算了前兩個手指的移動,這樣顯然是不合理的。而ScaleGestureDetector輕松的應(yīng)對了多個手指的情況。

ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener)
ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener, Handler handler)

和GestureDetector一樣,他也有兩個構(gòu)造方法,其中上面第二個構(gòu)造方法的Handler參數(shù)的用途也和GestureDetector中的一樣。
ScaleGestureDetector只有一個監(jiān)聽器接口,OnScaleGestureListener。SimpleOnScaleGestureListener是該接口的空實現(xiàn)類。有三個回調(diào)方法:

  • onScaleBegin,在縮放手勢開始時回調(diào)。縮放手勢開始,當(dāng)兩個手指放在屏幕上的時候會調(diào)用該方法(只調(diào)用一次)。如果返回 false 則表示不處理當(dāng)前這次縮放手勢。
  • onScale,縮放手勢過程中回調(diào)。縮放被觸發(fā)(會調(diào)用0次或者多次),如果返回 true 則表示當(dāng)前縮放事件已經(jīng)被處理,檢測器會重新積累縮放因子,返回 false 則會繼續(xù)積累縮放因子。
  • onScaleEnd,在縮放手勢結(jié)束時回調(diào)。
    以下是ScaleGestureDetector的簡單用法
scaleDetector = new ScaleGestureDetector(this,
            new ScaleGestureDetector.SimpleOnScaleGestureListener(){
                @Override
                public boolean onScale(ScaleGestureDetector detector) {
                    float xFactor = detector.getCurrentSpanX()/detector.getPreviousSpanX();
                    float yFactor = detector.getCurrentSpanY()/detector.getPreviousSpanY();
                    Log.e("onScale","xFactor = " + xFactor+",yFactor = " + yFactor);
                    Log.e("onScale","scaleFactor = " + detector.getScaleFactor());
                    return super.onScale(detector);
                }
                @Override
                public boolean onScaleBegin(ScaleGestureDetector detector) {
                    return super.onScaleBegin(detector);
                }
                @Override
                public void onScaleEnd(ScaleGestureDetector detector) {
                    super.onScaleEnd(detector);
                }
            });
基本原理

上面的代碼演示了,ScaleGestureDetector的用法是很簡單的,它的實現(xiàn)原理,也并不復(fù)雜。對縮放手勢的監(jiān)聽,我們需要關(guān)心的兩個重要因素:一是縮放的中心點,二是縮放比例。

  • 計算縮放手勢的中心點。不管是兩點觸屏還是多點觸屏,計算中心點坐標(biāo)的方式都是將所有點的x坐標(biāo)和y坐標(biāo)分別相加,然后取平均值。
public boolean onTouchEvent(MotionEvent event) {
......
......
if (inAnchoredScaleMode()) {
            // In anchored scale mode, the focal pt is always where the double tap
            // or button down gesture started
            focusX = mAnchoredScaleStartX;
            focusY = mAnchoredScaleStartY;
            if (event.getY() < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } else {
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) continue;
                sumX += event.getX(i);
                sumY += event.getY(i);
            }

            focusX = sumX / div;
            focusY = sumY / div;
        }
......
......
}
  • 計算縮放比例。就是計算各個點到中心點的平均距離,用當(dāng)前的平均距離除以此次手勢移動前的平均距離。以此計算出縮放比例。
// 計算到焦點的平均距離
float devSumX = 0, devSumY = 0;
for (int i = 0; i < count; i++) {
    if (skipIndex == i) continue;
    devSumX += Math.abs(event.getX(i) - focusX);
    devSumY += Math.abs(event.getY(i) - focusY);
}
final float devX = devSumX / div;
final float devY = devSumY / div;

final float spanX = devX * 2;
final float spanY = devY * 2;
final float span;
if (inAnchoredScaleMode()) {
    span = spanY;
} else {
    // 相當(dāng)于 sqrt(x*x + y*y)
    span = (float) Math.hypot(spanX, spanY);
}

ScaleGestureDetector的onTouchEvent方法會監(jiān)聽,當(dāng)用戶移動的距離超過一定數(shù)值(數(shù)值大小由系統(tǒng)定義)后,會觸發(fā) onScaleBegin 方法,如果用戶在 onScaleBegin 方法里面返回了 true,表示接受事件后,就會重置縮放相關(guān)數(shù)值,并且開始積累縮放比例。

// mSpanSlop 和 mMinSpan 都是從系統(tǒng)里面取得的預(yù)定義數(shù)值,該數(shù)值實際上影響的是縮放的靈敏度。
// 不過該參數(shù)并沒有提供設(shè)置的方法,如果對靈敏度不滿意的話,則需要自定義一個ScaleGestureDetector的子類, 并且修改其中的數(shù)值。
final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
if (!mInProgress && span >=  minSpan &&
        (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
    mPrevSpanX = mCurrSpanX = spanX;
    mPrevSpanY = mCurrSpanY = spanY;
    mPrevSpan = mCurrSpan = span;
    mPrevTime = mCurrTime;
    mInProgress = mListener.onScaleBegin(this);
}

監(jiān)聽縮放手勢的移動,回調(diào)onScale方法

if (action == MotionEvent.ACTION_MOVE) {
    mCurrSpanX = spanX;
    mCurrSpanY = spanY;
    mCurrSpan = span;

    boolean updatePrev = true;

    if (mInProgress) {
        // 注意這里,用戶的返回值決定了是否重新計算縮放比例
        updatePrev = mListener.onScale(this);
    }

    // 如果用戶返回了 true ,就會重新計算縮放比例
    if (updatePrev) {
        mPrevSpanX = mCurrSpanX;
        mPrevSpanY = mCurrSpanY;
        mPrevSpan = mCurrSpan;
        mPrevTime = mCurrTime;
    }
}

以上就是ScaleGestureDetector實現(xiàn)縮放手勢監(jiān)聽的原理介紹,推薦去看一下源碼,源碼的邏輯也非常簡潔明了,相信看完之后也就能夠徹底理解它的原理了。

本文參考:
http://www.gcssloop.com/customview/gestruedector
http://www.gcssloop.com/customview/scalegesturedetector

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

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