国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

札記:android手勢識別功能實現(利用MotionEvent)

2019-12-12 04:35:11
字體:
來源:轉載
供稿:網友

摘要

本文是手勢識別輸入事件處理的完整學習記錄。內容包括輸入事件InputEvent響應方式,觸摸事件MotionEvent的概念和使用,觸摸事件的動作分類、多點觸摸。根據案例和API分析了觸摸手勢Touch Gesture的識別處理的一般過程。介紹了相關的GestureDetector,Scroller和VelocityTracker。最后分析drag和scale等一些手勢的識別。

輸入源分類

雖然android本身是一個完整的系統,它主要運行在移動設備的特性決定了我們在它上面開的app絕大數屬于客戶端程序,主要目標就是顯示界面處理交互,這點和web前端以及桌面上的應用類似。

作為“客戶端程序”,編寫的大部分功能就是處理用戶交互。不同系統(對應不同設備)可支持的用戶交互各有不同。
android可以運行在多種設備,從交互輸入上看, InputDevice.SOURCE_CLASS_xxx常量標識了sdk所支持的幾種不同輸入源的設備。有:觸屏,物理/虛擬按鍵,搖桿,鼠標等,下面的討論針對最廣泛的交互――觸屏( SOURCE_TOUCHSCREEN)。
觸屏設備從交互設計上看就是各種手勢,有點擊,雙擊,滑動,拖拽,縮放等等交互定義,本質上它們都是基礎的幾種觸摸事件的不同模式的組合。

在安卓觸屏系統中,支持單點、多點(點通常就是手指)觸摸,每個點有按下,移動和抬起。

觸屏交互的處理分不同觸屏操作――手勢的識別,然后是根據業務對應不同處理。為了響應不同的手勢,首先就需要識別它們。識別過程就是跟蹤收集系實時提供的反應用戶在屏幕上的動作的"基本事件",然后根據這些數據(事件集合)來判定出各種不同種類的高級別的“動作”。

android.view.GestureDetector提供了對onScroll、onLongPress、onFling等幾個最常見動作的監聽。而自己的app根據需要可以通過實現自己的GestureDetector類型來識別出類似Drag、Scale這樣的交互動作。

手勢識別是智能手機和平板等觸屏設備的主流交互/輸入方式,不同于PC上的鍵盤和鼠標。

輸入事件

用戶交互產生的輸入事件最終由InputEvent的子類來表示,目前包括KeyEvent(Object used to report key and button events)和MotionEvent(Object used to report movement (mouse, pen, finger, trackball) events.)。

接收InputEvent的地方有很多,根據框架對事件的傳播路徑依次有Activity、Window、View(ViewTree的一條路徑:view stack)。

多數情況下都是在用戶交互的具體View中接收并處理這些輸入事件。

View的事件處理有2種方式,一種是添加監聽器(event listener),另一種是重寫處理器方法( event handler)。前者比較方便,后者在自定義View時根據需要去重寫,而且CustomView也可以根據需要定義自己的處理器方法,或提供監聽接口。

事件監聽

事件監聽接口都是只包含一個方法的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等地方通過創建匿名類或實現對應接口(省去新類型和對象的分配)然后調用View.setOn...Listener()來完成注冊監聽。

根據android的ui-events(輸入事件)的傳遞機制,監聽器的回調方法會先于各種相應的處理器方法被執行,對于那些有返回boolean值的回調方法,返回值表示是否讓事件繼續被傳播,所以應該根據需要謹慎設計返回值,否則會阻塞其它處理的執行。

例如,當為View設置OnTouchListener之后,若回調方法onTouch返回true,那么在View的 boolean dispatchTouchEvent(MotionEvent event)中執行了回調方法后,就不再執行View中的處理器方法 boolean onTouchEvent(MotionEvent event)

事件處理器

事件處理器就是在“事件傳遞”經過當前View時調用的默認方法。通常也就是對應具體View的行為邏輯的實現(要知道監聽器不是必須的,甚至可以不去定義,而任何View都會為感興趣的事件提供處理)。

有關消息傳遞的知識可以寫一整篇了,這里略過,只需要知道,輸入事件會沿著ViewTree自頂向下穿過許多“相關的”View,然后這些View處理或繼續傳遞事件。事件到達ViewTree之前還會經過Activity和Window,最終的起源當然是系統負責收集的硬件事件,從“事件管理器”發送給交互中的界面相關的某個類,開始傳播。

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.

上面的處理器方法是站在事件傳播管道的當前節點來進行處理的,也就是處理只需要考慮當前View所提供的功能邏輯,并告知調用者是否已經處理結束――需要繼續傳遞?而對于ViewGroup類,它還承擔傳遞事件給childView的任務,下面的方法和事件傳遞密切相關:

  • 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).

了解在哪些地方可以接收事件,什么時候去處理消耗事件是界面編程的一個重要方面,但“輸入事件的傳遞過程”是一個重要且夠復雜的話題,本篇文章重點是觸屏事件的各種手勢識別,相關的知識僅從“理解的完整和條理性”出發占據一定篇幅。

TouchMode

對于觸屏設備,用戶開始觸摸直到離開屏幕(press->lift)期間,界面會處于TouchMode的交互狀態。大致來看,所有的View都在響應觸摸事件或者其它的KeyEvent(按鍵,按鈕等)事件。兩者在交互上截然不同,觸摸模式的狀態維護貫穿了整個系統,包括所有的Window和Activity對象(主要就是觸摸事件的分發的控制),通過View類的 public boolean isInTouchMode ()方法可以查看當前設備是否處在觸摸模式。

Gestures

用戶手指(一或多個)按下和最終完全離開屏幕的過程為一次觸屏操作,每次操作都可歸類為不同觸摸模式(touch pattern),最終被定義為不同的手勢(手勢和模式的定義是設計上的,用戶在使用任何觸屏設備后都會學習到不同的手勢),android支持的主要手勢有:

  • Touch
  • Long press
  • Swipe or drag
  • Long press drag
  • Double touch
  • Double touch drag
  • Pinch open
  • Pinch close

app需要根據系統提供的API來響應這些手勢。

手勢識別過程

為了實現對手勢的響應處理,需要理解觸摸事件的表示。而識別手勢的具體過程包括:

  1. 獲得觸摸事件數據。
  2. 分析是否匹配所支持的某個手勢。

MotionEvent

觸摸動作觸發的輸入事件由MotionEvent表示,它實現了Parcelable接口――IPC需求。
目前的設備幾乎都支持多點觸摸,每個觸摸中的手指被當做一個poiner。MotionEvent記錄了目前所有處于觸摸的poiner,包含它們各自的X,Y坐標,壓力,接觸區域等信息。

每個手指的按下、移動和抬起都會產生一個事件對象。每個事件對應一個“動作”,由MotionEvent.ACTION_xxx的常量來表示:

  • 在第一個手指按下時,觸發ACTION_DOWN
  • 后續手指按下時觸發ACTION_POINTER_DOWN
  • 任何一個手指的移動觸發ACTION_MOVE
  • 非最后一個手指抬起觸發ACTION_POINTER_UP
  • 最后離開屏幕時觸發ACTION_UP
  • 觸摸事件序列被中斷時觸發ACTION_CANCEL,一般是對應View的parent阻止的,比如觸摸超出區域時。

每一個手指的down,move和up都會產生事件。出于性能考慮,因為移動過程會產生大量的ACTION_MOVE事件,它們被“批量”發送,也就是一個MotionEvent中將可以包含若干個實際的ACTION_MOVE事件數據,很顯然,這些事件都是MOVE動作,而且poiner數量是一樣的――任何poiner的加入和去除都引發DOWN、UP事件,這樣就不是連續的MOVE事件了。

相比上一個MotionEvent數據,當前MotionEvent的所有數據都是最新的。打包的數據根據時間形成數組,而最新的數據被作為current數據。可以通過 getHistorical系列方法訪問“歷史事件”的數據。

下面是獲得當前MotionEvent中所有事件的各個poiner的坐標的標準形式:

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));  }}

前面提到了,事件具有動作分類,而且每個事件對象中包含所有pointer的相關數據。獲得action的方式是:

action = event.getAction() & MotionEvent.ACTION_MASK;

getAction和getActionMasked

getAction()返回的int數值內可能包含pointerIndex的信息(這里應該是類似View.MeasureSpec那樣利用bit位來提升性能的做法):對應ACTION_POINTER_DOWN和ACTION_POINTER_UP動作,返回值包含了觸發UP、DOWN的“當前”pointer的index值,然后可以在方法 getPointerId(int), getX(int), getY(int), getPressure(int), and getSize(int)中作為pointerIndex參數使用。方法 getActionIndex()就是用來獲取其中的pointerIndex。而 getActionMasked()和上面語句的執行邏輯是一樣的――返回不包含pointerIndex的action常量值。對應只有一個手指的情況,顯然getAction()和getActionMasked()是一樣的,因為返回值本身也沒有額外的pointerIndex數據。獲得事件動作應該使用getActionMasked――更準確些。

獲得某個pointer的數據的方式也比較特殊,比如獲得各個pointer的X坐標:

final int pointerCount = ev.getPointerCount();// p就是pointerIndexfor (int p = 0; p < pointerCount; p++) {  System.out.printf(" pointer %d: (%f,%f)",    ev.getPointerId(p), ev.getX(p), ev.getY(p));}

在一次手勢操作過程中,pointer的數量可能發生變化,每一個pointer在DOWN事件的時候就獲得一個關聯的id,可以作為它的有效標識,直至UP或CANCEL后(pointerCount變化)。

在單個的MotionEvent對象中, getPointerCount()返回了處于觸摸的pointer的總數,0~getPointerCount()-1的值就是當前所有pointer的pointerIndex。方法 float getX(int pointerIndex)接收index來獲得對應pointer的X坐標值。
類似的,其它接收pointerIndex參數的方法用以獲得pointer的其它屬性。如果需要關注某個手指的連續動作,比如第一個按下的手指,可以通過方法 int getPointerId(int pointerIndex)獲得pointerIndex的id,記錄此id,然后在每個MotionEvent數據檢查時通過方法 int findPointerIndex(int pointerId)得到id在當前MotionEvent數據中對應的pointerIndex,就可以訪問連續事件中指定id的pointer的屬性了。

最后,MotionEvent的以下方法是經常用到的:

  • long getEventTime() 獲得事件發生的時間。
  • long getDownTime() 獲得本次觸摸事件序列的第一個――手指按下(ACTION_DOWN)的發生時間。
  • int getAction()int getActionMasked()int getActionIndex()int getPointerCount()int getPointerId(int pointerIndex)float getX()float getX(int pointerIndex)等。

接收事件數據

手勢操作產生的一系列MotionEvent對象依次分發出去,傳遞并經過一些UI相關對象,一般的最終會經過對應的Activity和組成界面的那些和當前觸屏相關的View對象――沿著ViewTree從事件所在View向上的各個parent。

在當前界面的Activity中,可以通過重寫Activity的 boolean onTouchEvent(MotionEvent event)方法來接收觸摸事件,更多時候,因為View是具體實現UI交互的地方,所以在View的 boolean onTouchEvent(MotionEvent event)方法中接收事件。
一次觸摸操作會發送一系列事件,所以onTouchEvent會被“很多次”調用。

@Overridepublic 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);  }}

也可以通過設置監聽器來接收觸摸事件,這是針對具體的View對象進行的:

myView.setOnTouchListener(new OnTouchListener() {  public boolean onTouch(View v, MotionEvent event) {    // ... Respond to touch events        return true;  }});

需要注意的是,不論識別那種手勢操作,ACTION_DOWN動作一定需要返回true,否則按照調用約定,將認為當前處理忽略本次觸摸操作的事件序列,后續事件不會收到。

檢測手勢

在重寫的onTouch回調方法中根據收到的事件序列就可以判定出各種手勢。例如,一個ACTION_DOWN,緊接著是一系列的ACTION_MOVE,然后是ACTION_UP,這樣的序列通常就是scroll/drag手勢。總的說來,在實現識別手勢的邏輯時,需要“精心設計”代碼,往往需要考慮多少偏移才被當做有效滑動,多少時間間隙的down、up才算tap。 android.view.GestureDetector提供了對最常見的手勢的識別。下面分別對手勢識別的關鍵相關類型做介紹。

GestureDetector

它的作用就是識別onScroll、onFling onDown(), onLongPress()等操作。將收到的MotionEvent序列傳遞給GestureDetector,之后它觸發對應不同手勢的回調方法。
使用過程為:

1.準備GestureDetector對象,提供響應各種手勢回調方法的監聽器。OnGestureListener就是對不同手勢的回調接口,很好理解。

// public GestureDetector(Context context, OnGestureListener listener);mDetector = new GestureDetector(this, mGestureListener);

在onTouch方法中將收到的事件傳遞給GestureDetector。

@Overridepublic boolean onTouchEvent(MotionEvent event) {  boolean handled = mDetector.onTouchEvent(event);  return handled || super.onTouchEvent(event);}

如果只對GestureDetector的個別手勢的回調感興趣,監聽器可以繼承 GestureDetector.SimpleOnGestureListener。在onDown方法中需要返回true,否則后續事件會被忽略。

手勢運動

手勢可以分為運動型和非運動型。比如tap(輕敲)就沒有移動,而scroll要求手指有一定的移動距離。手指是否發生運動的判定有一個臨界值:touch slop,可以通過android.view.ViewConfiguration#getScaledTouchSlop獲得,表示觸摸被判定為滑動的最小距離。

非運動型手勢,比如點擊類型的,識別的邏輯主要是對“時間間隙”的檢測。運動型手勢稍復雜些,對運動的判定根據實際功能需要可以獲得有關運動的不同方面:

  • pointer的start和end位置。
  • 根據觸摸的x,y坐標計算出的移動方向。
  • 通過 getHistorical
  • pointer移動時的速度。

VelocityTracker

有時對手勢運動過程中的速度感興趣,可以通過android.view.VelocityTracker來根據收集的事件數據計算得到運動時的速度:

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

不嚴謹的區分下,scroll可以分跟隨手指的滑動――drag,和手指劃過屏幕后的附加減速滑動――fling。

通常,需要對手勢運動進行響應,比如畫面跟隨手指的移動而移動(平移),簡單的實現就是在ACTION_MOVE中即時偏移對應的x,y,這種情況下對動作的“響應時機”是顯而易見的。另一些情況下,需要達到平滑的滑動效果,但每次執行滑動的時機和滑動的增量都需要計算。比如,點擊上一頁,下一頁按鈕后執行的滾動翻頁效果――類似ViewPager的動畫效果那樣。再一種情況是,手指快速劃過屏幕后,需要讓顯示的內容繼續滑動然后漸漸停止――fling效果。這些情況下,都需要在未來一段時間內,不斷調整畫面,達到滾動動畫效果――每次執行滑動的時機和偏移量都需要計算。可以借助Scroller來完成“smoothly move”這樣的動畫效果。

推薦使用android.widget.OverScroller,它兼容性好,且支持邊緣效果。和VelocityTracker一樣,Scroller是一個“計算工具”,它支持startScroll、fling兩個滑動效果,和上面的例子對應。從設計上,它獨立于滾動效果的執行,只提供對滾動動畫過程的計算和狀態判定。

Scroller的使用流程:

準備Scroller對象。

// 在構造函數,onCreate等合適的初始化的地方mScroller = new OverScroller(context);

在合適的時候開啟滾動動畫。一般的,fling效果會結合GestureDetector,識別出手指的fling手勢后開啟滾動動畫:在OnGestureListener中的onFling中執行Scroller.fling()方法。

而Scroller.fling()所開啟的“平滑的滑動效果”可以在任何需要開啟滑動的時候執行。

mScroller.fling(startX, startY, velocityX, velocityY,      minX, maxX, minY, maxY, overX, overY);mScroller.startScroll(startX, startY, dx, dy, duration);

在動畫的每一幀的執行時刻,計算滾動增量,應用到具體View對象。在自定義View時,可以依靠android.view.View#postOnAnimation,android.view.View#postInvalidateOnAnimation()方法簡單的觸發在下一動畫幀,以執行動畫操作。或者使用Animation等可以獲得動畫幀執行頻率的機制。View本身有computeScroll()方法可以供子類執行動畫式滾動邏輯――結合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自身提供了滾動功能,ViewPager也使用Scroller完成平滑的滑動行為。一般在自定義帶滑動行為的控件時使用Scroller。框架的幾個控件使用EdgeEffect完成一些邊緣效果。

Multi-Touch

上面對MotionEvent的介紹中可以看到,每個處于觸摸的手指被當做一個pointer。目前大多數手機設備幾乎都是支持10點觸摸。
是否考慮多點觸摸是根據View的功能而定。比如scroll一般一個手指就可以,而scale這一的就必須2個手指以上了。

MotionEvent的getPointerId和findPointerIndex方法提供了對當前事件數據的每個pointer的標識,根據pointerIndex可以調用其它以它為參數的方法獲得對應pointer的不同方面的值。pointerId可以作為一個pointer觸屏期間的唯一標識。

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);}

對于單點觸摸,通常在onTouchEvent方法中根據getAction就可以判定出對應動作。而多點觸摸時需要使用getActionMasked方法。區別前面提到了,下面的代碼片段給出了有關多點觸摸的一般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 descriptionpublic 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提供了一些多點觸摸相關輔助方法,兼容版本。

ViewConfiguration

該類提供了一些UI相關的常量,關于超時時間,大小,和距離等。會根據系統的版本和運行的設備環境,如分辨率,尺寸等,提供統一的標準參考值,為UI元素提供一致的交互體驗。

  • Touch Slop:表示pointer被視為滾動手勢的最小的移動距離。
  • Fling Velocity:表示手指移動被視為觸發fling的臨界速度。

ViewGroup管理TouchEvent

事件攔截

在非ViewGroup的View中響應觸摸事件的“職責”比較單一,就是根據當前View的交互需求識別然后執行交互邏輯。也就是只需要在android.view.View#onTouchEvent中處理觸摸產生的事件序列。

ViewGroup繼承View,所以它本身可以很據需要在onTouchEvent()中處理事件。另一方面,作為其它View的parent,它必須對childViews執行layout,并且有控制MotionEvent傳遞給目標childView的方法onInterceptTouchEvent()。注意ViewGroup本身可以處理事件,因為它同時也是合格的View子類。根據類的功能而不同,比如ViewPager會處理左右滑動的事件,但將上下滑動的事件傳遞給childView。要知到,ViewGroup可以包含View,也可以不包含。所以實際的事件有的是childView應該處理的,有的是“落在”ViewGroup本身區域內。

相關方法

有關事件分發的機制這里只簡單提及,ViewGroup可以管理MotionEvent的傳遞。涉及到下面的方法:

boolean onInterceptTouchEvent(MotionEvent ev)

該方法用來攔截傳遞給目標childView(可以是ViewGroup,這里不一定是事件的最終目標view,而是事件傳遞路徑經過當前ViewGroup后的下一個view)的MotionEvent事件,可以做些額外操作,甚至是阻止事件的傳遞自己處理。如果ViewGroup希望自己的onTouchEvent()處理手勢事件,可以重寫此方法并在onTouchEvent()中配合完成期望的手勢處理。

(1)、事件經過ViewGroup的順序

  1. onInterceptTouchEvent()中接收到down事件,作為后續事件的起點。
  2. down事件可以被childView處理,或者由當前ViewGroup的onTouchEvent()方法處理。自己處理時onInterceptTouchEvent()返回true,對應onTouchEvent()也應該返回true,這樣ViewGroup就可以收到后續的事件,否則――onInterceptTouchEvent()返回true,而onTouchEvent()返回false――后續事件將交給ViewGroup的parent處理。在兩個方法都返回true之后,后續事件就直接交給ViewGroup的onTouchEvent()去處理,onInterceptTouchEvent()不再收到后續事件。
  3. 該方法在donw事件返回false,后續所有事件,先傳遞到該方法,然后是給對應目標childView:的onTouchEvent()或onInterceptTouchEvent()方法――和當前ViewGroup同樣的事件消耗規則。
  4. 方法返回true后,目標view收到同樣的事件作為最后的事件,動作變為CANCEL,后續事件由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()的合作對事件傳遞的影響主要體現在down事件的處理上,后續事件的傳遞受此影響。

boolean onTouchEvent(MotionEvent event)

ViewGroup繼承View的onTouchEvent(),沒有任何改變。

其返回值含義如下:

true表示事件被處理(消耗),這樣以后事件的傳遞終止。

false表示未處理,那么會沿著事件傳遞的路徑依次返回parent中去處理――parent的onTouchEvent()被執行,直到某個parent的onTouchEvent()返回true。

void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

該方法上由childView調用的。childView調用后并傳遞true時,會沿著ViewTree中root到目標View的view hierarchy一直向上依次通知各個parent去設置一個和觸摸相關的標記FLAG_DISALLOW_INTERCEPT,傳遞false或者一次觸摸操作結束后會清除此標記。

檔ViewGroup包含此標記時,其默認的行為是在通過方法boolean dispatchTouchEvent(MotionEvent ev)分發事件的時候會忽略調用onInterceptTouchEvent()去攔截事件。

拓展:Dragging和Scaling

Drag操作

android 3.0以上提供了api對拖拽進行支持,見 View.OnDragListener。下面自己處理onTouchEvent()方法來響應drag操作,移動目標View。

實現的重點是對移動距離的檢測,按照設計,從第一個手指觸摸目標View引發down操作開始,只要還有手指處于觸摸狀態,就檢測對應手指的移動來移動View。移動的距離是計算pointer的MOVE動作對應事件x,y坐標的距離。需要注意的是,必須是檢測同一個pointer,因為允許多點觸摸,那么就需要記錄一個作為移動參考的pointer――定義為activePointer。規則是:第一個手指ACTION_DOWN時記錄對應pointerId作為activePointer,如果有手指離開就記錄剩余的某個pointer作為新的activePointer。

在ACTION_MOVE中獲得新的x,y和最后的(每次設置activePointer時記錄對應x,y作為最后的坐標)坐標進行對比,計算產生的距離就是移動距離。

// The ‘active pointer' is the one currently moving our object.private int mActivePointerId = INVALID_POINTER_ID;@Overridepublic 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中設置mActivePointerId,以及上一次的觸摸位置。在ACTION_MOVE中記錄移動到的位置,以及更新最后的觸摸位置。最后,在UP、CANCEL中清除記錄的pointerId。

可見,drag手勢的識別重點就是記錄作為移動參考的pointerId,它必須是連續的。

對于drag操作的識別和響應,可以直接使用GestureDetector響應其中的onScroll()方法即可。

scroll,drag和pan這些都是一樣的手勢/操作。

Scale

可以使用ScaleGestureDetector來檢測縮放動作。下面的例子是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));    // 縮放系數變化后通知View重繪    invalidate();    return true;  }}

關于GestureDetector的用法前面給出了,上面代碼片段只展示ScaleGestureDetector、ScaleGestureDetector.SimpleOnScaleGestureListener的一般用法。

注意onTouchEvent()中先執行ScaleGestureDetector的事件檢測,然后是GestureDetector的,只要兩次識別都未處理時,才調用父類的默認行為。

小結

理解手勢識別的整體過程是在onTouchEvent中根據MotionEvent事件序列來匹配不同的模式是整片文章的目標。要知道,GestureDetector和ScaleGestureDetector這些框架提供的類型都是方便大家在自定義View時的手勢識別功能的實現。只要掌握手勢識別的思路,可以自己識別任何期望的觸摸事件模式。不過,研究框架GestureDetector的源碼,以及一些開源的控件中對手勢操作的處理是一個很好的開始。

資料

官方文檔

文章主要內容參考來自api 22的開發文檔。

Using Touch Gestures

文件路徑:/docs/training/gestures/detector.html

Input Events

文件路徑:/docs/guide/topics/ui/ui-events.htm

案例:PhotoView

在自定義View時根據需要會出現監聽特殊的手勢的需要,這個時候就需要定義自己的GestureDetector類型了。研究系統的GestureDetector類的實現非常有幫助,如果需要識別多種手勢時,根據實際的特征,可以設計多個Detector類型,用來識別不同手勢,但需要注意在使用它們時對事件的消耗順序,比如drag和scale手勢的先后識別。

開源項目PhotoView用來展示圖片并支持各種手勢對圖片進行縮放,平移等操作。它里面包含了幾個手勢識別的類,建議可以閱讀它的代碼來作為對手勢識別的“實現細節”的實踐。

源碼下載:項目下載

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 宣武区| 天等县| 梧州市| 纳雍县| 彭山县| 东宁县| 汉源县| 皮山县| 游戏| 铁力市| 平塘县| 敦化市| 沽源县| 昌宁县| 海阳市| 永州市| 富平县| 伊川县| 柘城县| 洛川县| 堆龙德庆县| 吉木萨尔县| 平陆县| 平定县| 和平区| 共和县| 司法| 湄潭县| 上高县| 汾西县| 安塞县| 东兰县| 清水河县| 尼木县| 醴陵市| 榆中县| 揭东县| 隆尧县| 洪泽县| 丰宁| 始兴县|