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