【朝花夕拾】Android自定義View篇之(八)多點觸控

宋者為王發表於2019-07-09

前言

       轉載請宣告,轉自【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來加強理解。同樣,如果本文有描述不妥或者不準確的地方,歡迎來拍磚,感謝!

相關文章