安卓自定义View进阶-多点触控详解
Android 多點(diǎn)觸控詳解,在前面的幾篇文章中我們大致了解了 Android 中的事件處理流程和一些簡單的處理方案,本次帶大家了解 Android 多點(diǎn)觸控相關(guān)的一些知識(shí)。
多點(diǎn)觸控 ( Multitouch,也稱 Multi-touch ),即同時(shí)接受屏幕上多個(gè)點(diǎn)的人機(jī)交互操作,多點(diǎn)觸控是從 Android 2.0 開始引入的功能,在 Android 2.2 時(shí)對(duì)這一部分進(jìn)行了重新設(shè)計(jì)。
在本文開始之前,先回顧一下 MotionEvent詳解 中提到過的內(nèi)容:
- Android 將所有的事件都封裝進(jìn)了 Motionvent 中。
- 我們可以通過復(fù)寫 onTouchEvent 或者設(shè)置 OnTouchListener 來獲取 View 的事件。
- 多點(diǎn)觸控獲取事件類型請(qǐng)使用 getActionMasked() 。
- 追蹤事件流請(qǐng)使用 PointId。
多點(diǎn)觸控相關(guān)的事件:
| ACTION_DOWN | 第一個(gè) 手指 初次接觸到屏幕 時(shí)觸發(fā)。 |
| ACTION_MOVE | 手指 在屏幕上滑動(dòng) 時(shí)觸發(fā),會(huì)多次觸發(fā)。 |
| ACTION_UP | 最后一個(gè) 手指 離開屏幕 時(shí)觸發(fā)。 |
| ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已經(jīng)有手指在屏幕上)。 |
| ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。 |
| 以下事件類型不推薦使用 | ---以下事件在 2.2 版本以上被標(biāo)記為廢棄--- |
| ACTION_POINTER_1_DOWN | 第 2 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_2_DOWN | 第 3 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_3_DOWN | 第 4 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_1_UP | 第 2 個(gè)手指抬起,已廢棄,不推薦使用。 |
| ACTION_POINTER_2_UP | 第 3 個(gè)手指抬起,已廢棄,不推薦使用。 |
| ACTION_POINTER_3_UP | 第 4 個(gè)手指抬起,已廢棄,不推薦使用。 |
多點(diǎn)觸控相關(guān)的方法:
| getActionMasked() | 與 getAction() 類似,多點(diǎn)觸控需要使用這個(gè)方法獲取事件類型。 |
| getActionIndex() | 獲取該事件是哪個(gè)指針(手指)產(chǎn)生的。 |
| getPointerCount() | 獲取在屏幕上手指的個(gè)數(shù)。 |
| getPointerId(int pointerIndex) | 獲取一個(gè)指針(手指)的唯一標(biāo)識(shí)符ID,在手指按下和抬起之間ID始終不變。 |
| findPointerIndex(int pointerId) | 通過PointerId獲取到當(dāng)前狀態(tài)下PointIndex,之后通過PointIndex獲取其他內(nèi)容。 |
| getX(int pointerIndex) | 獲取某一個(gè)指針(手指)的X坐標(biāo) |
| getY(int pointerIndex) | 獲取某一個(gè)指針(手指)的Y坐標(biāo) |
回顧完畢,開始正文。
一、多點(diǎn)觸控相關(guān)問題
在引入多點(diǎn)觸控之前,事件的類型很少,基本事件類型只有按下(down)、移動(dòng)(move) 和 抬起(up),即便加上那些特殊的事件類型也只有幾種而已,所以我們可以用幾個(gè)常量來標(biāo)記這些事件,在使用的時(shí)候使用 getAction() 方法來獲取具體的事件,之后和這些常量進(jìn)行對(duì)比就行了。
在 Android 2.0 版本的時(shí)候,開始引入多點(diǎn)觸控技術(shù),由于技術(shù)上并不成熟,硬件和驅(qū)動(dòng)也跟不上,多數(shù)設(shè)備只能支持追蹤兩三個(gè)點(diǎn)而已,因此在設(shè)計(jì) API 上采取了一種簡單粗暴的方案,添加了幾個(gè)常量用于多點(diǎn)觸控的事件類型的判斷。
| ACTION_POINTER_1_DOWN | 第 2 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_2_DOWN | 第 3 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_3_DOWN | 第 4 個(gè)手指按下,已廢棄,不推薦使用。 |
| ACTION_POINTER_1_UP | 第 2 個(gè)手指抬起,已廢棄,不推薦使用。 |
| ACTION_POINTER_2_UP | 第 3 個(gè)手指抬起,已廢棄,不推薦使用。 |
| ACTION_POINTER_3_UP | 第 4 個(gè)手指抬起,已廢棄,不推薦使用。 |
這些事件類型是用來判斷非主要手指(第一個(gè)按下的稱為主要手指)的按下和抬起,使用起來大概是這樣子:
switch (event.getAction()) {case MotionEvent.ACTION_DOWN: break;case MotionEvent.ACTION_UP: break;case MotionEvent.ACTION_MOVE: break;case MotionEvent.ACTION_POINTER_1_DOWN: break;case MotionEvent.ACTION_POINTER_2_DOWN: break;case MotionEvent.ACTION_POINTER_3_DOWN: break;case MotionEvent.ACTION_POINTER_1_UP: break;case MotionEvent.ACTION_POINTER_2_UP: break;case MotionEvent.ACTION_POINTER_3_UP: break; }看到這里可能會(huì)產(chǎn)生以下的一些疑問?
1.為什么沒有 ACTION_POINTER_X_MOVE ?
在多指觸控中所有的移動(dòng)事件都是使用 ACTION_MOVE, 并沒有追蹤某一個(gè)手指的 move 事件類型,個(gè)人猜測主要是因?yàn)?#xff1a;很難無歧義的實(shí)現(xiàn)單獨(dú)追蹤每一個(gè)手指。
要理解這個(gè),首先要明白設(shè)備是如何識(shí)別多點(diǎn)觸控的,設(shè)備沒有眼睛,不能像我們?nèi)艘粯涌吹接袔讉€(gè)手指(或者觸控筆)在屏幕上。
目前大多數(shù) Android 設(shè)備都是電容屏,它們感知觸摸是利用手指(觸控筆)與屏幕接觸產(chǎn)生的微小電流變化,之后通過計(jì)算這些電流變化來得出具體的觸摸位置,在多點(diǎn)觸控中,當(dāng)兩個(gè)觸摸點(diǎn)足夠靠近時(shí),設(shè)備實(shí)際上是無法分清這兩個(gè)點(diǎn)的。因此當(dāng)兩個(gè)觸摸點(diǎn)靠近(重合)后再分開,設(shè)備很可能就無法正確的追蹤兩個(gè)點(diǎn)了,所以也很難實(shí)現(xiàn)無歧義的追蹤每一個(gè)點(diǎn)。
并且從軟件上來說,事件的編號(hào)產(chǎn)生和復(fù)用也是一個(gè)大問題,例如下面的場景:
| 一個(gè)手指按下(命名為A) | 1 | A手指的編號(hào)為0,id為0 |
| 一個(gè)手指按下(命名為B) | 2 | B手指的編號(hào)為1,id為1 |
| A手指抬起 | 1 | B手指編號(hào)變更為0,id不變?yōu)? |
| 一個(gè)手指按下(命名為C) | 2 | C手指編號(hào)為0,id為0,B手指編號(hào)為1,id為1 |
注意觀察上面編號(hào)和id的變化,有兩個(gè)問題,1、B手指的編號(hào)變化了。2、A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。所以這就引出了一個(gè)問題:如果存在 ACTION_POINTER_X_MOVE,那么X應(yīng)該用什么標(biāo)志呢?編號(hào)會(huì)變化,id雖然不會(huì)變化,但id會(huì)被復(fù)用,例如A手指抬起后C手指按下,C手指復(fù)用了A手指的id。所以不論使用哪一個(gè)都不能保證唯一性。
當(dāng)然了,解決問題最好的方式就是把問題拋出去,既然從硬件和軟件上都不能保證唯一性和不變性,就不做區(qū)分了,因此所有的 move 事件都是 ACTION_MOVE, 具體是哪個(gè)手指產(chǎn)生的 move 用戶可以結(jié)合其他事件(按下和抬起)來綜合判斷。
2.超過4個(gè)手指怎么辦?
2.0 兼容版,在2.2 之前的設(shè)計(jì)中,其提供的常量最多能判斷四個(gè)手指的抬起和落下,當(dāng)超過四個(gè)手指時(shí)怎么辦呢?
由于在 2.2 版本之前,由于沒有 getActionMasked 方法,我們可以自己自己手動(dòng)進(jìn)行計(jì)算,例如下面這樣 :
String TAG = "Gcs";int action = event.getAction() & MotionEvent.ACTION_MASK; int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;switch (action) {case MotionEvent.ACTION_DOWN:Log.e(TAG,"第1個(gè)手指按下");break;case MotionEvent.ACTION_UP:Log.e(TAG,"最后1個(gè)手指抬起");break;case MotionEvent.ACTION_POINTER_1_DOWN: // 此時(shí)相當(dāng)于 ACTION_POINTER_DOWNLog.e(TAG,"第"+(index+1)+"個(gè)手指按下");break;case MotionEvent.ACTION_POINTER_1_UP: // 此時(shí)相當(dāng)于 ACTION_POINTER_UPLog.e(TAG,"第"+(index+1)+"個(gè)手指抬起");break; }在上面的例子中有幾點(diǎn)比較關(guān)鍵:
2.1、action 與 Index 的獲得
我們?cè)?MotionEvent詳解 中了解過,Android中的事件一般用最后8位來表示事件類型,再往前8位來表示Index。
例如多指觸控的按下事件,其事件類型是 0x00000005, 其Index標(biāo)志位是 0x00000005,隨著更多的手指按下,其中變化的部分是 Index 標(biāo)志位,最后兩位是始終不變的,所以我們只要能將這兩個(gè)分離開就行了。
取得事件類型(action)
// 獲取事件類型 int action = event.getAction() & MotionEvent.ACTION_MASK;這個(gè)非常簡單,ACTION_MASK=0x000000ff, 與 getAction() 進(jìn)行按位與操作后保留最后8位內(nèi)容(十六進(jìn)制每一個(gè)字符轉(zhuǎn)化為二進(jìn)制是4位)。
例如:
0x00000105 & 0x000000ff = 0x00000005
取得事件索引(index)
// 獲取index編號(hào) int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;ACTION_POINTER_INDEX_MASK = 0x0000ff00
ACTION_POINTER_INDEX_SHIFT = 8
首先讓 getAction() 與 ACTION_POINTER_INDEX_MASK 按位與之后,只保留 Index 那8位,之后再右移8位,最終就拿到了 Index 的真實(shí)數(shù)值。
例如:
0x00000105 & 0x0000ff00 = 0x00000100
0x00000100?? 8 = 0x00000001
2.2、用 ACTION_POINTER_1_DOWN 代替 ACTION_POINTER_DOWN
這是因?yàn)樵?2.0 版本的時(shí)候還沒有 ACTION_POINTER_DOWN 的這個(gè)常量,但是它們兩個(gè)點(diǎn)數(shù)值是相同的,都是 0x00000005,這個(gè)你可以查看官方文檔或者源碼,甚至你直接寫 case 0x00000005 也行,抬起也是同理。
2.3、只考慮兼容 2.2 以上的版本
當(dāng)然了,如果你不需要兼容 2.0 版本,只需要兼容到 2.2 以上的話就很簡單了,像下面這樣:
String TAG = "Gcs";int index = event.getActionIndex();switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:Log.e(TAG,"第1個(gè)手指按下");break;case MotionEvent.ACTION_UP:Log.e(TAG,"最后1個(gè)手指抬起");break;case MotionEvent.ACTION_POINTER_DOWN:Log.e(TAG,"第"+(index+1)+"個(gè)手指按下");break;case MotionEvent.ACTION_POINTER_UP:Log.e(TAG,"第"+(index+1)+"個(gè)手指抬起");break; }3. index 和 pointId 的變化規(guī)則
在 2.2 版本以上,我們可以通過 getActionIndex() 輕松獲取到事件的索引(Index),但是這個(gè)事件索引的變化還是有點(diǎn)意思的,Index 變化有以下幾個(gè)特點(diǎn):
1、從 0 開始,自動(dòng)增長。
2、如果之前落下的手指抬起,后面手指的 Index 會(huì)隨之減小。
3、Index 變化趨向于第一次落下的數(shù)值(落下手指時(shí),前面有空缺會(huì)優(yōu)先填補(bǔ)空缺)。
4、對(duì) move 事件無效。
下面我們逐條解釋一下具體含義。
3.1、從 0 開始,自動(dòng)增長。
這一條非常簡單,也很容易理解,而且在 MotionEvent詳解 中講解 getAction() 與 getActionMasked() 也簡單說過。
| 第1個(gè)手指按下 | ACTION_DOWN (0x00000000) |
| 第2個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000105) |
| 第3個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000205) |
| 第4個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000305) |
注意加粗的位置,數(shù)值隨著手指按下而不斷變大。
3.2、如果之前落下的手指抬起,后面手指的 Index 會(huì)隨之減小。
這個(gè)也比較容易理解,像下面這樣:
| 第1個(gè)手指按下 | ACTION_DOWN (0x00000000) |
| 第2個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000105) |
| 第3個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000205) |
| 第2個(gè)手指抬起 | ACTION_POINTER_UP (0x00000106) |
| 第3個(gè)手指抬起 | ACTION_POINTER_UP (0x00000106) |
注意最后兩次觸發(fā)的事件,它的 Index 都是 1,這樣也比較容易解釋,當(dāng)原本的第 2 個(gè)手指抬起后,屏幕上就只剩下兩個(gè)手指了,之前的第 3 個(gè)手指就變成了第 2 個(gè),于是抬起時(shí)觸發(fā)事件的 Index 為 1,即之前落下的手指抬起,后面手指的 Index 會(huì)隨之減小。
3.3、Index 變化趨向于第一次落下的數(shù)值(落下手指時(shí),前面有空缺會(huì)優(yōu)先填補(bǔ)空缺)。
這個(gè)就有點(diǎn)神奇了,通過上一條規(guī)則,我們知道,某一個(gè)手指的 Index 可能會(huì)隨著其他手指的抬起而變小,這次我們用 4 個(gè)手指測試一下 Index 的變化趨勢(shì)。
| 第1個(gè)手指按下 | ACTION_DOWN (0x00000000) |
| 第2個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000105) |
| 第3個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000205) |
| 第2個(gè)手指抬起 | ACTION_POINTER_UP (0x00000106) |
| 第3個(gè)手指抬起 | ACTION_POINTER_UP (0x00000106) |
| 第4個(gè)手指按下 | ACTION_POINTER_DOWN (0x00000105) |
| 第3個(gè)手指抬起 | ACTION_POINTER_UP (0x00000206) |
這個(gè)要和上一個(gè)對(duì)比這看,重點(diǎn)觀察第 3 個(gè)手指所觸發(fā)事件區(qū)別,在上一個(gè)示例中,隨著第 2 個(gè)手指的抬起,第 3 個(gè)手指變化為第 2(01) 個(gè),所以抬起時(shí)觸發(fā)的是第 2 根手指的抬起事件(刪除線部分)。
但是,如果第 2 個(gè)手指抬起后,落在屏幕上另外一個(gè)手指會(huì)怎樣?經(jīng)過測試,發(fā)現(xiàn)另外落下的手指會(huì)替代之前第 2 個(gè)手指的位置,系統(tǒng)判定為 2(01),而不是順延下去變成 3(02),并且原本第3個(gè)手指的index變?yōu)樵瓉頂?shù)值(02),但是如果繼續(xù)落下其他的手指,數(shù)值則會(huì)順延。
即手指抬起時(shí)的 Index 會(huì)趨向于和按下時(shí)相同,雖然在手指數(shù)量不足時(shí),Index 會(huì)變小,但是當(dāng)手指變多時(shí),Index 會(huì)趨向于保持和按下時(shí)一樣。
PS:由于程序是從0開始計(jì)數(shù)的,所以 0 就是 1, 1 就是 2 …
3.4、對(duì) move 事件無效。
這個(gè)也比較容易理解,我們所取得的 Index 屬性實(shí)際上是從事件上分離下來的,但是 move 事件始終為 0x00000002,也就是說,在 move 時(shí)不論你移動(dòng)哪個(gè)手指,使用 getActionIndex() 獲取到的始終是數(shù)值 0。
既然 move 事件無法用事件索引(Index)區(qū)別,那么該如何區(qū)分 move 是那個(gè)手指發(fā)出的呢?這就要用到 pointId 了,pointId 和 index 最大的區(qū)別就是 pointId 是不變的,始終為第一次落下時(shí)生成的數(shù)值,不會(huì)受到其他手指抬起和落下的影響。
3.5、pointId 與 index 的異同。
| 1. 從 0 開始,自動(dòng)增長。 2. 落下手指時(shí)優(yōu)先填補(bǔ)空缺(填補(bǔ)之前抬起手指的編號(hào))。 | 1. Index 會(huì)變化,pointId 始終不變。 |
4. Move 相關(guān)事件
4.1 actionIndex 與 pointerIndex
在 move 中無法取得 actionIndex 的,我們需要使用 pointerIndex 來獲取更多的信息,例如某個(gè)手指的坐標(biāo):
getX(int pointerIndex) getY(int pointerIndex)但是這個(gè) pointerIndex 又是什么呢?和 actionIndex 有區(qū)別么?
實(shí)際上這個(gè) pointerIndex 和 actionIndex 區(qū)別并不大,兩者的數(shù)值是相同的,你可以認(rèn)為 pointerIndex 是特地為 move 事件準(zhǔn)備的 actionIndex。
4.2 pointerIndex 與 pointerId
| pointerIndex | 用于獲取具體事件,可能會(huì)隨著其他手指的抬起和落下而變化 |
| pointerId | 用于識(shí)別手指,手指按下時(shí)產(chǎn)生,手指抬起時(shí)回收,期間始終不變 |
這兩個(gè)數(shù)值使用以下兩個(gè)方法相互轉(zhuǎn)換。
| getPointerId(int pointerIndex) | 獲取一個(gè)指針(手指)的唯一標(biāo)識(shí)符ID,在手指按下和抬起之間ID始終不變。 |
| findPointerIndex(int pointerId) | 通過 pointerId 獲取到當(dāng)前狀態(tài)下 pointIndex,之后通過 pointIndex 獲取其他內(nèi)容。 |
通常情況下,pointerIndex 和 pointerId 是相同的,但也可能會(huì)因?yàn)槟承┦种傅奶鸲兊貌煌?/p>
4.3 遍歷多點(diǎn)觸控
先來一個(gè)簡單的,遍歷出多個(gè)手指的 move 事件:
String TAG = "Gcs"; switch (event.getActionMasked()) {case MotionEvent.ACTION_MOVE:for (int i = 0; i < event.getPointerCount(); i++) {Log.i("TAG", "pointerIndex="+i+", pointerId="+event.getPointerId(i));// TODO} }通過遍歷 pointerCount 獲取到所有的 pointerIndex,同時(shí)通過 pointerIndex 來獲取 pointerId,可以通過不同手指抬起和按下后移動(dòng)來觀察 pointerIndex 和 pointerId 的變化。
4.4 在多點(diǎn)觸控中追蹤單個(gè)手指
要實(shí)現(xiàn)追蹤單個(gè)手指還是有些麻煩的,需要同時(shí)使用上 actionIndex, pointerId 和 pointerIndex,例如,我們只追蹤第2個(gè)手指,并畫出其位置:
/*** 繪制出第二個(gè)手指第位置*/ public class MultiTouchTest extends CustomView {String TAG = "Gcs";// 用于判斷第2個(gè)手指是否存在boolean haveSecondPoint = false;// 記錄第2個(gè)手指第位置PointF point = new PointF(0, 0);public MultiTouchTest(Context context) {this(context, null);}public MultiTouchTest(Context context, AttributeSet attrs) {super(context, attrs);mDeafultPaint.setAntiAlias(true);mDeafultPaint.setTextAlign(Paint.Align.CENTER);mDeafultPaint.setTextSize(30);}@Overridepublic boolean onTouchEvent(MotionEvent event) {int index = event.getActionIndex();switch (event.getActionMasked()) {case MotionEvent.ACTION_POINTER_DOWN:// 判斷是否是第2個(gè)手指按下if (event.getPointerId(index) == 1){haveSecondPoint = true;point.set(event.getY(), event.getX());}break;case MotionEvent.ACTION_POINTER_UP:// 判斷抬起的手指是否是第2個(gè)if (event.getPointerId(index) == 1){haveSecondPoint = false;point.set(0, 0);}break;case MotionEvent.ACTION_MOVE:if (haveSecondPoint) {// 通過 pointerId 來獲取 pointerIndexint pointerIndex = event.findPointerIndex(1);// 通過 pointerIndex 來取出對(duì)應(yīng)的坐標(biāo)point.set(event.getX(pointerIndex), event.getY(pointerIndex));}break;}invalidate(); // 刷新return true;}@Overrideprotected void onDraw(Canvas canvas) {canvas.save();canvas.translate(mViewWidth/2, mViewHeight/2);canvas.drawText("追蹤第2個(gè)按下手指的位置", 0, 0, mDeafultPaint);canvas.restore();// 如果屏幕上有第2個(gè)手指則繪制出來其位置if (haveSecondPoint) {canvas.drawCircle(point.x, point.y, 50, mDeafultPaint);}} }這段代碼也非常短,其核心就是通過判斷數(shù)值為 1 的 pointerId 是否存在,如果存在就在 move 的時(shí)候取出其坐標(biāo),并繪制出來。
雖然邏輯簡單,但個(gè)人感覺寫起來還是有些麻煩,如果有更簡單的方案歡迎告訴我。
二、如何使用多點(diǎn)觸控
多點(diǎn)觸控應(yīng)用還是比較廣泛的,至少目前大部分的圖片查看都需要用到多點(diǎn)觸控技術(shù)(用于拖動(dòng)和縮放圖片)。
但是在某些看似不需要多觸控的地方也需要對(duì)多點(diǎn)觸控進(jìn)行判斷,只要是多點(diǎn)觸控可能引起錯(cuò)誤的地方都應(yīng)該加上多點(diǎn)觸控的判斷。例如使用到 move 事件的時(shí)候,由于 move 事件可能由多個(gè)手指同時(shí)觸發(fā),所以可能會(huì)出現(xiàn)同時(shí)被多個(gè)手指控制的情況,如果不適當(dāng)?shù)奶幚?#xff0c;這個(gè) move 就可能由任何一個(gè)手指觸發(fā)。
舉一個(gè)簡單的例子:
如果我們需要一個(gè)可以用單指拖動(dòng)的圖片。假如我們不進(jìn)行多指觸控的判斷,像下面這樣:
沒有針對(duì)多指觸控處理版本:
/*** 一個(gè)可以拖圖片動(dòng)的 View*/ public class DragView1 extends CustomView {String TAG = "Gcs";Bitmap mBitmap; // 圖片RectF mBitmapRectF; // 圖片所在區(qū)域Matrix mBitmapMatrix; // 控制圖片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);public DragView1(Context context) {this(context, null);}public DragView1(Context context, AttributeSet attrs) {super(context, attrs);// 調(diào)整圖片大小BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960/2;options.outHeight = 800/2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:// 判斷按下位置是否包含在圖片區(qū)域內(nèi)if (mBitmapRectF.contains((int)event.getX(), (int)event.getY())){canDrag = true;lastPoint.set(event.getX(), event.getY());}break;case MotionEvent.ACTION_UP:canDrag = false;case MotionEvent.ACTION_MOVE:if (canDrag) {// 移動(dòng)圖片mBitmapMatrix.postTranslate(event.getX() - lastPoint.x, event.getY() - lastPoint.y);// 更新上一次點(diǎn)位置lastPoint.set(event.getX(), event.getY());// 更新圖片區(qū)域mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix.mapRect(mBitmapRectF);invalidate();}break;}return true;}@Overrideprotected void onDraw(Canvas canvas) {canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);} }這個(gè)版本非常簡單,當(dāng)然了,如果正常使用(只使用一個(gè)手指)的話也不會(huì)出問題,但是當(dāng)使用多個(gè)手指,且有抬起和按下的時(shí)候就可能出問題,下面用一個(gè)典型的場景演示一下:
注意在第二個(gè)手指按下,第一個(gè)手指抬起時(shí),此時(shí)原本的第二個(gè)手指會(huì)被識(shí)別為第一個(gè),所以圖片會(huì)直接跳動(dòng)到第二個(gè)手指位置。
為了不出現(xiàn)這種情況,我們可以判斷一下 pointId 并且只獲取第一個(gè)手指的數(shù)據(jù),這樣就能避免這種情況發(fā)生了,如下。
針對(duì)多指觸控處理后版本:
/*** 一個(gè)可以拖圖片動(dòng)的 View*/ public class DragView extends CustomView {String TAG = "Gcs";Bitmap mBitmap; // 圖片RectF mBitmapRectF; // 圖片所在區(qū)域Matrix mBitmapMatrix; // 控制圖片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);public DragView(Context context) {this(context, null);}public DragView(Context context, AttributeSet attrs) {super(context, attrs);BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960/2;options.outHeight = 800/2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_POINTER_DOWN:// ▼ 判斷是否是第一個(gè)手指 && 是否包含在圖片區(qū)域內(nèi)if (event.getPointerId(event.getActionIndex()) == 0 && mBitmapRectF.contains((int)event.getX(), (int)event.getY())){canDrag = true;lastPoint.set(event.getX(), event.getY());}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_POINTER_UP:// ▼ 判斷是否是第一個(gè)手指if (event.getPointerId(event.getActionIndex()) == 0){canDrag = false;}break;case MotionEvent.ACTION_MOVE:// 如果存在第一個(gè)手指,且這個(gè)手指的落點(diǎn)在圖片區(qū)域內(nèi)if (canDrag) {// ▼ 注意 getX 和 getYint index = event.findPointerIndex(0);// Log.i(TAG, "index="+index);mBitmapMatrix.postTranslate(event.getX(index)-lastPoint.x, event.getY(index)-lastPoint.y);lastPoint.set(event.getX(index), event.getY(index));mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix.mapRect(mBitmapRectF);invalidate();}break;}return true;}@Overrideprotected void onDraw(Canvas canvas) {canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);} }可以看到,比起上一個(gè)版本,只添加了少量代碼,就變得更加“智能”了,可以準(zhǔn)確識(shí)別某一個(gè)手指,不會(huì)因?yàn)槭种柑鸲J(rèn)錯(cuò)手指。
重點(diǎn)注意最后,第一個(gè)手指抬起之后,圖片并沒有跳躍到第二個(gè)手指的位置。
上面的兩個(gè)對(duì)比示例都精簡到了極致,其核心依舊是正確的追蹤某一個(gè)手指,建議大家自己寫一遍體會(huì)一下。
我感覺很多人看到這里依舊是不明所以的,一些簡單的東西還好弄,但是復(fù)雜一些,如同時(shí)處理多個(gè)手指的數(shù)值就有些困難了,假如說你之前沒有接觸過多點(diǎn)觸控的處理,此時(shí)讓你實(shí)現(xiàn)用兩個(gè)手指來縮放圖片還是有些困難的。
因?yàn)檫@不僅要追蹤兩個(gè)手指的位置,還要根據(jù)位置變化來計(jì)算縮放比例和縮放中心,單單這兩個(gè)非常簡單的數(shù)學(xué)問題就能難倒一大批人。
當(dāng)然了,很多麻煩問題都有簡單的解決方案,假如說我們真的要實(shí)現(xiàn)一個(gè)可以用兩個(gè)或者多個(gè)手指縮放的控件,何必要自己算呢,可以嘗試一下 Android 自帶的解決方案:手勢(shì)檢測(GestureDetector、ScaleGestureDecetor),不僅能自動(dòng)幫你計(jì)算好縮放比例和縮放中心,而且還可以檢測出 單擊、長按、滑屏 等不同的手勢(shì),不過這就不是本篇的事情了,以后有時(shí)間會(huì)寫一下有關(guān)手勢(shì)檢測的用法(繼續(xù)挖坑)。
三、總結(jié)
前段時(shí)間因?yàn)楦鞣N事情比較忙,這篇文章也沒時(shí)間去寫,所以就一直拖到了現(xiàn)在,期間收到不少讀者催更,實(shí)在是抱歉了。今后在會(huì)盡量保證穩(wěn)定更新的,爭取盡快把自定義View系列這一個(gè)大坑填完。 ˊ_>ˋ
關(guān)于多點(diǎn)觸控,個(gè)人認(rèn)為還算一個(gè)比較重要的知識(shí)點(diǎn)。尤其是隨著 Android 的發(fā)展,很多炫酷的交互操作可能會(huì)需要用戶進(jìn)行拖拽操作。在進(jìn)行這類操作的時(shí)候進(jìn)行一下手指的判斷還是相當(dāng)重要的。
本文中需要注意的幾個(gè)知識(shí)點(diǎn):
- 如何兼容 2.0 版本的多點(diǎn)觸控(目前大部分都不需要兼容 2.0 了吧)。
- actionIndex、pointIndex 與 pointId 的區(qū)別和用法。
- 如何在多點(diǎn)觸控中正確的追蹤一個(gè)手指。
About Me
作者微博: @GcsSloop
參考資料
MotionEvent
<hr>轉(zhuǎn)自: http://www.gcssloop.com/customview/multi-touch
總結(jié)
以上是生活随笔為你收集整理的安卓自定义View进阶-多点触控详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 29岁程序员,该怎么在写作、沟通、能力方
- 下一篇: ERROR Error: [copy-w