【朝花夕拾】Android自定義View篇之(八)多點觸控
前言
轉載請宣告,轉自【https://www.cnblogs.com/andy-songwei/p/11155259.html】,謝謝!
在前面的文章中,介紹了不少觸控相關的知識,但都是基於單點觸控的,即一次只用一根手指。但是在實際使用App中,常常是多根手指同時操作,這就需要用到多點觸控相關的知識了。多點觸控是在Android2.0開始引入的,在現在使用的Android手機上都是支援多點觸控的。本系列文章將對常見的多點觸控相關的重點知識進行總結,並使用多點觸控來實現一些常見的效果,從而達到將理論知識付諸實踐的目的。
本文主要包含如下內容:
一、觸控事件感應的產生原理
在介紹多點觸控前,我們先了解一下現在手機螢幕觸控事件感應的原理。 當前手機使用的螢幕一般都是電容式觸控式螢幕,我們看看百度百科中對此的介紹:
電容式觸控式螢幕技術是利用人體的電流感應進行工作的。當手指觸控在螢幕上時,由於人體電場,使用者和觸控式螢幕表面形成以一個耦合電容,對於高頻電流來說,電容是直接導體,於是手指從接觸點吸走一個很小的電流。這個電流分別從觸控式螢幕的四角上的電極中流出,並且流經這四個電極的電流與手指到四角的距離成正比,控制器通過對這四個電流比例的精確計算,得出觸控點的位置。 (摘自百度百科【電容式觸控式螢幕】)
電容式觸控式螢幕感應觸控事件,和人體電場相關,這也就是為什麼用手指觸控時螢幕能有響應,但其它物體卻不行的原因。而早期的手機採用的是電阻式觸控式螢幕,當螢幕受到壓力時電阻有變化,通過電阻來感應觸控,所以除了手指外,其它物體也能讓螢幕產生響應。電容式觸控式螢幕支援多點觸控,但電阻式觸控式螢幕不能。
二、觸控事件與底層
在文章【【朝花夕拾】Android自定義View篇之(六)Android事件分發機制(中)從原始碼分析事件分發邏輯及經常遇到的一些“詭異”現象】的開頭我們介紹過“事件的前世今生”,事件是從硬體感應,然後經過驅動、框架,然後到達View的。前面講過的內容這裡不再贅述,我們看看下面這份截圖:
這是MotionEvent類中跟蹤與事件相關的主要方法的結果,幾乎都是很快就調到了native層。通過這些方法,我們可以直觀感受到事件與底層的密切聯絡。
三、事件輸入裝置以及MotionEvent中對應的事件說明
隨著Android系統版本的提升,以及Android硬體裝置的發展,事件輸入裝置和對應的事件特點也在不斷髮生著變化。軌跡球出現在很早的手機中,後來去掉了;多點觸控也是在Android2.0開始支援的......咱們這裡不一一列舉,當然,大家也不關心這些細節。這裡我彙總了目前我知道的一些事件輸入裝置,以及在MotionEvent中封裝的對應的響應事件。
如下表格顯示了它們大概的對應關係,由於我使用過的裝置有限,所以有些對應裝置的對應關係不太確定,下表中在括號內加了“?”。注意我這裡的措詞是“大概”,因為下面有些對應關係可能有交叉的情況等。本文關注的重點是多點觸控,其它的這裡咱們只做瞭解即可。
輸入裝置 | 響應事件 | 事件常量值 | 事件說明 |
單點觸控/ |
ACTION_DOWN | 0 | 第一個手指初次接觸到螢幕時觸發。 |
ACTION_UP | 1 | 最後一個手指離開螢幕時觸發。 | |
ACTION_MOVE | 2 | 手指在螢幕上滑動時觸發,會多次觸發。 | |
ACTION_CANCEL | 3 | 當前的手勢被中斷時觸發。 | |
ACTION_OUTSIDE | 4 | 事件發生在UI邊界之外時觸發。 | |
ACTION_POINTER_DOWN | 5 | 有非主要的手指按下(即按下之前已經有手指在螢幕上)。 | |
ACTION_POINTER_UP | 6 | 有非主要的手指擡起(即擡起之後仍然有手指在螢幕上)。 | |
滑鼠/軌跡球(?) | ACTION_HOVER_MOVE | 7 | 指標在視窗或者View區域移動,但沒有按下。 |
ACTION_SCROLL | 8 | 滾輪滾動,可以觸發水平滾動或垂直滾動 | |
ACTION_HOVER_ENTER | 9 | 指標移入到視窗或者View區域,但沒有按下。 | |
ACTION_HOVER_EXIT | 10 | 指標移出到視窗或者View區域,但沒有按下。 | |
鍵盤/操縱桿(?)/ |
ACTION_BUTTON_PRESS | 11 | 按鈕被按下 |
ACTION_BUTTON_RELEASE | 12 | 按鈕被釋放 | |
多點觸控 | ACTION_POINTER_1_DOWN | 0x0005 | --- |
ACTION_POINTER_2_DOWN | 0x0105 | 第 2 個手指按下,android2.2後已廢棄,不推薦使用。 | |
ACTION_POINTER_3_DOWN | 0x0205 | 第 3 個手指按下,android2.2後已廢棄,不推薦使用。 | |
ACTION_POINTER_1_UP | 0x0006 | --- | |
ACTION_POINTER_2_UP | 0x0106 | 第 2 個手指擡起,android2.2後已廢棄,不推薦使用。 | |
ACTION_POINTER_3_UP | 0x0206 | 第 3 個手指擡起,android2.2後已廢棄,不推薦使用。 |
特別注意:表格中“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”兩個常量,我看到過有一些知名部落格中對它們的描述是:第二根手指按下/擡起,已廢棄,不推薦使用。我通過實驗發現這個說法是錯誤的,所以特地糾正一下,在上述表格中沒有對它們進行描述,而在這裡特地強調一下。如下是驗證的程式碼和列印的結果:
1 @Override 2 public boolean onTouchEvent(MotionEvent event) { 3 Log.i(TAG, MotionEvent.actionToString(event.getAction()) + ";action=" + event.getAction()); 4 return super.onTouchEvent(event); 5 }
依次按下和擡起兩根手指,列印結果如下:
07-05 22:24:47.982 23249-23249/com.example.demos I/songzheweiwang: ACTION_DOWN;action=0 07-05 22:24:48.511 23249-23249/com.example.demos I/songzheweiwang: ACTION_POINTER_DOWN(1);action=261 07-05 22:24:49.599 23249-23249/com.example.demos I/songzheweiwang: ACTION_POINTER_UP(1);action=262 07-05 22:24:49.607 23249-23249/com.example.demos I/songzheweiwang: ACTION_UP;action=1
可以看到,整個過程中就沒有列印“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”這兩個值,而是分別對應列印的“ACTION_POINTER_2_DOWN”和“ACTION_POINTER_2_UP”。
在前面的表格中可以看到“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”這兩個值對應的十進位制值分別和“ACTION_POINTER_DOWN”和“ACTION_POINTER_UP”相等,這兩個值只有在Android2.2支援多點觸控後,系統提供的getActionMasked()方法中才會用到,用於表示所有非第一個手指的按下和擡起事件。至於這兩個值到底做什麼用的,我也不敢隨意說,官方沒有具體說明,再者它們都是過時的常量,咱們就不細究了。
另外,官網上給的常量值是按照32位來表示的,原始碼上用的是16位來表示的,不過這並沒有什麼影響,我這裡按照原始碼中的來講。
再牛X的博主也有出錯的時候,不要太迷信權威,有歧義的時候最好還是通過實驗來驗證一下比較好。
四、觸控事件與多點觸控
前面我們在處理單點觸控問題的時候,是在onTouchEvent(MotionEvent event)方法中通過使用event.getAction()來獲取事件常量進行判斷的。在Android2.0開始,要獲取多點觸控的事件,需要使用event.getActionMask()。如下所示:
1 @RequiresApi(api = Build.VERSION_CODES.KITKAT) 2 @Override 3 public boolean onTouchEvent(MotionEvent event) { 4 Log.i(TAG, "event=" + MotionEvent.actionToString(event.getActionMasked())); 5 switch (event.getActionMasked()) { 6 ...... 7 } 8 return super.onTouchEvent(event); 9 }
這裡MotionEvent.actionToString(int)是系統提供的方法,可以將int表示的事件轉為字串,方便觀察。方法的原始碼,讀者可以自己去看看,很簡單。
實際上在現在的系統版本中event.getAction()仍然能獲取多指事件,這些獲取的事件在上述表格中有說明,即上表中ACTION_POINTER_1_DOWN到ACTION_POINTER_3_UP,如果手指更多,事件也會更多。但是這個用法在Android2.0開始就被廢棄了,現在需要相容到2.0以下的場景太少了,所以這些過時的做法就不再介紹了,只要知道有這麼回事就可以了。
這一節介紹使用event.getActionMask()方法後獲取的幾個觸控相關的事件。ACTION_DOWN和ACTION_UP前面的文章已經介紹過多次了,前的表格中也有說明,這裡就不贅述了。
1、ACTION_CANCEL
這個事件在整個事件流被中斷時會呼叫,比如父佈局把ACTION_DOWN事件分發給了子View,但後面的MOVE和UP事件卻給攔截時,子View中會產生CANCEL事件。ACTION_CANCEL事件和ACTION_UP事件總有一個會產生,實際上不少場景下會把ACTION_CANCEL當做ACTION_UP對待,來處理當前的事件流。在前面的文章【【朝花夕拾】Android自定義View篇之(六)Android事件分發機制(中)從原始碼分析事件分發邏輯及經常遇到的一些“詭異”現象】的第四節介紹requestDisallowInterceptTouchEvent(true)的作用時,就演示過ACTION_CANCEL的產生,這裡不贅述了,不明白的可以去這篇文章看看。
還有一種常見的情形,ListView的使用場景。當手指觸控ListView時,會把ACTION_DOWN事件分發給ItemView,但是當手指開始滑動時,ListView發現這個時候需要自己消費這個滑動事件了,於是就把後續的MOVE和UP事件給攔截掉。ItemView被調侃了,絕望之下只能呼叫ACTION_CANCEL事件了。
這個事件算是一種比較特殊的事件了。
2、ACTION_OUTSIDE
這個事件比ACTION_CANCEL更特殊,一般很難觸發。官方的介紹說是事件發生UI控制元件邊界之外時觸發,但通過實驗,死活都觸發不了這個事件。事實上這個事件出現的場景比較少見,我目前知道PopWindow和Dialog使用時可能觸發這個場景。這裡簡單介紹一下使用Dialog時觸發該事件的場景。
先自定義一個如下的Dialog:
1 public class CustomDialog extends Dialog { 2 public CustomDialog(Context context) { 3 super(context); 4 init(); 5 } 6 7 @RequiresApi(api = Build.VERSION_CODES.KITKAT) 8 @Override 9 public boolean onTouchEvent(MotionEvent event) { 10 if (MotionEvent.ACTION_OUTSIDE == event.getAction()) { 11 Log.i("songzheweiwang", MotionEvent.actionToString(event.getAction())); 12 } 13 return super.onTouchEvent(event); 14 } 15 16 private void init() { 17 setContentView(R.layout.dialog_outside); 18 //清空原有的flag 19 getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 20 //設定監聽OutSide Touch 21 getWindow().setFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 22 } 23 }
注意第19行和第21行,需要設定相應的flag。
點選介面的對話方塊以外的區域,可以看到如下log(對話方塊的顯示和佈局比較簡單,這裡就不貼出來了):
07-04 07:22:57.719 15647-15647/com.example.demos I/songzheweiwang: ACTION_OUTSIDE
3、ACTION_POINTER_DOWN
第二根手指以及更多的手指觸控時都會觸發這個事件,不能從這個事件中判斷是第幾根手指。每根手指的事件都封裝在MotionEvent中了,要想判斷是第幾根手指,需要結合MotionEvent提供的getActionIndex(),getPointerId(int),findPointerIndex(int)等方法來確定,具體的使用方法後面會做詳細介紹。
4、ACTION_MOVE
無論是哪根手指移動,都會觸發該事件。
5、ACTION_POINTER_UP
只要擡起的手指不是最後一根,就會觸發這個事件,同樣無法直接判斷是第幾根手指擡起來的。
五、獲取事件位置的方法對比
在處理多點觸控的時候,往往需要獲取事件發生點的位置資訊來完成一些效果。MotionEvent提供了多個用於獲取事件位置的方法,一般處理事件是在View中來完成的,View本身也提供了一些判斷自身位置的方法,並且這些方法名稱和功能都非常相似,這導致在實際開發中,很容易混淆。這裡我們簡單瞭解並辨別這些方法的功能,如下表所示:
研究物件 | 方法名稱 | 方法作用說明 |
View | getLeft() | 獲取該View左邊界與直接父佈局左邊界的距離。以直接父佈局左上頂點為原點的座標系為參照。 |
getTop() | 獲取該View上邊界與直接父佈局上邊界的距離。 | |
getX() | 獲取該View左上頂點在座標系上的X座標值。參照的座標系同上。 | |
getY() | 獲取該View左上頂點在座標系上的Y座標值。 | |
MotionEvent | getX() | 獲取事件相對於所在View的X座標值。即以所在View的左上頂點為原點的座標系為參照。 |
getY() | 獲取事件相對於所在View的Y座標值。 | |
getX(int pointerIndex) | 獲取給定pointerIndex的事件的X座標值。該值也是相對於所在View而言的。 | |
getY(int pointerIndex) | 獲取給定pointerIndex的事件的Y座標值。 | |
getRawX() | 獲取事件與螢幕左邊界的距離。即以螢幕左上角為原點的座標系為參照。 | |
getRawY() | 獲取事件與螢幕頂部邊界的距離。 |
通過上表,我們發現,最重要的是要搞清楚各個方法所參照的座標系。為了直觀瞭解各個方法獲取的值的含義,我們參照上面的表格和下圖進行理解。
這其中涉及到的三個座標系分別為:
- View的getX()/getY()/getLeft()/getTop()所參照的,都是以直接父控制元件的左上角頂點為原點的座標系,即圖中標註的座標系。這裡getX()和getLeft(),getY()和getTop()的返回值是一樣的。
- MotionEvent的getX()/getY()/getX(int pointerIndx)/getY(int pointerIndex)所參照的,是以當前所在的View的左上角頂點為原點的座標系。後面兩個方法,是用於多點觸控中獲取對應事件的座標位置的,後面會再講到。
- getRawX()/getRawY()所參照的,是以整個螢幕左上角頂點為原點的座標系。getRawY()的值是包含了標題欄和狀態列高度的。
咱們用資料說話,這裡看看演示結果。自定義一個view,在onTouchEvent方法中打印出上述各個方法獲取的值。
1 public class CustomView extends View { 2 private static final String TAG = "CustomView"; 3 4 public CustomView(Context context, @Nullable AttributeSet attrs) { 5 super(context, attrs); 6 } 7 8 @Override 9 public boolean onTouchEvent(MotionEvent event) { 10 float viewLeft = getLeft(); 11 float viewTop = getTop(); 12 float viewX = getX(); 13 float viewY = getY(); 14 float eventX = event.getX(); 15 float eventY = event.getY(); 16 float rawX = event.getRawX(); 17 float rawY = event.getRawY(); 18 int index = event.getActionIndex(); 19 float pointerX = event.getX(index); 20 float pointerY = event.getY(index); 21 Log.i(TAG, "viewLeft=" + viewLeft + ";viewTop=" + viewTop 22 + ";\n viewX=" + viewX + ";viewY=" + viewY 23 + ";\n eventX=" + eventX + ";eventY=" + eventY 24 + ";\n rawX=" + rawX + ";rawY=" + rawY 25 + ";\n index=" + index + ";pointerX=" + pointerX + ";pointerY=" + pointerY); 26 return super.onTouchEvent(event); 27 } 28 }
佈局效果如前面的截圖所示,
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <com.example.demos.customviewdemo.CustomView 7 android:layout_width="200dp" 8 android:layout_height="200dp" 9 android:layout_centerHorizontal="true" 10 android:layout_marginTop="100dp" 11 android:background="@android:color/darker_gray" /> 12 </RelativeLayout>
觸控介面中的自定義View,抓取ACTION_DOWN事件的log如下所示:
viewLeft=240.0;viewTop=300.0; viewX=240.0;viewY=300.0; eventX=387.0;eventY=424.0; rawX=627.0;rawY=1003.0; index=0;pointerX=387.0;pointerY=424.0
當前的測試機density=3.0,且標題欄和狀態列的高度值之和為279px。通過列印結果中正好rawY = eventY + viewY + 279,和前面給的結論對應上了。
這裡需要注意的是getX()和getY()這個方法,在單點觸控的時候很好理解,因為同時只有一個事件,但在多點觸控中,就不太好理解了。如下是兩個手指觸控捕捉到的log:
ACTION_DOWN viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=0;pointerX=380.0;pointerY=215.0 ACTION_POINTER_DOWN(0) viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=1;pointerX=206.0;pointerY=364.0 ACTION_POINTER_UP(0) viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=0;pointerX=380.0;pointerY=215.0 ACTION_UP viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=206.0;eventY=364.0;rawX=446.0;rawY=943.0;index=0;pointerX=206.0;pointerY=364.0
前三個事件時,eventX和eventY的值是一樣的。ACTION_POINTER_DOWN(0)表示有第二根手指按下了,ACTION_POINTER_UP(0)表示其中一根手指擡起來了。按照我們的理解,另外一個手指按下了,eventX和eventY應該記錄的是第二根手指按下的事件的座標才對,不可能和第一根手指按下的事件座標一樣。所以這裡就是需要著重注意的地方,我們先看看官網API中對它的描述:
public float getX () getX(int) for the first pointer index (may be an arbitrary pointer identifier).
描述中說,該方法獲取的是第一個pointerIndex對應事件的座標,即pointerIndex = 0對應的手指的觸控事件座標(這裡我是根據實驗的結果和官網的說明來下的結論,不保證完全正確,請注意)。括號中也補充說明了,也有可能是一個隨意的Pointer識別符號。看到這裡,我們應該可以明白上述log中的現象了吧。
六、多點觸控重難點
在多點觸控中,最難理解的地方應該是pointerIndex和pointerId的理解和使用了,當然這不僅是難點,也是重點,應該在處理很多多點觸控的問題時,都需要涉及到它們。
1、主要手指和非主要手指
在分析多點觸控時,我們需要先理解兩個概念:主要手指和主要手指。在手指按下時,主要手指是指第一個按下的手指,其它後面按下的手指就是非主要手指。在手指擡起時,主要手指是指最後一個離開螢幕的手指,提前離開的為非主要手指。所以整個過程中,主要手指和非主要手指是會變化的,因為第一個按下的手指很有可能不是最後一個離開螢幕的,“皇帝輪流做,今天到我家”嘛,這一點需要理解清楚!所以ACTION_DOWN和ACTION_UP都是主要手指產生的事件,ACTION_POINTER_DOWN和ACTION_POINTER_UP是非主要手指事件。
2、手指的編碼pointerId
在前面說過,在多點觸控中,除第一根手指外,其他手指按下時,通過getActionMasked()獲得的事件都是ACTION_POINTER_DOWN。那麼,當多個手指同時按在螢幕上,產生的那麼多事件,如何來確定是第幾根手指的事件呢?
系統的解決辦法是:當每一根手指按下時,為其編號!當手指第一次按下時,系統會為這根手指生成一個唯一的編號,我們這裡稱之為pointerId。當這個手指擡起時,或者該事件被攔截了,系統會回收這個編號。當需要檢視某個手指事件相關資訊時,需要通過這個pointerId來找到這個手指。另外,當有手指再次按下時,之前被系統回收的編號可能會再次被使用。
這裡我們需要記住一個結論:只要某根手指沒有離開螢幕,那麼無論中間有多少手指按下擡起,這個手指的pointerId都不會變化(事件被攔截除外)。
3、手指的序號pointerIndex
我們知道了pointerId就像這個手指的身份證一樣重要,但是我們怎樣才能獲取到這個編號呢?很遺憾,系統並沒有提供直接得到這個編號的方法,只有在MotionEvent中提供了一個間接的方式:getPointerId(int pointerIndex)。
現在是不是又有疑問了,這個pointerIndex是什麼?如何獲取?它是做什麼用的?
MotionEvent提供了一個方法,getActionIndex(),通過這個方法可獲取這個pointerIndex的值。繼續看看原始碼:
1 /** 2 * For {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} 3 * as returned by {@link #getActionMasked}, this returns the associated 4 * pointer index. 5 * The index may be used with {@link #getPointerId(int)}, 6 * {@link #getX(int)}, {@link #getY(int)}, {@link #getPressure(int)}, 7 * and {@link #getSize(int)} to get information about the pointer that has 8 * gone down or up. 9 * @return The index associated with the action. 10 */ 11 public final int getActionIndex() { 12 return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK) 13 >> ACTION_POINTER_INDEX_SHIFT; 14 }
通過這段原始碼,我們應該夠窺察到pointerIndex的一些用武之處了吧。再繼續看看方法體中這些方法的資訊:
1 //=============MotionEvent.java=============== 2 ...... 3 public static final int ACTION_POINTER_INDEX_MASK = 0xff00; 4 public static final int ACTION_POINTER_INDEX_SHIFT = 8; 5 private static native int nativeGetAction(long nativePtr); 6 /** 7 *...... 8 * Consider using {@link #getActionMasked} 9 *...... 9 */ 10 public final int getAction() { 11 return nativeGetAction(mNativePtr); 12 } 13 ......
看到這裡就明白了,pointerIndex實際上就是getAction()獲取的事件值取高8位得到的。getAction()的註釋中也說得很明白,建議使用getActionMasked()方法來獲取事件,繼續看看它的原始碼:
1 //===========MotionEvent.java========== 2 ...... 3 public static final int ACTION_MASK = 0xff; 4 /** 5 * Return the masked action being performed, without pointer index information. 6 * Use {@link #getActionIndex} to return the index associated with pointer actions. 7 * @return The action, such as {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}. 8 */ 9 public final int getActionMasked() { 10 return nativeGetAction(mNativePtr) & ACTION_MASK; 11 } 12 ......
我們又發現,系統建議使用的getActionMasked()方法,得到的事件,實際上是getAction()得到的值的低8位表示的。
現在我們明白了,getActionMasked()和getActionIndex()的值分別就是getAction()的低8位和高8位兩個部分。這種用一個int來儲存兩個資訊的做法,在Android原始碼中比較常見,因為pointerIndex和action的範圍都很少,單獨給每一個分配一個空間,比較浪費。在前面的文章【【朝花夕拾】Android自定義View篇之(一)View繪製流程】中,MeasureSpec就是將Mode和Size整合在一起的例子。到這裡,我們就清楚了pointerIndex的來歷了。
結合ACTION_POINTER_X_DOWN/UP的值以及對應事件的說明,就能清楚pointerIndex表示的是按下/擡起事件對應手指的序號(正好對應上了這個X值)。那麼既然有了pointerIndex了,為啥還要多此一舉再搞一個pointerId呢?我總結了一下,大概有兩點原因:
(1)現在假設一種場景,食指和中指依次按下,那麼通過前面pointerIndex的計算方法,它們的pointerIndex的值分別就是0和1了;在擡起的時候如果也是食指先擡起中指後擡起,那麼食指觸發的事件為ACTION_POINTER_UP,中指觸發的事件為ACTION_UP了,此時食指和中指對應的index就分別變成了1和0了。同一根手指在這個過程中的pointerIndex值變了,可見這個值是動態變化的,我們前面給過一個結論,同一根手指在按下到擡起整個過程中pointerId值是不會變化的,pointerId更穩定。
(2)我們前面也說過,任何一根手指在移動的時候,響應的事件都是ACTION_MOVE,而ACTION_MOVE = 2,經過getActionIndex()計算,得到的pointerIndex值為0,根本無法區分哪根手指,可見在ACTION_MOVE事件中這個值是失效的。而我們知道,在很多場景下我們需要在ACTION_MOVE事件中做事情,關鍵時刻pointerIndex卻掉鏈子了。在getActionIndex()的原始碼註釋中也做了說明,它用於ACTION_POINTER_DOWN和ACTION_POINTER_UP事件。此時就需要用pointerId來追蹤事件流了。
我們可以這樣理解,pointerId是觸控手指的身份證,而pointerIndex是住址,住址可能經常變動,在四處奔波中可能連有效住址都沒有,但身份證就是跟隨一輩子不變化的,這樣是不是好記憶多了。這裡再簡單總結一下它的特點:1)pointerIndex是不固定的;2)pointerIndex對多點觸控的down和up事件有效,對move事件無效。
4、pointerId的複用和pointerIndex變化舉例
這裡,我們通過A,B,C三根手指的按下和擡起,來觀察這兩個值的變化情況:
事件 | 手指數量 | pointerIndex及pointerId變化 |
A手指按下 | 1 | A手指pointerIndex=0,pointerId=0 |
B手指按下 | 2 | A手指pointerIndex=0,pointerId=0;B手指pointerIndex=1,pointerId=1 |
A手指擡起 | 1 | B手指pointerIndex=0,pointerId=1 |
C手指按下 | 2 | C手指pointerIndex=0,pointerId=0;B手指pointerIndex=1,pointerId=1 |
當A手指擡起後,B手指的pointerIndex從1變成了0;當C手指按下後,B手指的pointerIndex又從0變成了1;B手指的pointerId一直是1,沒有變化。C手指按下,C複用了A手指被系統回收的pointerId,值為0。現在應該能夠有個直觀的感受了吧。而且我們還能得到幾個變化規律:
1)按下手指時,從0開始自動增長。
2)如果之前按下的手指擡起,後面的手指會隨之減小。
3)無論手指如何變化,當前還在螢幕上的手指的pointerIndex,都是從0開始的連續序列值。
4)剛按下的手指,如果前面的pointerId序列中有空缺,會按照該值的大小由小到大填補前面的空缺,且該手指初始時pointerIndex和pointerId值相等。如果前面pointerId沒有空缺,則往後面新增。
5)當有手指擡起,後來又有手指按下,之前留下的手指的pointerIndex變化會趨向於自己第一次按下時的數值,也就是趨向於自己的pointerId值變化。
還有更多的規律,讀者可以自己總結。最後再看一組圖示來理解一下這個變化過程:
5、多點觸控常見的幾個方法
除了前提到的getActionMasked()和getActionId()外,MotionEvent類還提供瞭如下幾個常用的方法,用於處理多點觸控和獲取不同手指的資訊。
(1)getPointerCounter()
作用:獲取在螢幕上手指的個數
1 /** 2 * The number of pointers of data contained in this event. Always 3 * >= 1. 4 */ 5 public final int getPointerCount() { 6 return nativeGetPointerCount(mNativePtr); 7 } 8 ...... 9 private static native int nativeGetPointerCount(long nativePtr);
(2)getPointerId(int pointerIndex)
作用:獲取手指的唯一識別符號ID
1 public final int getPointerId(int pointerIndex) { 2 return nativeGetPointerId(mNativePtr, pointerIndex); 3 } 4 .... 5 private static native int nativeGetPointerId(long nativePtr, int pointerIndex);
(3)findPointerIndex(int pointerId)
作用:通過pointerId獲取pointerIndex,然後根據pointerIndex來獲取該手指事件的相關資訊
1 public final int findPointerIndex(int pointerId) { 2 return nativeFindPointerIndex(mNativePtr, pointerId); 3 } 4 ...... 5 private static native int nativeFindPointerIndex(long nativePtr, int pointerId);
(4)getX(int pointerIndex)
作用:獲取給定pointerIndex對應手指的X座標。
1 public final float getX(int pointerIndex) { 2 return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT); 3 } 4 ...... 5 private static native float nativeGetAxisValue(long nativePtr, 6 int axis, int pointerIndex, int historyPos);
(5)getY(int pointerIndex)
作用:獲取給定pointerIndex對應手指的Y座標。
1 public final float getY(int pointerIndex) { 2 return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT); 3 } 4 ...... 5 private static native float nativeGetAxisValue(long nativePtr, 6 int axis, int pointerIndex, int historyPos);
從如上的方法可以看出,在獲取指定手指的事件資訊時,都是通過引數pointerIndex來確定的。我們前面說過pointerIndex就像是家庭住址,pointerId就像身份證號,要找到某個人需要通過他的家庭住址來找,而不是身份證號,這樣就容易理解了。另外,這幾個方法都是直接呼叫了native方法,可見觸控事件和底層的依賴程度。
當然,MotionEvent類還提供了很多用於獲取歷史事件,事件時間,壓力大小等的方法,讀者可以通過下面的參考文章中瞭解詳細的使用和功能。
參看文章
【安卓自定義View進階-MotionEvent詳解】
【安卓自定義View進階-多點觸控詳解】
【電容式觸控式螢幕】
【MotionEvent】
本部分主要介紹基礎和理論部分知識,接下來會通過練習和demo來加強理解。同樣,如果本文有描述不妥或者不準確的地方,歡迎來拍磚,感