Android 觸摸手勢基礎 官方文檔概覽
觸摸手勢檢測基礎
手勢檢測一般包含兩個階段:
1.獲取touch事件數據
2.解析這些數據,看它們是否滿足你的應用所支持的某種手勢。
相關API:
兼容版的:
(Note that MotionEventCompat is not a replacement for the MotionEvent class. Rather, it provides static utility methods to which you pass your MotionEvent object in order to receive the desired action associated with that event.)
一般的Activity或View中的touch事件處理
Activity或View類的onTouchEvent()
回調函數會接收到touch事件。
為了截獲touch事件,你需要覆寫Activity或View的onTouchEvent方法。
View中還可以使用setOnTouchListener()方法添加點擊事件的 View.OnTouchListener
監聽對象。這樣就可以不繼承View而處理點擊事件。
但是如果需要處理雙擊、長按、fling(快滑)等手勢,你需要利用 GestureDetector
類。
onTouchEvent方法的返回值
onTouchEvent方法的返回值,如果返回true,意味着你已經處理過了touch事件;如果返回false,將會繼續通過view stack傳遞事件,直到事件被處理。
這里需要注意 ACTION_DOWN
事件,如果返回false,則該listener將不會被告知后面的一系列 ACTION_MOVE
和 ACTION_UP
事件。
檢測手勢
Android提供了GestureDetector
類來檢測一般的手勢。
基本使用:
1.生成GestureDetector
對象(或GestureDetectorCompat對象),構造時的參數傳入監聽器對象。
監聽器對象實現GestureDetector.OnGestureListener
接口。
如果你僅僅是想利用其中的一些手勢而不是全部,那么你可以選擇繼承GestureDetector.SimpleOnGestureListener類,這是一個適配器模式,即這個類實現了GestureDetector.OnGestureListener
接口,為其中所有的方法提供了空實現(返回值都是false),當繼承GestureDetector.SimpleOnGestureListener類時,子類只需要覆寫感興趣的方法,其他方法是空實現。
2.為了讓 GestureDetector
對象接收到事件,需要覆寫View或Activity中的 onTouchEvent()
方法,將事件傳遞給detector對象。
一個例子:

package com.example.hellogesturedetector; import android.os.Bundle; import android.app.Activity; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.Menu; import android.view.MotionEvent; import android.view.ViewGroup; import android.widget.TextView; public class HelloGestureDetectorActivity extends Activity { private static final String LOG_TAG = "HelloGesture"; private GestureDetector mGestureDetector = null; private TextView mGestureTextView = null; private TextView mDoubleTapTextView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_hello_gesture_detector); mGestureTextView = (TextView) findViewById(R.id.gesture); mDoubleTapTextView = (TextView) findViewById(R.id.doubleTap); // 構造GestureDetector對象,傳入監聽器對象 mGestureDetector = new GestureDetector(this, mOnGestureListener); // 傳入雙擊監聽器對象 mGestureDetector.setOnDoubleTapListener(mDoubleTapListener); } @Override public boolean onTouchEvent(MotionEvent event) { // 在onTouchEvent方法中將事件傳遞給手勢檢測對象,否則手勢監聽對象中的回調函數是不會被調用的 mGestureDetector.onTouchEvent(event); return super.onTouchEvent(event); } private OnGestureListener mOnGestureListener = new OnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { Log.i(LOG_TAG, "onSingleTapUp: " + e.toString()); mGestureTextView.setText("onSingleTapUp: "); return false; } @Override public void onShowPress(MotionEvent e) { Log.i(LOG_TAG, "onShowPress: " + e.toString()); mGestureTextView.setText("onShowPress: "); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { Log.i(LOG_TAG, "onScroll: " + e1.toString() + ", " + e2.toString()); mGestureTextView.setText("onScroll "); return false; } @Override public void onLongPress(MotionEvent e) { Log.i(LOG_TAG, "onLongPress: " + e.toString()); mGestureTextView.setText("onLongPress: "); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { Log.i(LOG_TAG, "onFling: " + e1.toString() + ", " + e2.toString()); mGestureTextView.setText("onFling "); return false; } @Override public boolean onDown(MotionEvent e) { Log.i(LOG_TAG, "onDown: " + e.toString()); mGestureTextView.setText("onDown: "); return false; } }; private OnDoubleTapListener mDoubleTapListener = new OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { Log.i("LOG_TAG", "onSingleTapConfirmed: " + e.toString()); mDoubleTapTextView.setText("onSingleTapConfirmed: "); return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { Log.i("LOG_TAG", "onDoubleTapEvent: " + e.toString()); mDoubleTapTextView.setText("onDoubleTapEvent: "); return false; } @Override public boolean onDoubleTap(MotionEvent e) { Log.i("LOG_TAG", "onDoubleTap: " + e.toString()); mDoubleTapTextView.setText("onDoubleTap: "); return false; } }; }
根據官網上說:
關於onDown()方法的返回值,最好是返回true,因為所有的手勢都是從onDown()信息開始的。
如果像 GestureDetector.SimpleOnGestureListener
默認實現一樣返回false,系統就會認為你想要忽略之后的其他手勢,然后GestureDetector.OnGestureListener
的其他方法就不會被調用。
但是實際程序驗證的時候,發現返回true還是false好像沒有什么影響。(??)
跟蹤運動 速度
有很多不同的方法來記錄手勢中的運動,比如pointer的起始位置和終止位置;pointer運動的方向;手勢的歷史(通過 getHistorySize()
方法得到);還有pointer的運動速度。
Android提供了VelocityTracker
類和VelocityTrackerCompat類,來記錄touch事件的速度。
代碼例子:

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; } }
滾動手勢
如果一個標准的布局有可能會超出它的容器的邊界,可以把它嵌套在一個ScrollView中,這樣就會得到一個可以滾動的布局,由framewok處理。
實現一個自定義的scroller應該只在一些特殊情況下需要。
Scroller用來隨時間制造滾動動畫,使用平台標准的滾動物理參數(摩擦力、速度等)。
Scroller自己本身實際上並不繪制任何東西。
Scroller記錄滾動的偏移值,但是它並不會將這些位置應用到你的View,你需要自己動手。
詳見:http://developer.android.com/training/gestures/scroll.html
多點觸摸手勢
當多個pointer同時觸摸屏幕,系統會生成如下事件:
ACTION_DOWN
—For the first pointer that touches the screen. This starts the gesture. The pointer data for this pointer is always at index 0 in theMotionEvent
.ACTION_POINTER_DOWN
—For extra pointers that enter the screen beyond the first. The pointer data for this pointer is at the index returned bygetActionIndex()
.ACTION_MOVE
—A change has happened during a press gesture.ACTION_POINTER_UP
—Sent when a non-primary pointer goes up.ACTION_UP
—Sent when the last pointer leaves the screen.
你可以依靠每一個pointer的index和ID來追蹤每一個pointer:
Index:MotionEvent會把每一個pointer的信息放在一個數組里,index即是這個數組索引。大多數你用的MotionEvent方法是以這個index作為參數的。
ID:每一個pointer還有一個ID映射,在touch事件中保持恆定一致(persistent),這樣就可以在整個手勢中跟蹤一個單獨的pointer。
pointer在一個motion event中出現的順序是未定的,所以pointer的index在不同的事件中是可變的,但是只要pointer保持active,它的ID是保持不變的。
通過getPointerId()
獲得ID,這樣就可以在多個motion event中追蹤pointer。然后對於連續的motion event,可以使用findPointerIndex()方法來獲得指定ID的pointer在當前事件中的index。
比如:
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); }
獲取MotionEvent的動作應該使用getActionMasked()方法(或者是兼容版的MotionEventCompat.getActionMasked())。
與舊版的getAction()不同,getActionMasked()
方法是被設計為可以多個pointer工作的。
它會返回帶掩模的動作,不帶pointer用於index的那些位。
你可以使用getActionIndex()來得到index。
拖動和縮放
拖動一個對象:
如果是Android 3.0以上,可以使用View的新接口View.OnDragListener
參見:Drag and Drop。
其他參見:http://developer.android.com/training/gestures/scale.html
縮放可以使用 ScaleGestureDetector
。
ScaleGestureDetector
可以和GestureDetector
一起使用。
ViewGroup中的Touch事件處理
處理 ViewGroup
的touch事件要麻煩一些,因為很可能各種touch事件的目標不是ViewGroup而是它的child。
為了確保每一個child正確地接收到touch events,需要覆寫ViewGroup的onInterceptTouchEvent()方法。
如果onInterceptTouchEvent()方法返回true,說明MotionEvent被截獲了,它將不會被傳遞給child,而是傳遞給parent的 onTouchEvent()
方法。
如果你在parent的onInterceptTouchEvent()方法中返回了true,先前還在處理touch event的child view將會接收到一個 ACTION_CANCEL
,之后的事件就會全傳遞到parent的onTouchEvent中。
如果 onInterceptTouchEvent()
方法返回false,則事件繼續順着view結構向下傳遞,parent不會截獲事件,也不會調用parent的onTouchEvent()方法。
另:
ViewConfiguration提供一些常量。
TouchDelegate類可以用來設置View的觸摸區域。
用法見:http://developer.android.com/training/gestures/viewgroup.html
參考資料
Training: Using Touch Gestures
http://developer.android.com/training/gestures/index.html