札記:android手勢識別功能實(shí)現(xiàn)(利用MotionEvent)
摘要
本文是手勢識別輸入事件處理的完整學(xué)習(xí)記錄。內(nèi)容包括輸入事件InputEvent響應(yīng)方式,觸摸事件MotionEvent的概念和使用,觸摸事件的動(dòng)作分類、多點(diǎn)觸摸。根據(jù)案例和API分析了觸摸手勢Touch Gesture的識別處理的一般過程。介紹了相關(guān)的GestureDetector,Scroller和VelocityTracker。最后分析drag和scale等一些手勢的識別。
輸入源分類
雖然android本身是一個(gè)完整的系統(tǒng),它主要運(yùn)行在移動(dòng)設(shè)備的特性決定了我們在它上面開的app絕大數(shù)屬于客戶端程序,主要目標(biāo)就是顯示界面處理交互,這點(diǎn)和web前端以及桌面上的應(yīng)用類似。
作為“客戶端程序”,編寫的大部分功能就是處理用戶交互。不同系統(tǒng)(對應(yīng)不同設(shè)備)可支持的用戶交互各有不同。
android可以運(yùn)行在多種設(shè)備,從交互輸入上看, InputDevice.SOURCE_CLASS_xxx
常量標(biāo)識了sdk所支持的幾種不同輸入源的設(shè)備。有:觸屏,物理/虛擬按鍵,搖桿,鼠標(biāo)等,下面的討論針對最廣泛的交互——觸屏( SOURCE_TOUCHSCREEN)。
觸屏設(shè)備從交互設(shè)計(jì)上看就是各種手勢,有點(diǎn)擊,雙擊,滑動(dòng),拖拽,縮放等等交互定義,本質(zhì)上它們都是基礎(chǔ)的幾種觸摸事件的不同模式的組合。
在安卓觸屏系統(tǒng)中,支持單點(diǎn)、多點(diǎn)(點(diǎn)通常就是手指)觸摸,每個(gè)點(diǎn)有按下,移動(dòng)和抬起。
觸屏交互的處理分不同觸屏操作——手勢的識別,然后是根據(jù)業(yè)務(wù)對應(yīng)不同處理。為了響應(yīng)不同的手勢,首先就需要識別它們。識別過程就是跟蹤收集系實(shí)時(shí)提供的反應(yīng)用戶在屏幕上的動(dòng)作的"基本事件",然后根據(jù)這些數(shù)據(jù)(事件集合)來判定出各種不同種類的高級別的“動(dòng)作”。
android.view.GestureDetector提供了對onScroll、onLongPress、onFling等幾個(gè)最常見動(dòng)作的監(jiān)聽。而自己的app根據(jù)需要可以通過實(shí)現(xiàn)自己的GestureDetector類型來識別出類似Drag、Scale這樣的交互動(dòng)作。
手勢識別是智能手機(jī)和平板等觸屏設(shè)備的主流交互/輸入方式,不同于PC上的鍵盤和鼠標(biāo)。
輸入事件
用戶交互產(chǎn)生的輸入事件最終由InputEvent的子類來表示,目前包括KeyEvent(Object used to report key and button events)和MotionEvent(Object used to report movement (mouse, pen, finger, trackball) events.)。
接收InputEvent的地方有很多,根據(jù)框架對事件的傳播路徑依次有Activity、Window、View(ViewTree的一條路徑:view stack)。
多數(shù)情況下都是在用戶交互的具體View中接收并處理這些輸入事件。
View的事件處理有2種方式,一種是添加監(jiān)聽器(event listener),另一種是重寫處理器方法( event handler)。前者比較方便,后者在自定義View時(shí)根據(jù)需要去重寫,而且CustomView也可以根據(jù)需要定義自己的處理器方法,或提供監(jiān)聽接口。
事件監(jiān)聽
事件監(jiān)聽接口都是只包含一個(gè)方法的interface,如:
// 在View.java中 public interface OnTouchListener { boolean onTouch(View v, MotionEvent event); } public interface OnLongClickListener { boolean onLongClick(View v); } public interface OnClickListener { void onClick(View v); } public interface OnKeyListener { boolean onKey(View v, int keyCode, KeyEvent event); }
在Activity等地方通過創(chuàng)建匿名類或?qū)崿F(xiàn)對應(yīng)接口(省去新類型和對象的分配)然后調(diào)用View.setOn...Listener()來完成注冊監(jiān)聽。
根據(jù)android的ui-events(輸入事件)的傳遞機(jī)制,監(jiān)聽器的回調(diào)方法會(huì)先于各種相應(yīng)的處理器方法被執(zhí)行,對于那些有返回boolean值的回調(diào)方法,返回值表示是否讓事件繼續(xù)被傳播,所以應(yīng)該根據(jù)需要謹(jǐn)慎設(shè)計(jì)返回值,否則會(huì)阻塞其它處理的執(zhí)行。
例如,當(dāng)為View設(shè)置OnTouchListener之后,若回調(diào)方法onTouch返回true,那么在View的 boolean dispatchTouchEvent(MotionEvent event)
中執(zhí)行了回調(diào)方法后,就不再執(zhí)行View中的處理器方法 boolean onTouchEvent(MotionEvent event)
。
事件處理器
事件處理器就是在“事件傳遞”經(jīng)過當(dāng)前View時(shí)調(diào)用的默認(rèn)方法。通常也就是對應(yīng)具體View的行為邏輯的實(shí)現(xiàn)(要知道監(jiān)聽器不是必須的,甚至可以不去定義,而任何View都會(huì)為感興趣的事件提供處理)。
有關(guān)消息傳遞的知識可以寫一整篇了,這里略過,只需要知道,輸入事件會(huì)沿著ViewTree自頂向下穿過許多“相關(guān)的”View,然后這些View處理或繼續(xù)傳遞事件。事件到達(dá)ViewTree之前還會(huì)經(jīng)過Activity和Window,最終的起源當(dāng)然是系統(tǒng)負(fù)責(zé)收集的硬件事件,從“事件管理器”發(fā)送給交互中的界面相關(guān)的某個(gè)類,開始傳播。
View類中包括下面的事件處理方法:
onKeyDown(int, KeyEvent)
- Called when a new key event occurs.onKeyUp(int, KeyEvent)
- Called when a key up event occurs.onTrackballEvent(MotionEvent)
- Called when a trackball motion event occurs.onTouchEvent(MotionEvent)
- Called when a touch screen motion event occurs.onFocusChanged(boolean, int, Rect)
- Called when the view gains or loses focus.
上面的處理器方法是站在事件傳播管道的當(dāng)前節(jié)點(diǎn)來進(jìn)行處理的,也就是處理只需要考慮當(dāng)前View所提供的功能邏輯,并告知調(diào)用者是否已經(jīng)處理結(jié)束——需要繼續(xù)傳遞?而對于ViewGroup類,它還承擔(dān)傳遞事件給childView的任務(wù),下面的方法和事件傳遞密切相關(guān):
Activity.dispatchTouchEvent(MotionEvent)
- This allows your Activity to intercept all touch events before they are dispatched to the window.ViewGroup.onInterceptTouchEvent(MotionEvent)
- This allows a ViewGroup to watch events as they are dispatched to child Views.ViewParent.requestDisallowInterceptTouchEvent(boolean)
- Call this upon a parent View to indicate that it should not intercept touch events with onInterceptTouchEvent(MotionEvent).
了解在哪些地方可以接收事件,什么時(shí)候去處理消耗事件是界面編程的一個(gè)重要方面,但“輸入事件的傳遞過程”是一個(gè)重要且夠復(fù)雜的話題,本篇文章重點(diǎn)是觸屏事件的各種手勢識別,相關(guān)的知識僅從“理解的完整和條理性”出發(fā)占據(jù)一定篇幅。
TouchMode
對于觸屏設(shè)備,用戶開始觸摸直到離開屏幕(press->lift)期間,界面會(huì)處于TouchMode的交互狀態(tài)。大致來看,所有的View都在響應(yīng)觸摸事件或者其它的KeyEvent(按鍵,按鈕等)事件。兩者在交互上截然不同,觸摸模式的狀態(tài)維護(hù)貫穿了整個(gè)系統(tǒng),包括所有的Window和Activity對象(主要就是觸摸事件的分發(fā)的控制),通過View類的 public boolean isInTouchMode ()
方法可以查看當(dāng)前設(shè)備是否處在觸摸模式。
Gestures
用戶手指(一或多個(gè))按下和最終完全離開屏幕的過程為一次觸屏操作,每次操作都可歸類為不同觸摸模式(touch pattern),最終被定義為不同的手勢(手勢和模式的定義是設(shè)計(jì)上的,用戶在使用任何觸屏設(shè)備后都會(huì)學(xué)習(xí)到不同的手勢),android支持的主要手勢有:
- Touch
- Long press
- Swipe or drag
- Long press drag
- Double touch
- Double touch drag
- Pinch open
- Pinch close
app需要根據(jù)系統(tǒng)提供的API來響應(yīng)這些手勢。
手勢識別過程
為了實(shí)現(xiàn)對手勢的響應(yīng)處理,需要理解觸摸事件的表示。而識別手勢的具體過程包括:
- 獲得觸摸事件數(shù)據(jù)。
- 分析是否匹配所支持的某個(gè)手勢。
MotionEvent
觸摸動(dòng)作觸發(fā)的輸入事件由MotionEvent表示,它實(shí)現(xiàn)了Parcelable接口——IPC需求。
目前的設(shè)備幾乎都支持多點(diǎn)觸摸,每個(gè)觸摸中的手指被當(dāng)做一個(gè)poiner。MotionEvent記錄了目前所有處于觸摸的poiner,包含它們各自的X,Y坐標(biāo),壓力,接觸區(qū)域等信息。
每個(gè)手指的按下、移動(dòng)和抬起都會(huì)產(chǎn)生一個(gè)事件對象。每個(gè)事件對應(yīng)一個(gè)“動(dòng)作”,由MotionEvent.ACTION_xxx的常量來表示:
- 在第一個(gè)手指按下時(shí),觸發(fā)ACTION_DOWN
- 后續(xù)手指按下時(shí)觸發(fā)ACTION_POINTER_DOWN
- 任何一個(gè)手指的移動(dòng)觸發(fā)ACTION_MOVE
- 非最后一個(gè)手指抬起觸發(fā)ACTION_POINTER_UP
- 最后離開屏幕時(shí)觸發(fā)ACTION_UP
- 觸摸事件序列被中斷時(shí)觸發(fā)ACTION_CANCEL,一般是對應(yīng)View的parent阻止的,比如觸摸超出區(qū)域時(shí)。
每一個(gè)手指的down,move和up都會(huì)產(chǎn)生事件。出于性能考慮,因?yàn)橐苿?dòng)過程會(huì)產(chǎn)生大量的ACTION_MOVE事件,它們被“批量”發(fā)送,也就是一個(gè)MotionEvent中將可以包含若干個(gè)實(shí)際的ACTION_MOVE事件數(shù)據(jù),很顯然,這些事件都是MOVE動(dòng)作,而且poiner數(shù)量是一樣的——任何poiner的加入和去除都引發(fā)DOWN、UP事件,這樣就不是連續(xù)的MOVE事件了。
相比上一個(gè)MotionEvent數(shù)據(jù),當(dāng)前MotionEvent的所有數(shù)據(jù)都是最新的。打包的數(shù)據(jù)根據(jù)時(shí)間形成數(shù)組,而最新的數(shù)據(jù)被作為current數(shù)據(jù)??梢酝ㄟ^ getHistorical
系列方法訪問“歷史事件”的數(shù)據(jù)。
下面是獲得當(dāng)前MotionEvent中所有事件的各個(gè)poiner的坐標(biāo)的標(biāo)準(zhǔn)形式:
void printSamples(MotionEvent ev) { final int historySize = ev.getHistorySize(); final int pointerCount = ev.getPointerCount(); for (int h = 0; h < historySize; h++) { System.out.printf("At time %d:", ev.getHistoricalEventTime(h)); for (int p = 0; p < pointerCount; p++) { System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h)); } } System.out.printf("At time %d:", ev.getEventTime()); for (int p = 0; p < pointerCount; p++) { System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getX(p), ev.getY(p)); } }
前面提到了,事件具有動(dòng)作分類,而且每個(gè)事件對象中包含所有pointer的相關(guān)數(shù)據(jù)。獲得action的方式是:
action = event.getAction() & MotionEvent.ACTION_MASK;
getAction和getActionMasked
getAction()返回的int數(shù)值內(nèi)可能包含pointerIndex的信息(這里應(yīng)該是類似View.MeasureSpec那樣利用bit位來提升性能的做法):對應(yīng)ACTION_POINTER_DOWN和ACTION_POINTER_UP動(dòng)作,返回值包含了觸發(fā)UP、DOWN的“當(dāng)前”pointer的index值,然后可以在方法 getPointerId(int), getX(int), getY(int), getPressure(int), and getSize(int)中作為pointerIndex參數(shù)使用。方法 getActionIndex()
就是用來獲取其中的pointerIndex。而 getActionMasked()
和上面語句的執(zhí)行邏輯是一樣的——返回不包含pointerIndex的action常量值。對應(yīng)只有一個(gè)手指的情況,顯然getAction()和getActionMasked()是一樣的,因?yàn)榉祷刂当旧硪矝]有額外的pointerIndex數(shù)據(jù)。獲得事件動(dòng)作應(yīng)該使用getActionMasked——更準(zhǔn)確些。
獲得某個(gè)pointer的數(shù)據(jù)的方式也比較特殊,比如獲得各個(gè)pointer的X坐標(biāo):
final int pointerCount = ev.getPointerCount(); // p就是pointerIndex for (int p = 0; p < pointerCount; p++) { System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getX(p), ev.getY(p)); }
在一次手勢操作過程中,pointer的數(shù)量可能發(fā)生變化,每一個(gè)pointer在DOWN事件的時(shí)候就獲得一個(gè)關(guān)聯(lián)的id,可以作為它的有效標(biāo)識,直至UP或CANCEL后(pointerCount變化)。
在單個(gè)的MotionEvent對象中, getPointerCount()
返回了處于觸摸的pointer的總數(shù),0~getPointerCount()-1的值就是當(dāng)前所有pointer的pointerIndex。方法 float getX(int pointerIndex)
接收index來獲得對應(yīng)pointer的X坐標(biāo)值。
類似的,其它接收pointerIndex參數(shù)的方法用以獲得pointer的其它屬性。如果需要關(guān)注某個(gè)手指的連續(xù)動(dòng)作,比如第一個(gè)按下的手指,可以通過方法 int getPointerId(int pointerIndex)
獲得pointerIndex的id,記錄此id,然后在每個(gè)MotionEvent數(shù)據(jù)檢查時(shí)通過方法 int findPointerIndex(int pointerId)
得到id在當(dāng)前MotionEvent數(shù)據(jù)中對應(yīng)的pointerIndex,就可以訪問連續(xù)事件中指定id的pointer的屬性了。
最后,MotionEvent的以下方法是經(jīng)常用到的:
long getEventTime()
獲得事件發(fā)生的時(shí)間。long getDownTime()
獲得本次觸摸事件序列的第一個(gè)——手指按下(ACTION_DOWN)的發(fā)生時(shí)間。int getAction()
、int getActionMasked()
、int getActionIndex()
、int getPointerCount()
、int getPointerId(int pointerIndex)
、float getX()
、float getX(int pointerIndex)
等。
接收事件數(shù)據(jù)
手勢操作產(chǎn)生的一系列MotionEvent對象依次分發(fā)出去,傳遞并經(jīng)過一些UI相關(guān)對象,一般的最終會(huì)經(jīng)過對應(yīng)的Activity和組成界面的那些和當(dāng)前觸屏相關(guān)的View對象——沿著ViewTree從事件所在View向上的各個(gè)parent。
在當(dāng)前界面的Activity中,可以通過重寫Activity的 boolean onTouchEvent(MotionEvent event)
方法來接收觸摸事件,更多時(shí)候,因?yàn)閂iew是具體實(shí)現(xiàn)UI交互的地方,所以在View的 boolean onTouchEvent(MotionEvent event)
方法中接收事件。
一次觸摸操作會(huì)發(fā)送一系列事件,所以onTouchEvent會(huì)被“很多次”調(diào)用。
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "ACTION_DOWN"); return true; case MotionEvent.ACTION_POINTER_DOWN: Log.d(TAG, "ACTION_POINTER_DOWN"); return true; case MotionEvent.ACTION_MOVE: Log.d(TAG, "ACTION_MOVE"); return true; case MotionEvent.ACTION_UP: Log.d(TAG, "ACTION_UP"); return true; case MotionEvent.ACTION_POINTER_UP: Log.d(TAG, "ACTION_POINTER_UP"); return true; case MotionEvent.ACTION_CANCEL: Log.d(TAG, "ACTION_CANCEL"); return true; default: Log.d(TAG, "default: action = " + action); return super.onTouchEvent(event); } }
也可以通過設(shè)置監(jiān)聽器來接收觸摸事件,這是針對具體的View對象進(jìn)行的:
myView.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { // ... Respond to touch events return true; } });
需要注意的是,不論識別那種手勢操作,ACTION_DOWN動(dòng)作一定需要返回true,否則按照調(diào)用約定,將認(rèn)為當(dāng)前處理忽略本次觸摸操作的事件序列,后續(xù)事件不會(huì)收到。
檢測手勢
在重寫的onTouch回調(diào)方法中根據(jù)收到的事件序列就可以判定出各種手勢。例如,一個(gè)ACTION_DOWN,緊接著是一系列的ACTION_MOVE,然后是ACTION_UP,這樣的序列通常就是scroll/drag手勢??偟恼f來,在實(shí)現(xiàn)識別手勢的邏輯時(shí),需要“精心設(shè)計(jì)”代碼,往往需要考慮多少偏移才被當(dāng)做有效滑動(dòng),多少時(shí)間間隙的down、up才算tap。 android.view.GestureDetector
提供了對最常見的手勢的識別。下面分別對手勢識別的關(guān)鍵相關(guān)類型做介紹。
GestureDetector
它的作用就是識別onScroll、onFling onDown(), onLongPress()等操作。將收到的MotionEvent序列傳遞給GestureDetector,之后它觸發(fā)對應(yīng)不同手勢的回調(diào)方法。
使用過程為:
1.準(zhǔn)備GestureDetector對象,提供響應(yīng)各種手勢回調(diào)方法的監(jiān)聽器。OnGestureListener就是對不同手勢的回調(diào)接口,很好理解。
// public GestureDetector(Context context, OnGestureListener listener); mDetector = new GestureDetector(this, mGestureListener);
在onTouch方法中將收到的事件傳遞給GestureDetector。
@Override public boolean onTouchEvent(MotionEvent event) { boolean handled = mDetector.onTouchEvent(event); return handled || super.onTouchEvent(event); }
如果只對GestureDetector的個(gè)別手勢的回調(diào)感興趣,監(jiān)聽器可以繼承 GestureDetector.SimpleOnGestureListener
。在onDown方法中需要返回true,否則后續(xù)事件會(huì)被忽略。
手勢運(yùn)動(dòng)
手勢可以分為運(yùn)動(dòng)型和非運(yùn)動(dòng)型。比如tap(輕敲)就沒有移動(dòng),而scroll要求手指有一定的移動(dòng)距離。手指是否發(fā)生運(yùn)動(dòng)的判定有一個(gè)臨界值:touch slop,可以通過android.view.ViewConfiguration#getScaledTouchSlop獲得,表示觸摸被判定為滑動(dòng)的最小距離。
非運(yùn)動(dòng)型手勢,比如點(diǎn)擊類型的,識別的邏輯主要是對“時(shí)間間隙”的檢測。運(yùn)動(dòng)型手勢稍復(fù)雜些,對運(yùn)動(dòng)的判定根據(jù)實(shí)際功能需要可以獲得有關(guān)運(yùn)動(dòng)的不同方面:
- pointer的start和end位置。
- 根據(jù)觸摸的x,y坐標(biāo)計(jì)算出的移動(dòng)方向。
- 通過 getHistorical
- pointer移動(dòng)時(shí)的速度。
VelocityTracker
有時(shí)對手勢運(yùn)動(dòng)過程中的速度感興趣,可以通過android.view.VelocityTracker來根據(jù)收集的事件數(shù)據(jù)計(jì)算得到運(yùn)動(dòng)時(shí)的速度:
public class MainActivity extends Activity { private static final String DEBUG_TAG = "Velocity"; ... private VelocityTracker mVelocityTracker = null; @Override public boolean onTouchEvent(MotionEvent event) { int index = event.getActionIndex(); int action = event.getActionMasked(); int pointerId = event.getPointerId(index); switch(action) { case MotionEvent.ACTION_DOWN: if(mVelocityTracker == null) { // Retrieve a new VelocityTracker object to watch the velocity of a motion. mVelocityTracker = VelocityTracker.obtain(); } else { // Reset the velocity tracker back to its initial state. mVelocityTracker.clear(); } // Add a user's movement to the tracker. mVelocityTracker.addMovement(event); break; case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); // When you want to determine the velocity, call // computeCurrentVelocity(). Then call getXVelocity() // and getYVelocity() to retrieve the velocity for each pointer ID. mVelocityTracker.computeCurrentVelocity(1000); // Log velocity of pixels per second // Best practice to use VelocityTrackerCompat where possible. Log.d("", "X velocity: " + VelocityTrackerCompat.getXVelocity(mVelocityTracker, pointerId)); Log.d("", "Y velocity: " + VelocityTrackerCompat.getYVelocity(mVelocityTracker, pointerId)); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // Return a VelocityTracker object back to be re-used by others. mVelocityTracker.recycle(); break; } return true; } }
Scroller
不嚴(yán)謹(jǐn)?shù)膮^(qū)分下,scroll可以分跟隨手指的滑動(dòng)——drag,和手指劃過屏幕后的附加減速滑動(dòng)——fling。
通常,需要對手勢運(yùn)動(dòng)進(jìn)行響應(yīng),比如畫面跟隨手指的移動(dòng)而移動(dòng)(平移),簡單的實(shí)現(xiàn)就是在ACTION_MOVE中即時(shí)偏移對應(yīng)的x,y,這種情況下對動(dòng)作的“響應(yīng)時(shí)機(jī)”是顯而易見的。另一些情況下,需要達(dá)到平滑的滑動(dòng)效果,但每次執(zhí)行滑動(dòng)的時(shí)機(jī)和滑動(dòng)的增量都需要計(jì)算。比如,點(diǎn)擊上一頁,下一頁按鈕后執(zhí)行的滾動(dòng)翻頁效果——類似ViewPager的動(dòng)畫效果那樣。再一種情況是,手指快速劃過屏幕后,需要讓顯示的內(nèi)容繼續(xù)滑動(dòng)然后漸漸停止——fling效果。這些情況下,都需要在未來一段時(shí)間內(nèi),不斷調(diào)整畫面,達(dá)到滾動(dòng)動(dòng)畫效果——每次執(zhí)行滑動(dòng)的時(shí)機(jī)和偏移量都需要計(jì)算??梢越柚鶶croller來完成“smoothly move”這樣的動(dòng)畫效果。
推薦使用android.widget.OverScroller,它兼容性好,且支持邊緣效果。和VelocityTracker一樣,Scroller是一個(gè)“計(jì)算工具”,它支持startScroll、fling兩個(gè)滑動(dòng)效果,和上面的例子對應(yīng)。從設(shè)計(jì)上,它獨(dú)立于滾動(dòng)效果的執(zhí)行,只提供對滾動(dòng)動(dòng)畫過程的計(jì)算和狀態(tài)判定。
Scroller的使用流程:
準(zhǔn)備Scroller對象。
// 在構(gòu)造函數(shù),onCreate等合適的初始化的地方 mScroller = new OverScroller(context);
在合適的時(shí)候開啟滾動(dòng)動(dòng)畫。一般的,fling效果會(huì)結(jié)合GestureDetector,識別出手指的fling手勢后開啟滾動(dòng)動(dòng)畫:在OnGestureListener中的onFling中執(zhí)行Scroller.fling()方法。
而Scroller.fling()所開啟的“平滑的滑動(dòng)效果”可以在任何需要開啟滑動(dòng)的時(shí)候執(zhí)行。
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY); mScroller.startScroll(startX, startY, dx, dy, duration);
在動(dòng)畫的每一幀的執(zhí)行時(shí)刻,計(jì)算滾動(dòng)增量,應(yīng)用到具體View對象。在自定義View時(shí),可以依靠android.view.View#postOnAnimation,android.view.View#postInvalidateOnAnimation()方法簡單的觸發(fā)在下一動(dòng)畫幀,以執(zhí)行動(dòng)畫操作?;蛘呤褂肁nimation等可以獲得動(dòng)畫幀執(zhí)行頻率的機(jī)制。View本身有computeScroll()方法可以供子類執(zhí)行動(dòng)畫式滾動(dòng)邏輯——結(jié)合postInvalidateOnAnimation()。
boolean animEnd = false; if (mScroller.computeScrollOffset()) { int currX = mScroller.getCurrX(); int currY = mScroller.getCurrY(); // 修改Viewx,y位置,可以使用View的scroll方法 } else { animEnd = false; } if (!animEnd) { postInvalidateOnAnimation(); }
像ScrollView,HorizontalScrollView自身提供了滾動(dòng)功能,ViewPager也使用Scroller完成平滑的滑動(dòng)行為。一般在自定義帶滑動(dòng)行為的控件時(shí)使用Scroller??蚣艿膸讉€(gè)控件使用EdgeEffect完成一些邊緣效果。
Multi-Touch
上面對MotionEvent的介紹中可以看到,每個(gè)處于觸摸的手指被當(dāng)做一個(gè)pointer。目前大多數(shù)手機(jī)設(shè)備幾乎都是支持10點(diǎn)觸摸。
是否考慮多點(diǎn)觸摸是根據(jù)View的功能而定。比如scroll一般一個(gè)手指就可以,而scale這一的就必須2個(gè)手指以上了。
MotionEvent的getPointerId和findPointerIndex方法提供了對當(dāng)前事件數(shù)據(jù)的每個(gè)pointer的標(biāo)識,根據(jù)pointerIndex可以調(diào)用其它以它為參數(shù)的方法獲得對應(yīng)pointer的不同方面的值。pointerId可以作為一個(gè)pointer觸屏期間的唯一標(biāo)識。
private int mActivePointerId; public boolean onTouchEvent(MotionEvent event) { .... // Get the pointer ID mActivePointerId = event.getPointerId(0); // ... Many touch events later... // Use the pointer ID to find the index of the active pointer // and fetch its position int pointerIndex = event.findPointerIndex(mActivePointerId); // Get the pointer's current position float x = event.getX(pointerIndex); float y = event.getY(pointerIndex); }
對于單點(diǎn)觸摸,通常在onTouchEvent方法中根據(jù)getAction就可以判定出對應(yīng)動(dòng)作。而多點(diǎn)觸摸時(shí)需要使用getActionMasked方法。區(qū)別前面提到了,下面的代碼片段給出了有關(guān)多點(diǎn)觸摸的一般API:
int action = MotionEventCompat.getActionMasked(event); // Get the index of the pointer associated with the action. int index = MotionEventCompat.getActionIndex(event); int xPos = -1; int yPos = -1; Log.d(DEBUG_TAG,"The action is " + actionToString(action)); if (event.getPointerCount() > 1) { Log.d(DEBUG_TAG,"Multitouch event"); // The coordinates of the current screen contact, relative to // the responding View or Activity. xPos = (int)MotionEventCompat.getX(event, index); yPos = (int)MotionEventCompat.getY(event, index); } else { // Single touch event Log.d(DEBUG_TAG,"Single touch event"); xPos = (int)MotionEventCompat.getX(event, index); yPos = (int)MotionEventCompat.getY(event, index); } ... // Given an action int, returns a string description public static String actionToString(int action) { switch (action) { case MotionEvent.ACTION_DOWN: return "Down"; case MotionEvent.ACTION_MOVE: return "Move"; case MotionEvent.ACTION_POINTER_DOWN: return "Pointer Down"; case MotionEvent.ACTION_UP: return "Up"; case MotionEvent.ACTION_POINTER_UP: return "Pointer Up"; case MotionEvent.ACTION_OUTSIDE: return "Outside"; case MotionEvent.ACTION_CANCEL: return "Cancel"; } return ""; }
類MotionEventCompat提供了一些多點(diǎn)觸摸相關(guān)輔助方法,兼容版本。
ViewConfiguration
該類提供了一些UI相關(guān)的常量,關(guān)于超時(shí)時(shí)間,大小,和距離等。會(huì)根據(jù)系統(tǒng)的版本和運(yùn)行的設(shè)備環(huán)境,如分辨率,尺寸等,提供統(tǒng)一的標(biāo)準(zhǔn)參考值,為UI元素提供一致的交互體驗(yàn)。
- Touch Slop:表示pointer被視為滾動(dòng)手勢的最小的移動(dòng)距離。
- Fling Velocity:表示手指移動(dòng)被視為觸發(fā)fling的臨界速度。
ViewGroup管理TouchEvent
事件攔截
在非ViewGroup的View中響應(yīng)觸摸事件的“職責(zé)”比較單一,就是根據(jù)當(dāng)前View的交互需求識別然后執(zhí)行交互邏輯。也就是只需要在android.view.View#onTouchEvent中處理觸摸產(chǎn)生的事件序列。
ViewGroup繼承View,所以它本身可以很據(jù)需要在onTouchEvent()中處理事件。另一方面,作為其它View的parent,它必須對childViews執(zhí)行l(wèi)ayout,并且有控制MotionEvent傳遞給目標(biāo)childView的方法onInterceptTouchEvent()。注意ViewGroup本身可以處理事件,因?yàn)樗瑫r(shí)也是合格的View子類。根據(jù)類的功能而不同,比如ViewPager會(huì)處理左右滑動(dòng)的事件,但將上下滑動(dòng)的事件傳遞給childView。要知到,ViewGroup可以包含View,也可以不包含。所以實(shí)際的事件有的是childView應(yīng)該處理的,有的是“落在”ViewGroup本身區(qū)域內(nèi)。
相關(guān)方法
有關(guān)事件分發(fā)的機(jī)制這里只簡單提及,ViewGroup可以管理MotionEvent的傳遞。涉及到下面的方法:
boolean onInterceptTouchEvent(MotionEvent ev)
該方法用來攔截傳遞給目標(biāo)childView(可以是ViewGroup,這里不一定是事件的最終目標(biāo)view,而是事件傳遞路徑經(jīng)過當(dāng)前ViewGroup后的下一個(gè)view)的MotionEvent事件,可以做些額外操作,甚至是阻止事件的傳遞自己處理。如果ViewGroup希望自己的onTouchEvent()處理手勢事件,可以重寫此方法并在onTouchEvent()中配合完成期望的手勢處理。
(1)、事件經(jīng)過ViewGroup的順序
- onInterceptTouchEvent()中接收到down事件,作為后續(xù)事件的起點(diǎn)。
- down事件可以被childView處理,或者由當(dāng)前ViewGroup的onTouchEvent()方法處理。自己處理時(shí)onInterceptTouchEvent()返回true,對應(yīng)onTouchEvent()也應(yīng)該返回true,這樣ViewGroup就可以收到后續(xù)的事件,否則——onInterceptTouchEvent()返回true,而onTouchEvent()返回false——后續(xù)事件將交給ViewGroup的parent處理。在兩個(gè)方法都返回true之后,后續(xù)事件就直接交給ViewGroup的onTouchEvent()去處理,onInterceptTouchEvent()不再收到后續(xù)事件。
- 該方法在donw事件返回false,后續(xù)所有事件,先傳遞到該方法,然后是給對應(yīng)目標(biāo)childView:的onTouchEvent()或onInterceptTouchEvent()方法——和當(dāng)前ViewGroup同樣的事件消耗規(guī)則。
- 方法返回true后,目標(biāo)view收到同樣的事件作為最后的事件,動(dòng)作變?yōu)镃ANCEL,后續(xù)事件由ViewGroup的onTouchEvent()處理,該方法也不再收到。
(2)、返回值
Return true to steal motion events from the children and have them dispatched to this ViewGroup through onTouchEvent(). The current target will receive an ACTION_CANCEL event, and no further messages will be delivered here.
注意:ViewGroup中onInterceptTouchEvent()和onTouchEvent()的合作對事件傳遞的影響主要體現(xiàn)在down事件的處理上,后續(xù)事件的傳遞受此影響。
boolean onTouchEvent(MotionEvent event)
ViewGroup繼承View的onTouchEvent(),沒有任何改變。
其返回值含義如下:
true表示事件被處理(消耗),這樣以后事件的傳遞終止。
false表示未處理,那么會(huì)沿著事件傳遞的路徑依次返回parent中去處理——parent的onTouchEvent()被執(zhí)行,直到某個(gè)parent的onTouchEvent()返回true。
void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
該方法上由childView調(diào)用的。childView調(diào)用后并傳遞true時(shí),會(huì)沿著ViewTree中root到目標(biāo)View的view hierarchy一直向上依次通知各個(gè)parent去設(shè)置一個(gè)和觸摸相關(guān)的標(biāo)記FLAG_DISALLOW_INTERCEPT,傳遞false或者一次觸摸操作結(jié)束后會(huì)清除此標(biāo)記。
檔ViewGroup包含此標(biāo)記時(shí),其默認(rèn)的行為是在通過方法boolean dispatchTouchEvent(MotionEvent ev)分發(fā)事件的時(shí)候會(huì)忽略調(diào)用onInterceptTouchEvent()去攔截事件。
拓展:Dragging和Scaling
Drag操作
android 3.0以上提供了api對拖拽進(jìn)行支持,見 View.OnDragListener。下面自己處理onTouchEvent()方法來響應(yīng)drag操作,移動(dòng)目標(biāo)View。
實(shí)現(xiàn)的重點(diǎn)是對移動(dòng)距離的檢測,按照設(shè)計(jì),從第一個(gè)手指觸摸目標(biāo)View引發(fā)down操作開始,只要還有手指處于觸摸狀態(tài),就檢測對應(yīng)手指的移動(dòng)來移動(dòng)View。移動(dòng)的距離是計(jì)算pointer的MOVE動(dòng)作對應(yīng)事件x,y坐標(biāo)的距離。需要注意的是,必須是檢測同一個(gè)pointer,因?yàn)樵试S多點(diǎn)觸摸,那么就需要記錄一個(gè)作為移動(dòng)參考的pointer——定義為activePointer。規(guī)則是:第一個(gè)手指ACTION_DOWN時(shí)記錄對應(yīng)pointerId作為activePointer,如果有手指離開就記錄剩余的某個(gè)pointer作為新的activePointer。
在ACTION_MOVE中獲得新的x,y和最后的(每次設(shè)置activePointer時(shí)記錄對應(yīng)x,y作為最后的坐標(biāo))坐標(biāo)進(jìn)行對比,計(jì)算產(chǎn)生的距離就是移動(dòng)距離。
// The ‘a(chǎn)ctive pointer' is the one currently moving our object. private int mActivePointerId = INVALID_POINTER_ID; @Override public boolean onTouchEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final float x = MotionEventCompat.getX(ev, pointerIndex); final float y = MotionEventCompat.getY(ev, pointerIndex); // Remember where we started (for dragging) mLastTouchX = x; mLastTouchY = y; // Save the ID of this pointer (for dragging) mActivePointerId = MotionEventCompat.getPointerId(ev, 0); break; } case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float y = MotionEventCompat.getY(ev, pointerIndex); // Calculate the distance moved final float dx = x - mLastTouchX; final float dy = y - mLastTouchY; mPosX += dx; mPosY += dy; invalidate(); // Remember this touch position for the next move event mLastTouchX = x; mLastTouchY = y; break; } case MotionEvent.ACTION_UP: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_CANCEL: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex); mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex); mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } break; } } return true; }
上面的方法分別在ACTION_DOWN和ACTION_POINTER_UP中設(shè)置mActivePointerId,以及上一次的觸摸位置。在ACTION_MOVE中記錄移動(dòng)到的位置,以及更新最后的觸摸位置。最后,在UP、CANCEL中清除記錄的pointerId。
可見,drag手勢的識別重點(diǎn)就是記錄作為移動(dòng)參考的pointerId,它必須是連續(xù)的。
對于drag操作的識別和響應(yīng),可以直接使用GestureDetector響應(yīng)其中的onScroll()方法即可。
scroll,drag和pan這些都是一樣的手勢/操作。
Scale
可以使用ScaleGestureDetector來檢測縮放動(dòng)作。下面的例子是drag和scale一起識別的代碼范例,注意其識別操作對事件的消耗順序:
private ScaleGestureDetector mScaleDetector; private GestureDetector mGestureDetector; private float mScaleFactor = 1.f; public MyCustomView(Context mContext){ ... mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); } ... public boolean onTouchEvent(MotionEvent event) { boolean retVal = mScaleGestureDetector.onTouchEvent(event); retVal = mGestureDetector.onTouchEvent(event) || retVal; return retVal || super.onTouchEvent(event); } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { mScaleFactor *= detector.getScaleFactor(); // 控制縮放的最大值 mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); // 縮放系數(shù)變化后通知View重繪 invalidate(); return true; } }
關(guān)于GestureDetector的用法前面給出了,上面代碼片段只展示ScaleGestureDetector、ScaleGestureDetector.SimpleOnScaleGestureListener的一般用法。
注意onTouchEvent()中先執(zhí)行ScaleGestureDetector的事件檢測,然后是GestureDetector的,只要兩次識別都未處理時(shí),才調(diào)用父類的默認(rèn)行為。
小結(jié)
理解手勢識別的整體過程是在onTouchEvent中根據(jù)MotionEvent事件序列來匹配不同的模式是整片文章的目標(biāo)。要知道,GestureDetector和ScaleGestureDetector這些框架提供的類型都是方便大家在自定義View時(shí)的手勢識別功能的實(shí)現(xiàn)。只要掌握手勢識別的思路,可以自己識別任何期望的觸摸事件模式。不過,研究框架GestureDetector的源碼,以及一些開源的控件中對手勢操作的處理是一個(gè)很好的開始。
資料
官方文檔
文章主要內(nèi)容參考來自api 22的開發(fā)文檔。
Using Touch Gestures
文件路徑:/docs/training/gestures/detector.html
Input Events
文件路徑:/docs/guide/topics/ui/ui-events.htm
案例:PhotoView
在自定義View時(shí)根據(jù)需要會(huì)出現(xiàn)監(jiān)聽特殊的手勢的需要,這個(gè)時(shí)候就需要定義自己的GestureDetector類型了。研究系統(tǒng)的GestureDetector類的實(shí)現(xiàn)非常有幫助,如果需要識別多種手勢時(shí),根據(jù)實(shí)際的特征,可以設(shè)計(jì)多個(gè)Detector類型,用來識別不同手勢,但需要注意在使用它們時(shí)對事件的消耗順序,比如drag和scale手勢的先后識別。
開源項(xiàng)目PhotoView用來展示圖片并支持各種手勢對圖片進(jìn)行縮放,平移等操作。它里面包含了幾個(gè)手勢識別的類,建議可以閱讀它的代碼來作為對手勢識別的“實(shí)現(xiàn)細(xì)節(jié)”的實(shí)踐。
源碼下載:項(xiàng)目下載
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android通過滑動(dòng)實(shí)現(xiàn)Activity跳轉(zhuǎn)(手勢識別器應(yīng)用)
- Android手勢識別器GestureDetector使用詳解
- Android View進(jìn)行手勢識別詳解
- Android基礎(chǔ)開發(fā)之手勢識別
- Android應(yīng)用開發(fā)中觸摸屏手勢識別的實(shí)現(xiàn)方法解析
- android開發(fā)之為activity增加左右手勢識別示例
- android創(chuàng)建手勢識別示例代碼
- android使用gesturedetector手勢識別示例分享
- 理解Android的手勢識別提高APP的用戶體驗(yàn)
- Android使用GestureOverlayView控件實(shí)現(xiàn)手勢識別
相關(guān)文章
Android編程實(shí)現(xiàn)應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法,涉及Android中屬性設(shè)置的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android中基于XMPP協(xié)議實(shí)現(xiàn)IM聊天程序與多人聊天室
這篇文章主要介紹了Android中基于XMPP協(xié)議實(shí)現(xiàn)IM聊天程序與多人聊天室的方法,XMPP基于XML數(shù)據(jù)格式傳輸,一般用于即時(shí)消息(IM)以及在線現(xiàn)場探測,需要的朋友可以參考下2016-02-02Android開發(fā)之多線程中實(shí)現(xiàn)利用自定義控件繪制小球并完成小球自動(dòng)下落功能實(shí)例
這篇文章主要介紹了Android開發(fā)之多線程中實(shí)現(xiàn)利用自定義控件繪制小球并完成小球自動(dòng)下落功能的方法,涉及Android多線程編程及圖形繪制相關(guān)技巧,需要的朋友可以參考下2015-12-12android實(shí)現(xiàn)圖片閃爍動(dòng)畫效果的兩種實(shí)現(xiàn)方式(實(shí)用性高)
本文通過兩種方法給大家講解了android實(shí)現(xiàn)圖片閃爍動(dòng)畫效果,實(shí)用性非常高,對這兩種方法感興趣的朋友一起通過本文學(xué)習(xí)吧2016-09-09Android.permission.MODIFY_PHONE_STATE權(quán)限問題解決辦法
這篇文章主要介紹了Android.permission.MODIFY_PHONE_STATE權(quán)限問題解決辦法的相關(guān)資料,這里提供了幾種方法幫助大家解決這種問題,需要的朋友可以參考下2016-12-12