Android開發(fā)多手指觸控事件處理
正文
多點(diǎn)觸控,一直以來都是事件處理中比較晦澀的一個(gè)話題。其一是因?yàn)樗臋C(jī)制與我們常規(guī)思維有點(diǎn)不同,基二是因?yàn)槲覀冇玫谋容^少。那么作為一個(gè)有點(diǎn)追求的Android開發(fā)者,我們必須要掌握這些,這樣可以提高代碼的格調(diào)。
寫這篇文章還是有點(diǎn)難度的,我反反復(fù)復(fù)修改了好多次,真的是刪了又改,改了又刪,只為把多點(diǎn)觸控講得明明白白。最后我決定把本文分為三部分進(jìn)行講解
- 講解多手指觸摸的一些關(guān)鍵性概念。雖然這部分概念非常抽象,并且也無法用源碼去解釋(源碼在底層),但是這部分概念是最關(guān)鍵的。如果你想掌握多點(diǎn)觸控,必須理解并記住這些概念。
- 講解多手指觸摸事件在ViewGroup是如何分發(fā)處理的。因?yàn)橹挥欣斫饬诉@個(gè),我們才能寫出正確的多手指觸摸事件的代碼。
- 通過一個(gè)例子講解如何在滑動(dòng)控件中支持多手指滑動(dòng)。
好了,廢話不多說了,讓我們開始這次愉快的旅程吧。
觸摸事件
首先我們從MotionEvent.getAction()講起吧。很多地方把這個(gè)方法的返回值叫做觸摸事件的類型,其實(shí)這個(gè)叫法是錯(cuò)誤的,它的返回值不僅包含事件的類型,還包含手指的索引值。
假如MotionEvent.getAction()返回一個(gè)值,用十六進(jìn)制表示為0X0100,這個(gè)值的高八位的值是01,用二進(jìn)制表示就是0000 0001,它表示手指的索引,而低八位的值是00,用二進(jìn)制表示就是0000 0000,它才表示事件的類型。
事件類型
那么我們怎么獲取這個(gè)事件的類型呢,我想大家應(yīng)該都想到了事件類型的掩碼,MotionEvent.getActionMask()就是通過事件類型掩碼獲取事件類型的。
那么,為什么大家一直說MotionEvent.getAction()返回的就是事件類型呢?因?yàn)檫@是一個(gè)巧合,對于單手指操作,MotionEvent.getAction()的返回值中,高八位的索引值是0,因此它正好與事件類型的值一樣。
對于支持多手指操作,MotionEvent.getAction()返回值的事件索引就不再一直是0了,它會(huì)隨著手指的增加而改變,因此MotionEvent.getActionMask()才是返回事件類型的正確操作。
那么我們來看下,多手指觸摸情況下所支持的事件類型
| 事件類型 | 事件說明 |
|---|---|
| ACTION_DOWN | 第一個(gè)手指按下 |
| ACTION_POINTER_DOWN | 其它手指按下 |
| ACTION_MOVE | 手指移動(dòng) |
| ACTION_POINTER_UP | 不是最后一個(gè)手指抬起 |
| ACTION_UP | 最后一個(gè)手指抬起 |
我們通過一個(gè)例子來解釋下這幾個(gè)事件的觸發(fā)時(shí)機(jī)。
- 當(dāng)?shù)谝粋€(gè)手指按下的時(shí)候,此時(shí)觸發(fā)的事件類型是
ACTION_DOWN。 - 當(dāng)有第二個(gè),甚至更多的手指按下的時(shí)候就會(huì)觸發(fā)
ACTION_POINTER_DOWN事件。 - 當(dāng)任意一個(gè)手指滑動(dòng)的時(shí)候,就會(huì)觸發(fā)
ACTION_MOVE事件。 - 當(dāng)不是最后一個(gè)手指抬起時(shí),會(huì)觸發(fā)
ACTION_POINTER_UP事件。 - 當(dāng)最后一個(gè)手指擇時(shí),會(huì)觸發(fā)
ACTION_UP事件。
手指索引
MotionEvent.getAction()返回值中還有個(gè)神秘的手指索引,它可以通過MotionEvent.getActionIndex()獲取。那么它有啥用呢?對于單手指,沒有任何叼用,但是對于多手指,那它的作用就大了,這可以獲取手指的觸摸事件的信息,例如MotionEvent.getX(int pointerIndex)獲取X坐標(biāo)值。
手指ID
剛才在事件類型部分,不知大家有沒有注意到,ACTION_MOVE是不區(qū)分手指的,那么我們怎么知道是哪個(gè)手指觸發(fā)了ACTION_MOVE的呢?你是不是第一時(shí)間想到了手指索引?請你放棄這個(gè)想法!
人可以通過眼睛觀察到手指的按下順序,但是硬件和軟件是無法做到的,而手指的索引在事件中可能會(huì)改變的。那么一個(gè)嚴(yán)峻的問題來了,如何跟蹤一個(gè)手指呢?用PointerId!至于原理是什么,我也不太清楚。
那么怎么獲取一個(gè)手指的PointerId呢?當(dāng)遇到ACTION_DOWN和ACTION_POINTER_DOWN的時(shí)候,通過如下代碼獲取
// 獲取手指的索引 int pointerIndex = motionEvent.getActionIndex(); // 通過手指索引獲取手指ID int pointerId = motionEvent.getPointerId(pointerIndex);
在前面的手指索引部分,我們知道通過索引可能獲取事件的信息,例如坐標(biāo)值,如下代碼
// 獲取手指索引
int pointerIndex = event.getActionIndex();
// 獲取坐標(biāo)值
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
然而在ACTION_MOVE事件中,我們要獲取某個(gè)手指的坐標(biāo)值,怎么辦呢?首先我們要保存在ACTION_DOWN和ACTION_POINTER_DOWN中保存手指PointerId值,然后通過這個(gè)PointerId調(diào)用MotionEvent.findPointerIndex(int pointerId)獲取手指索引值,最后通過索引值獲取坐標(biāo)值,代碼如下
case MotionEvent.ACTION_MOVE:
// 根據(jù)PointerId獲取某個(gè)手指的索引
int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
// 獲取坐標(biāo)值
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
break;
多手指事件處理
對于多手指觸摸事件呢,其實(shí)比單手指只是多出了ACTION_POINTER_DOWN和ACTION_POINTER_UP兩個(gè)事件,那么這兩個(gè)事件在ViewGroup中是如何分發(fā)處理的呢?如果要用源碼來分析呢,這篇文章的篇幅就太長了,但是呢,恰巧這兩個(gè)事件與ACTION_MOVE的分發(fā)處理流程是一樣的。如果你還不懂ACTION_MOVE是如何分發(fā)處理的,可以參考我之前寫的ViewGroup事件分發(fā)和處理源碼分析。
支持多手指的滑動(dòng)控件
掌握了前面的基礎(chǔ)知識(shí)后,我們現(xiàn)在就又到了喜聞樂見的實(shí)戰(zhàn)環(huán)節(jié),在這一部分,我們要使一個(gè)滑動(dòng)控件支持多手指滑動(dòng)。
在實(shí)現(xiàn)這個(gè)功能之前,我們要明確實(shí)現(xiàn)思路
- 只有主手指能控制控件的滑動(dòng)。
- 如果有手指按下,就認(rèn)為這個(gè)手指是主手指。
- 當(dāng)有手指抬起時(shí),如果是主手指,那就必須重新找一個(gè)手指作為新的主手指。
首先我們需要一個(gè)可滑動(dòng)的控件,這個(gè)控件取自手把手教你如何寫事件處理的代碼這篇文章的滑動(dòng)控件,并且我需要大家對這篇文章的講的事件處理能理解清楚,因?yàn)橄旅鎸懙拇a,我不會(huì)去解釋這些基本知識(shí)。
我們前面說過,ACTION_POINTER_DOWN和ACTION_POINTER_UP的處理流程是和ACTION_MOVE一樣的,那么要不要截?cái)嗄兀磕蔷鸵串?dāng)遇到這兩個(gè)事件的時(shí)候我們要做什么。
根據(jù)實(shí)現(xiàn)思路中的第二條,如果有手指按下,就認(rèn)為是主手指,因此在處理ACTION_POINTER_DOWN時(shí)候只是簡單獲取手指的PointerId,然后保存為主手指即可,所以不需要去截?cái)唷?/p>
根據(jù)實(shí)現(xiàn)思路的第三條,如果抬起的是主手指,那么就要重新找一個(gè)替代的手指作為主手指,所以也不需要去截?cái)唷?/p>
那么,在onInterceptTouchEvent()和onTouchEvent()的處理方式是一樣的,首先我們看下保存主手指的代碼如下
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
onPrimaryPointerDown(ev);
break;
case MotionEvent.ACTION_POINTER_DOWN:
onPrimaryPointerDown(ev);
break;
}
return super.onInterceptTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
onPrimaryPointerDown(event);
break;
}
return true;
}
/**
* 當(dāng)有新手指按下的時(shí)候,就認(rèn)作是主手指,于是重新記錄按下點(diǎn)的坐標(biāo),以及更新最新的X坐標(biāo)。
*
* @param event 觸摸事件。
*/
private void onPrimaryPointerDown(MotionEvent event) {
// 獲取手指索引
int pointerIndex = event.getActionIndex();
// 通過手指索引獲取手指ID
mPrimaryPointerId = event.getPointerId(pointerIndex);
// 通過手指索引保存坐標(biāo)值
mLastX = mStartX = event.getX(pointerIndex);
mStartY = event.getY(pointerIndex);
}
然后,我們來看下當(dāng)有主手指抬起時(shí),如何尋找替代的主手指
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_POINTER_UP:
onPrimaryPointerUp(ev);
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_UP:
onPrimaryPointerUp(event);
break;
}
return true;
}
/**
* 當(dāng)主手指抬起時(shí),尋找一個(gè)新的主手指,并且更新最新的X坐標(biāo)值為新主手指的X坐標(biāo)值。
*
* @param event
*/
private void onPrimaryPointerUp(MotionEvent event) {
// 獲取抬起手指的索引值
int pointerIndex = event.getActionIndex();
// 通過索引值,獲取抬起手指的ID
int pointerId = event.getPointerId(pointerIndex);
// 如果抬起手指的ID等于主手指的ID
if (pointerId == mPrimaryPointerId) {
// 尋找一個(gè)已經(jīng)存在的手指索引
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
// 通過新的手指索引獲取手指ID
mPrimaryPointerId = event.getPointerId(newPointerIndex);
// 通過新的手指索引獲取坐標(biāo)值
mLastX = event.getX(newPointerIndex);
}
}
把這些問題解決后,那么在處理滑動(dòng)的代碼的時(shí)候,就要通過這個(gè)主手指ID來獲取坐標(biāo)值,然后根據(jù)這些坐標(biāo)值來決定滑動(dòng),我這里用部分代碼來演示下
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
// 獲取主手指的坐標(biāo)值
PointF primaryPointerPoint = getPrimaryPointerPoint(ev);
// 根據(jù)坐標(biāo)值判斷是否需要滑動(dòng)
if (canScroll(primaryPointerPoint.x, primaryPointerPoint.y)) {
mBeingDragged = true;
getParent().requestDisallowInterceptTouchEvent(true);
// 執(zhí)行一次滑動(dòng)
performDrag(primaryPointerPoint.x);
mLastX = primaryPointerPoint.x;
// 可以滑動(dòng)就截?cái)嗍录?
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 獲取主手指在某個(gè)事件觸發(fā)時(shí)的坐標(biāo)。
*
* @param event 觸摸事件。
* @return 如果成功,返回坐標(biāo)點(diǎn),否則返回null。
*/
private PointF getPrimaryPointerPoint(MotionEvent event) {
PointF pointF = null;
if (mPrimaryPointerId != INVALID_POINTER_ID) {
int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
if (pointerIndex != -1) {
pointF = new PointF(event.getX(pointerIndex), event.getY(pointerIndex));
}
}
return pointF;
}
總結(jié)
要掌握多手指滑動(dòng),必須先得掌握其關(guān)鍵的概念,有了這些概念我們就可以知道事件何時(shí)觸發(fā),怎么跟蹤一個(gè)手指。然后我們需要掌握多手指事件的處理流程,巧合的是,只要知道ACTION_MOVE的處理流程就明白了多手指事件的流程。最后我們要掌握為一個(gè)滑動(dòng)控件添加多手指支持的實(shí)現(xiàn)思路。
有了這三步,基本上就可以實(shí)現(xiàn)一個(gè)支持多手指滑動(dòng)的控件。不過請注意我的措辭,是基本上,是基本上,是基本上!
最后,我默默地留下一個(gè)github地址,供大家參考。
以上就是Android開發(fā)多手指觸控事件處理的詳細(xì)內(nèi)容,更多關(guān)于Android多手指觸控的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用TextInputLayout創(chuàng)建登陸頁面
這篇文章主要為大家詳細(xì)介紹了Android使用TextInputLayout創(chuàng)建登陸頁面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Android 7.0調(diào)用相機(jī)崩潰詳解及解決辦法
這篇文章主要介紹了 Android 7.0調(diào)用相機(jī)崩潰詳解及解決辦法的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android開發(fā)之登錄驗(yàn)證實(shí)例教程
這篇文章主要介紹了Android開發(fā)之登錄驗(yàn)證實(shí)現(xiàn)方法,包括發(fā)送數(shù)據(jù)、服務(wù)器端驗(yàn)證、配置文件等,需要的朋友可以參考下2014-08-08
解析Android開發(fā)優(yōu)化之:軟引用與弱引用的應(yīng)用
Java從JDK1.2版本開始,就把對象的引用分為四種級(jí)別,從而使程序能更加靈活的控制對象的生命周期。這四種級(jí)別由高到低依次為:強(qiáng)引用、軟引用、弱引用和虛引用,本篇文章重點(diǎn)介紹一下軟引用和弱引用2013-05-05
Android中不支持動(dòng)態(tài)申請權(quán)限的原因
這篇文章主要介紹了Android中不支持動(dòng)態(tài)申請權(quán)限的原因,本文列舉了幾個(gè)不支持動(dòng)態(tài)申請權(quán)限的原因,需要的朋友可以參考下2015-01-01
android實(shí)現(xiàn)listview分頁的方法
這篇文章主要介紹了android實(shí)現(xiàn)listview分頁的方法,涉及Android生成listview列表的相關(guān)技巧,需要的朋友可以參考下2015-05-05

