Android開發時的多點觸控是如何實現的?

對於Android自定義控件開發,多點觸控是一個必須要懂的知識點。因爲在正常的情況下操作正常的控件,使用多指操作時,基本上都會出現問題。當需要對多指操作進行兼容時,就需要這方面的知識了。

本文選自《Android自定義控件高級進階與精彩實例》一書,帶你瞭解多點觸控的基本知識。


—— 正文 ——

假如,我們做了這麼一個功能,圖像跟隨手指移動。

在單指操作下,圖像的移動非常流暢、正確,而如果我們使用兩根手指的話,就會出現下面這種情況。

從效果圖可以看出,在第2根手指放下,而第1根手指擡起時,圖像會出現跳躍,直接從第1根手指的位置移動到了第2根手指的位置,這明顯是不對的。這只是一個簡單的例子,一般使用單指操作的控件改到多指操作的時候,都會出現問題。

這便是本文講解多點觸控的初衷。既然多點觸控會造成這麼多問題,那麼下面就來詳細瞭解它吧。

單點觸控與多點觸控

1
單點觸控

單點觸控與多點觸控是相對的,單點觸控的意思是,我們只考慮一根手指的情況,而且僅處理一根手指的觸摸事件,而多點觸控是處理多根手指的觸摸事件。

一般我們處理MotionEvent事件,通過MotionEvent.getAction來獲取事件類型,這就是單點觸控。在單點觸控中,會涉及對下面幾個消息的處理。

除了消息外,我們也經常用下面這幾個函數來獲取手指的位置等信息,這些函數都沒有參數,也都只有在單點觸控時才能使用。

對於這幾個函數的使用方法,這裏就不再贅述了。可以看到,我們平常所處理的MotionEvent事件,以及常用的MotionEvent函數都只是針對單點觸控的,那麼哪些纔是多點觸控的事件和函數呢?

2
多點觸控

首先,多點觸控的消息類型只能通過getActionMasked來獲取。因此,判斷當前代碼處理的是單點觸控還是多點觸控,單從獲取消息類型的函數就可以看出。

說明:單點觸控是通過getAction來獲取當前事件類型的,而多點觸控是通過getActionMasked來獲取的。

多點觸控涉及的消息類型與單點觸控的不一樣,它的消息類型如下。

比如以下圖中的手指按下順序,我們來看看其中的事件觸發順序。

在效果圖中,先後有3根手指按下,按下順序是1、2、3,擡起順序是1、3、2,而事件觸發順序如下表。

這裏需要注意,

第1根手指按下時,收到的消息是ACTION_DOWN;

隨後的手指再按下時,收到的是ACTION_POINTER_DOWN;

當有手指擡起時,收到的是ACTION_POINTER_UP;

當最後一根手指擡起時,收到的是ACTION_UP。

對多點觸控消息進行處理的代碼如下:

 1String TAG = "qijian";
 2@Override
 3public boolean onTouchEvent(MotionEvent event) {
 4    switch (event.getActionMasked()) {
 5    case MotionEvent.ACTION_DOWN:
 6        Log.e(TAG,"第1根手指按下");
 7        break;
 8    case MotionEvent.ACTION_UP:
 9        Log.e(TAG,"最後一根手指擡起");
10        break;
11    case MotionEvent.ACTION_POINTER_DOWN:
12        Log.e(TAG,"又一根手指按下");
13        break;
14    case MotionEvent.ACTION_POINTER_UP:
15        Log.e(TAG,"又一根手指擡起");
16        break;
17    }
18    return true;
19}
20...
21    }

這裏僅列出了手指按下和手指擡起所觸發的消息類型,而在手指移動時,無論是單點觸控還是多點觸控,所觸發的消息都是MotionEvent.ACTION_MOVE。

在多點觸控時,我們可以通過代碼來獲取當前移動的是哪根手指。

多點觸控

1
識別按下的手指

上面講解了在什麼情況下會觸發什麼消息,但我們怎麼來識別當前按下的是哪根手指呢?

在MotionEvent中有一個Pointer的概念:

一個Pointer就代表一個觸摸點,每個Pointer都有自己的消息類型,也有自己的X座標值。一個MotionEvent對象中可能會存儲多個Pointer的相關信息,每個Pointer都有自己的PointerIndex和PointerId。在多點觸控中,就是用PointerIndex和PointerId來標識用戶手指的。

  • PointerIndex表示當前手指的索引,PointerId是手指按下時分配的唯一id,用來標識這根手指。
  • 每根手指從按下、移動到離開屏幕,PointerId是不變的,而PointerIndex則不是固定的。

通過下面這個例子,我們來了解一下PointerIndex與PointerId的區別。

可見同一根手指的id是不變的,而PointerIndex是會變化的,但總是以0、1或者0、1、2這樣的形式出現,而不可能出現0、2這樣間隔了一個數或者1、2這種沒有0索引值的形式。

針對PointerIndex與PointerId,在MotionEvent類中經常使用下面這幾個函數。

  • public final int getActionIndex:

用於獲取當前活動手指的PointerIndex值。

  • public final int getPointerId(int pointerIndex):

用於根據PointerIndex值獲取手指的PointerId,其中pointerIndex表示手指的PointerIndex值。

  • public final int getPointerCount:

用於獲取用戶按下的手指個數,一般我們用它來遍歷屏幕上的所有手指,遍歷手指的代碼如下:

1for (int i = 0; i < event.getPointerCount(); i++) {
2    int pointerId = event.getPointerId(i);
3}

前面講過,PointerIndex是從0開始的,表示當前所有手指的索引,值從0到getPointerCount() − 1,不會出現不連續的數。因此,我們通過event.getPointerCount可以得到當前屏幕上的手指個數。然後從0開始遍歷PointerIndex,同時我們還能通過int pointerId = event.getPointerId(i)來得到每根手指PointerIndex所對應的PointerId。

  • public final int findPointerIndex(int pointerId):

用於根據PointerId反向找到手指的PointerIndex值。

由此,我們就知道了PointerIndex與PointerId的關係,以及它們相互之間的換算方法。下面再來看看通過PointerIndex和PointerId能得到什麼。

2
獲取手指位置信息

通過PointerIndex與PointerId,可以使用以下函數獲得手指的位置信息。

  • public final float getX(int pointerIndex):

根據PointerIndex得到對應手指的X座標值,該函數的意義與單點觸控裏的getX函數相同。

  • public final float getY(int pointerIndex):

同樣地,根據PointerIndex得到對應手指的Y座標值,該函數的意義與單點觸控裏的getY函數相同。

實例:追蹤第2根手指

現在,我們將通過一個實例來學習上面講到的函數。

這裏實現的效果是:當用戶按下第2根手指時,就開始追蹤這根手指,無論其他手指是否擡起,只要這根手指沒有擡起,就一直顯示這根手指的位置,如下如。

從效果圖可以看出,先後總共按下了3根手指,分別在左(第1根手指)、中(第2根手指)、右(第3根手指)。

擡起手指時,先擡起左側第1根手指,然後擡起右側第3根手指。可以看到,第2根手指的觸摸點,我們使用白色圓圈顯示,無論第3根手指是否按下,還是其他手指是否擡起,白色圓圈總是跟着第2根手指的移動來顯示。這就實現了跟蹤第2根手指軌跡的效果。

下面我們來看看這個效果是怎麼實現的吧。

1
自定義View並初始化

佈局很簡單,就是一個全屏View,爲了在View上畫圓圈,我們必須自定義View,其中的初始化代碼如下:

 1public class MultiTouchView extends View {
 2    // 用於判斷第2根手指是否存在
 3    private boolean haveSecondPoint = false;
 4    // 記錄第2根手指的位置
 5    private PointF point = new PointF(0, 0);
 6    private Paint mDefaultPaint = new Paint();
 7
 8    public MultiTouchView(Context context) {
 9        super(context);
10        init();
11    }
12
13    public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
14        super(context, attrs);
15        init();
16    }
17
18    public MultiTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
19        super(context, attrs, defStyleAttr);
20        init();
21    }
22
23    private void init() {
24        mDefaultPaint.setColor(Color.WHITE);
25        mDefaultPaint.setAntiAlias(true);
26        mDefaultPaint.setTextAlign(Paint.Align.CENTER);
27        mDefaultPaint.setTextSize(30);
28    }
29}

這樣我們就自定義了一個View,很明顯它內部不會再包裹其他的View控件,所以繼承自View類即可。

我們定義了3個變量,其中:

  • haveSecondPoint用於判斷第2根手指是否按下。
  • point用於記錄第2根手指的位置。
  • mDefaultPaint是畫筆變量,用於畫第2根手指位置處的白色圓圈。

2
onTouchEvent

然後,在用戶按下手指時,需要加以判斷,當前是第幾根手指,然後獲取第2根手指的位置,下面列出完整代碼:

 1public boolean onTouchEvent(MotionEvent event) {
 2    int index = event.getActionIndex();
 3
 4    switch (event.getActionMasked()) {
 5    case MotionEvent.ACTION_POINTER_DOWN:
 6        if (event.getPointerId(index) == 1) {
 7            haveSecondPoint = true;
 8            point.set(event.getX(), event.getY());
 9        }
10        break;
11    case MotionEvent.ACTION_MOVE:
12        try {
13            if (haveSecondPoint) {
14                int pointerIndex = event.findPointerIndex(1);
15                point.set(event.getX(pointerIndex), event.getY(pointerIndex));
16            }
17        } catch (Exception e) {
18            haveSecondPoint = false;
19        }
20        break;
21    case MotionEvent.ACTION_POINTER_UP:
22        if (event.getPointerId(index) == 1) {
23            haveSecondPoint = false;
24        }
25        break;
26    case MotionEvent.ACTION_UP:
27        haveSecondPoint = false;
28        break;
29    }
30
31    invalidate();
32    return true;
33}

獲取當前活動手指的PointerIndex值:

1int index = event.getActionIndex();

我們知道,當第1根手指按下的時候觸發的是ACTION_DOWN消息,隨後的手指按下的時候觸發的都是ACTION_POINTER_DOWN消息。因爲我們要跟蹤第2根手指,所以這裏只需要識別ACTION_POINTER_DOWN消息即可:

1case MotionEvent.ACTION_POINTER_DOWN:
2    if (event.getPointerId(index) == 1) {
3        haveSecondPoint = true;
4        point.set(event.getX(), event.getY());
5    }
6    break;

我們也知道PointerIndex是變化的,而PointerId是不變的,PointerId根據手指按下的順序從0到1逐漸增加。因此,第2根手指的PointerId就是1。當(event.getPointerId(index) == 1時,就表示當前按下的是第2根手指,將haveSecondPoint設爲true,並將得到的第2根手指的位置設置到point中。

到這裏,大家可能會產生疑問,上面提到的多點觸控獲取手指位置都用的是event.getX(pointerIndex),而這裏怎麼直接用event.getX了呢?其實這裏使用event.getX (pointerIndex)也是可以的,大家可以先記下這個問題,後面我們再詳細講解。

當手指移動時,會觸發ACTION_MOVE消息:

 1case MotionEvent.ACTION_MOVE:
 2    try {
 3        if (haveSecondPoint) {
 4            int pointerIndex = event.findPointerIndex(1);
 5            point.set(event.getX(pointerIndex), event.getY(pointerIndex));
 6        }
 7    } catch (Exception e) {
 8        haveSecondPoint = false;
 9    }
10    break;

需要注意,因爲這裏使用event.findPointerIndex(1)來強制獲取PointerId爲1的手指PointerIndex,在異常情況下可能出現越界,所以使用try…catch…來進行保護。

在這裏,我們使用event.getX(pointerIndex)來獲取指定手指的位置信息。同樣地,這個問題也放在後面講解。

當手指擡起時,會觸發ACTION_POINTER_UP消息:

1case MotionEvent.ACTION_POINTER_UP:
2    if (event.getPointerId(index) == 1) {
3        haveSecondPoint = false;
4    }
5    break;

同樣地,使用event.getPointerId(index)來獲取當前擡起手指的PointerId,如果是1,那就說明是第2根手指擡起了,這時就把haveSecondPoint設爲false。

當全部手指擡起時,會觸發ACTION_UP消息:

1case MotionEvent.ACTION_UP:
2    haveSecondPoint = false;
3    break;

在最後一根手指擡起時,把haveSecondPoint設爲false,白色圓圈從屏幕上消失。

最後,調用invalidate();來重繪界面。

3
onDraw

在重繪界面時,主要是在point中存儲的第2根手指的位置處畫一個白色圓圈:

 1protected void onDraw(Canvas canvas) {
 2
 3    canvas.drawColor(Color.GREEN);
 4    if (haveSecondPoint) {
 5        canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);
 6    }
 7
 8    canvas.save();
 9    canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
10    canvas.drawText("追蹤第2個按下手指的位置", 0, 0, mDefaultPaint);
11    canvas.restore();
12}

首先,爲整個屏幕繪一層綠色,把上一屏的內容清掉:

1canvas.drawColor(Color.GREEN);

然後,如果第2根手指按下了,則在它的位置處畫一個圓圈:

1if (haveSecondPoint) {
2    canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);
3}

最後,在佈局的中間位置寫上提示文字:

1canvas.save();
2canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
3canvas.drawText("追蹤第2個按下手指的位置", 0, 0, mDefaultPaint);
4canvas.restore();

有關Canvas的操作及寫字的操作,在《Android自定義控件開發入門與實戰》一書中有詳細章節講述,這裏就不再贅述了。

在寫好控件以後,直接利用XML引入佈局即可,這裏不再展示,效果就是我們想要的樣子。

<u style="margin: 0px; padding: 0px; border: 0px;">關於作者</u>

啓艦

本名張恩偉,Android研發專家、CSDN博客專家、CSDN博客之星,《Android自定義控件入門與實戰》《Android自定義控件高級進階與精彩實例》作者,電子工業出版社博文視點優秀作者,曾就職於阿里巴巴,現就職於vivo。

圖書推薦

▊《Android自定義控件高級進階與精彩實例》

啓艦 著

  • 專注於介紹Android自定義控件進階知識
  • 通過精彩的案例對各種繪製、動畫技術進行了糅合講解

本書主要內容有3D特效的實現、高級矩陣知識、消息處理機制、派生類型的選擇方法、多點觸控及輔助類、RecyclerView的使用方法及3D卡片的實現、動畫框架Lottie的講解與實戰等。

讀者可以通過本書從宏觀層面、源碼層面對Android自定義控件建立完整的認識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章