Android 多点触控
1.多點(diǎn)觸控
多點(diǎn)觸控 ( Multitouch,也稱 Multi-touch ),即同時接受屏幕上多個點(diǎn)的人機(jī)交互操作,多點(diǎn)觸控是從 Android 2.0 開始引入的功能,在 Android 2.2 時對這一部分進(jìn)行了重新設(shè)計。
多點(diǎn)觸控相關(guān)問題:
在引入多點(diǎn)觸控之前,事件的類型很少,基本事件類型只有按下(down)、移動(move) 和 抬起(up),即便加上那些特殊的事件類型也只有幾種而已,所以我們可以用幾個常量來標(biāo)記這些事件,在使用的時候使用 getAction() 方法來獲取具體的事件,之后和這些常量進(jìn)行對比就行了。
在 Android 2.0 版本的時候,開始引入多點(diǎn)觸控技術(shù),由于技術(shù)上并不成熟,硬件和驅(qū)動也跟不上,多數(shù)設(shè)備只能支持追蹤兩三個點(diǎn)而已,因此在設(shè)計 API 上采取了一種簡單粗暴的方案,添加了幾個常量用于多點(diǎn)觸控的事件類型的判斷。
①ACTION_POINTER_1_DOWN 第 2 個手指按下,已廢棄,不推薦使用。
②ACTION_POINTER_2_DOWN 第 3 個手指按下,已廢棄,不推薦使用。
③ACTION_POINTER_3_DOWN 第 4 個手指按下,已廢棄,不推薦使用。
④ACTION_POINTER_1_UP 第 2 個手指抬起,已廢棄,不推薦使用。
⑤ACTION_POINTER_2_UP 第 3 個手指抬起,已廢棄,不推薦使用。
⑥ACTION_POINTER_3_UP 第 4 個手指抬起,已廢棄,不推薦使用。
這些事件類型是用來判斷非主要手指(第一個按下的稱為主要手指)的按下和抬起,使用起來大概是這樣子:
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;
}
看到這里可能會產(chǎn)生一些疑問?
(1)為什么沒有 ACTION_POINTER_X_MOVE ?
在多指觸控中所有的移動事件都是使用 ACTION_MOVE, 并沒有追蹤某一個手指的 move 事件類型。簡單說,設(shè)備很難無歧義的實(shí)現(xiàn)單獨(dú)追蹤每一個手指。
要理解這個,首先要明白設(shè)備是如何識別多點(diǎn)觸控的,設(shè)備沒有眼睛,不能像我們?nèi)艘粯涌吹接袔讉€手指(或者觸控筆)在屏幕上。
目前大多數(shù) Android 設(shè)備都是電容屏,它們感知觸摸是利用手指(觸控筆)與屏幕接觸產(chǎn)生的微小電流變化,之后通過計算這些電流變化來得出具體的觸摸位置,在多點(diǎn)觸控中,當(dāng)兩個觸摸點(diǎn)足夠靠近時,設(shè)備實(shí)際上是無法分清這兩個點(diǎn)的。因此當(dāng)兩個觸摸點(diǎn)靠近(重合)后再分開,設(shè)備很可能就無法正確的追蹤兩個點(diǎn)了,所以也很難實(shí)現(xiàn)無歧義的追蹤每一個點(diǎn)。
并且從軟件上來說,事件的編號產(chǎn)生和復(fù)用也是一個大問題,例如下面的場景:
事件 手指數(shù)量 編號變化
一個手指按下(命名為A) 1 A手指的編號為0,id為0
一個手指按下(命名為B) 2 B手指的編號為1,id為1
A手指抬起 1 B手指編號變更為0,id不變?yōu)?
一個手指按下(命名為C) 2 C手指編號為0,id為0,B手指編號為1,id為1
注意觀察上面編號和id的變化,有兩個問題:
①B手指的編號變化了。
②A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。
所以這就引出了一個問題:如果存在 ACTION_POINTER_X_MOVE,那么X應(yīng)該用什么標(biāo)志呢?編號會變化,id雖然不會變化,但id會被復(fù)用,例如A手指抬起后C手指按下,C手指復(fù)用了A手指的id。所以不論使用哪一個都不能保證唯一性。
當(dāng)然了,解決問題最好的方式就是把問題拋出去,既然從硬件和軟件上都不能保證唯一性和不變性,就不做區(qū)分了,因此所有的 move 事件都是 ACTION_MOVE, 具體是哪個手指產(chǎn)生的 move 用戶可以結(jié)合其他事件(按下和抬起)來綜合判斷。
(2)超過4個手指怎么辦?
2.0 兼容版,在2.2 之前的設(shè)計中,其提供的常量最多能判斷四個手指的抬起和落下,當(dāng)超過四個手指時怎么辦呢?在 2.2 版本之前,由于沒有 getActionMasked 方法,我們可以自己手動進(jìn)行計算,例如下面這樣 :
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個手指按下”);
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,“最后1個手指抬起”);
break;
case MotionEvent.ACTION_POINTER_1_DOWN:
// 此時相當(dāng)于 ACTION_POINTER_DOWN
Log.e(TAG,“第”+(index+1)+“個手指按下”);
break;
case MotionEvent.ACTION_POINTER_1_UP:
// 此時相當(dāng)于 ACTION_POINTER_UP
Log.e(TAG,“第”+(index+1)+“個手指抬起”);
break;
}
在上面的例子中有幾點(diǎn)比較關(guān)鍵:
①action 與 Index 的獲得
Android中的事件一般用最后8位來表示事件類型,再往前8位來表示Index。例如多指觸控的按下事件,其事件類型是 0x00000005, 其Index標(biāo)志位是 0x00000005,隨著更多的手指按下,其中變化的部分是 Index 標(biāo)志位,最后兩位是始終不變的,所以我們只要能將這兩個分離開就行了。
取得事件類型(action):
int action = event.getAction() & MotionEvent.ACTION_MASK;
這個非常簡單,ACTION_MASK=0x000000ff, 與 getAction() 進(jìn)行按位與操作后保留最后8位內(nèi)容(十六進(jìn)制每一個字符轉(zhuǎn)化為二進(jìn)制是4位)。
例如:0x00000105 & 0x000000ff = 0x00000005
取得事件索引(index):
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
②用 ACTION_POINTER_1_DOWN 代替 ACTION_POINTER_DOWN
這是因?yàn)樵?2.0 版本的時候還沒有 ACTION_POINTER_DOWN 的這個常量,但是它們兩個點(diǎn)數(shù)值是相同的,都是 0x00000005,這個你可以查看官方文檔或者源碼,甚至你直接寫 case 0x00000005 也行,抬起也是同理。
③只考慮兼容 2.2 以上的版本
當(dāng)然了,如果你不需要兼容 2.0 版本,只需要兼容到 2.2 以上的話就很簡單了,像下面這樣:
int index = event.getActionIndex();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,“第1個手指按下”);
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,“最后1個手指抬起”);
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.e(TAG,“第”+(index+1)+“個手指按下”);
break;
case MotionEvent.ACTION_POINTER_UP:
Log.e(TAG,“第”+(index+1)+“個手指抬起”);
break;
}
④index 和 pointId 的變化規(guī)則
在 2.2 版本以上,可以通過 getActionIndex() 輕松獲取到事件的索引(Index),但是這個事件索引的變化還是有點(diǎn)意思的,Index 變化有以下幾個特點(diǎn):
(1)從 0 開始,自動增長。
(2)如果之前落下的手指抬起,后面手指的 Index 會隨之減小。
(3)Index 變化趨向于第一次落下的數(shù)值(落下手指時,前面有空缺會優(yōu)先填補(bǔ)空缺)。
(4)對 move 事件無效。
下面我們逐條解釋一下具體含義。
(1)從 0 開始,自動增長。
手指按下 觸發(fā)事件(數(shù)值)
第1個手指按下 ACTION_DOWN (0x00000000)
第2個手指按下 ACTION_POINTER_DOWN (0x00000105)
第3個手指按下 ACTION_POINTER_DOWN (0x00000205)
第4個手指按下 ACTION_POINTER_DOWN (0x00000305)
注意高8位的位置,數(shù)值隨著手指按下而不斷變大。
(2)如果之前落下的手指抬起,后面手指的 Index 會隨之減小。
手指按下 觸發(fā)事件(數(shù)值)
第1個手指按下 ACTION_DOWN (0x00000000)
第2個手指按下 ACTION_POINTER_DOWN (0x00000105)
第3個手指按下 ACTION_POINTER_DOWN (0x00000205)
第2個手指抬起 ACTION_POINTER_UP (0x00000106)
第3個手指抬起 ACTION_POINTER_UP (0x00000106)
注意最后兩次觸發(fā)的事件,它的 Index 都是 1,這樣也比較容易解釋,當(dāng)原本的第 2 個手指抬起后,屏幕上就只剩下兩個手指了,之前的第 3 個手指就變成了第 2 個,于是抬起時觸發(fā)事件的 Index 為 1,即之前落下的手指抬起,后面手指的 Index 會隨之減小。
(3)Index 變化趨向于第一次落下的數(shù)值(落下手指時,前面有空缺會優(yōu)先填補(bǔ)空缺)。
這個就有點(diǎn)神奇了,通過上一條規(guī)則知道,某一個手指的 Index 可能會隨著其他手指的抬起而變小,這次用 4 個手指測試一下Index的變化趨勢。
第1個手指按下 ACTION_DOWN (0x00000000)
第2個手指按下 ACTION_POINTER_DOWN (0x00000105)
第3個手指按下 ACTION_POINTER_DOWN (0x00000205)
第2個手指抬起 ACTION_POINTER_UP (0x00000106)
第4個手指按下 ACTION_POINTER_DOWN (0x00000105)
第3個手指抬起 ACTION_POINTER_UP (0x00000206)
這個要和上一個對比著看,重點(diǎn)觀察第 3 個手指所觸發(fā)事件區(qū)別,在上一個示例中,隨著第 2 個手指的抬起,第 3 個手指變化為第 2(01) 個,所以抬起時觸發(fā)的是第 2 根手指的抬起事件。
但是,如果第 2 個手指抬起后,落在屏幕上另外一個手指會怎樣?經(jīng)過測試,發(fā)現(xiàn)另外落下的手指會替代之前第 2 個手指的位置,系統(tǒng)判定為 2(01),而不是順延下去變成 3(02),并且原本第3個手指的index變?yōu)樵瓉頂?shù)值(02),但是如果繼續(xù)落下其他的手指,數(shù)值則會順延。
即手指抬起時的 Index 會趨向于和按下時相同,雖然在手指數(shù)量不足時,Index 會變小,但是當(dāng)手指變多時,Index 會趨向于保持和按下時一樣。
(4)對 move 事件無效。
這個也比較容易理解,我們所取得的 Index 屬性實(shí)際上是從事件上分離下來的,但是 move 事件始終為 0x00000002,也就是說,在 move 時不論你移動哪個手指,使用 getActionIndex() 獲取到的始終是數(shù)值 0。
既然 move 事件無法用事件索引(Index)區(qū)別,那么該如何區(qū)分 move 是那個手指發(fā)出的呢?這就要用到 pointId 了,pointId 和 index 最大的區(qū)別就是 pointId 是不變的,始終為第一次落下時生成的數(shù)值,不會受到其他手指抬起和落下的影響。
(5)pointId 與 index 的相同點(diǎn):
①從 0 開始,自動增長。
②落下手指時優(yōu)先填補(bǔ)空缺(填補(bǔ)之前抬起手指的編號)。
不同點(diǎn):
Index 會變化,pointId 始終不變。
2.Move 相關(guān)事件
①actionIndex 與 pointerIndex
在 move 中無法取得 actionIndex 的,我們需要使用 pointerIndex 來獲取更多的信息,例如某個手指的坐標(biāo):getX(int pointerIndex)、getY(int pointerIndex)。但是這個 pointerIndex 又是什么呢?和 actionIndex 有區(qū)別么?
實(shí)際上這個 pointerIndex 和 actionIndex 區(qū)別并不大,兩者的數(shù)值是相同的,你可以認(rèn)為 pointerIndex 是特地為 move 事件準(zhǔn)備的 actionIndex。
②pointerIndex 與 pointerId
pointerIndex:用于獲取具體事件,可能會隨著其他手指的抬起和落下而變化
pointerId:用于識別手指,手指按下時產(chǎn)生,手指抬起時回收,期間始終不變
這兩個數(shù)值使用以下兩個方法相互轉(zhuǎn)換:
getPointerId(int pointerIndex):獲取一個指針(手指)的唯一標(biāo)識符ID,在手指按下和抬起之間ID始終不變。
findPointerIndex(int pointerId):通過 pointerId 獲取到當(dāng)前狀態(tài)下 pointIndex,之后通過 pointIndex 獲取其他內(nèi)容。
③ 遍歷多點(diǎn)觸控
比如遍歷出多個手指的 move 事件:
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,同時通過 pointerIndex 來獲取 pointerId,可以通過不同手指抬起和按下后移動來觀察 pointerIndex 和 pointerId 的變化。
④在多點(diǎn)觸控中追蹤單個手指
要實(shí)現(xiàn)追蹤單個手指還是有些麻煩的,需要同時使用上 actionIndex, pointerId 和 pointerIndex,例如,只追蹤第2個手指,并畫出其位置:
/**
-
繪制出第二個手指第位置
*/
public class MultiTouchTest extends CustomView {
// 用于判斷第2個手指是否存在
boolean haveSecondPoint = false;
// 記錄第2個手指第位置
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);
}@override
public boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
// 判斷是否是第2個手指按下
if (event.getPointerId(index)==1){
haveSecondPoint = true;
point.set(event.getY(), event.getX());
}
break;
case MotionEvent.ACTION_POINTER_UP:
// 判斷抬起的手指是否是第2個
if (event.getPointerId(index)==1){
haveSecondPoint = false;
point.set(0, 0);
}
break;
case MotionEvent.ACTION_MOVE:
if (haveSecondPoint) {
// 通過 pointerId 來獲取 pointerIndex
int pointerIndex = event.findPointerIndex(1);
// 通過 pointerIndex 來取出對應(yīng)的坐標(biāo)
point.set(event.getX(pointerIndex), event.getY(pointerIndex));
}
break;
}
invalidate(); // 刷新
return true;
}@Override
// 如果屏幕上有第2個手指則繪制出來其位置if (haveSecondPoint) {canvas.drawCircle(point.x, point.y, 50, mDeafultPaint);}
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(mViewWidth/2, mViewHeight/2);
canvas.drawText(“追蹤第2個按下手指的位置”, 0, 0, mDeafultPaint);
canvas.restore();}
}
這段代碼也非常短,其核心就是通過判斷數(shù)值為 1 的 pointerId 是否存在,如果存在就在 move 的時候取出其坐標(biāo),并繪制出來。
3.如何使用多點(diǎn)觸控
多點(diǎn)觸控應(yīng)用還是比較廣泛的,至少目前大部分的圖片查看都需要用到多點(diǎn)觸控技術(shù)(用于拖動和縮放圖片)。
但是在某些看似不需要多觸控的地方也需要對多點(diǎn)觸控進(jìn)行判斷,只要是多點(diǎn)觸控可能引起錯誤的地方都應(yīng)該加上多點(diǎn)觸控的判斷。例如使用到 move 事件的時候,由于 move 事件可能由多個手指同時觸發(fā),所以可能會出現(xiàn)同時被多個手指控制的情況,如果不適當(dāng)?shù)奶幚?#xff0c;這個 move 就可能由任何一個手指觸發(fā)。
舉一個簡單的例子:
如果我們需要一個可以用單指拖動的圖片。假如我們不進(jìn)行多指觸控的判斷,像下面這樣:
/**
-
一個可以拖圖片動的 View
*/
public class DragView1 extends CustomView {
Bitmap mBitmap; // 圖片
RectF mBitmapRectF; // 圖片所在區(qū)域
Matrix mBitmapMatrix; // 控制圖片的 matrix
boolean canDrag = false;
PointF lastPoint = new PointF(0, 0);public DragView1(Context context) {
this(context, null);
}public DragView1(Context context, AttributeSet attrs) {
mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();
super(context, attrs);
// 調(diào)整圖片大小
BitmapFactory.Options options = new BitmapFactory.Options();
options.outWidth = 960/2;
options.outHeight = 800/2;}
@Override
public 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) {
// 移動圖片
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;
}@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
}
}
這個版本非常簡單,如果正常使用(只使用一個手指)的話也不會出問題,但是當(dāng)使用多個手指,且有抬起和按下的時候就可能出問題:在第二個手指按下,第一個手指抬起時,此時原本的第二個手指會被識別為第一個,所以圖片會直接跳動到第二個手指位置。
為了不出現(xiàn)這種情況,我們可以判斷一下 pointId 并且只獲取第一個手指的數(shù)據(jù),這樣就能避免這種情況發(fā)生了,如下:
/**
-
一個可以拖圖片動的 View
*/
public class DragView extends CustomView {
Bitmap mBitmap; // 圖片
RectF mBitmapRectF; // 圖片所在區(qū)域
Matrix mBitmapMatrix; // 控制圖片的 matrix
boolean canDrag = false;
PointF lastPoint = new PointF(0, 0);public DragView(Context context) {
this(context, null);
}public DragView(Context context, AttributeSet attrs) {
mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();
super(context, attrs);
BitmapFactory.Options options = new BitmapFactory.Options();
options.outWidth = 960/2;
options.outHeight = 800/2;}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// 判斷是否是第一個手指 && 是否包含在圖片區(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:
// ▼ 判斷是否是第一個手指
if (event.getPointerId( event.getActionIndex())==0){
canDrag = false;
}
break;
case MotionEvent.ACTION_MOVE:
// 如果存在第一個手指,且這個手指的落點(diǎn)在圖片區(qū)域內(nèi)
if (canDrag) {
// 注意 getX 和 getY
int index = event.findPointerIndex(0);
// Log.i(TAG, “index=”+index);
mBitmapMatrix.postTranslate( event.getX(index)-lastPoint.x, event.getY(index)-lastPoint.y);
lastPostTranslate(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;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
}
}
可以看到,比起上一個版本,只添加了少量代碼,就變得更加“智能”了,可以準(zhǔn)確識別某一個手指,不會因?yàn)槭种柑鸲J(rèn)錯手指:第一個手指抬起之后,圖片不會跳躍到第二個手指的位置。
總結(jié)
以上是生活随笔為你收集整理的Android 多点触控的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 工作流经典资料收集
- 下一篇: (Android) 如何使用HOOK实现