Android開發(fā)多手指觸控事件處理
正文
多點(diǎn)觸控,一直以來都是事件處理中比較晦澀的一個話題。其一是因?yàn)樗臋C(jī)制與我們常規(guī)思維有點(diǎn)不同,基二是因?yàn)槲覀冇玫谋容^少。那么作為一個有點(diǎn)追求的Android開發(fā)者,我們必須要掌握這些,這樣可以提高代碼的格調(diào)。
寫這篇文章還是有點(diǎn)難度的,我反反復(fù)復(fù)修改了好多次,真的是刪了又改,改了又刪,只為把多點(diǎn)觸控講得明明白白。最后我決定把本文分為三部分進(jìn)行講解
- 講解多手指觸摸的一些關(guān)鍵性概念。雖然這部分概念非常抽象,并且也無法用源碼去解釋(源碼在底層),但是這部分概念是最關(guān)鍵的。如果你想掌握多點(diǎn)觸控,必須理解并記住這些概念。
- 講解多手指觸摸事件在ViewGroup是如何分發(fā)處理的。因?yàn)橹挥欣斫饬诉@個,我們才能寫出正確的多手指觸摸事件的代碼。
- 通過一個例子講解如何在滑動控件中支持多手指滑動。
好了,廢話不多說了,讓我們開始這次愉快的旅程吧。
觸摸事件
首先我們從MotionEvent.getAction()
講起吧。很多地方把這個方法的返回值叫做觸摸事件的類型,其實(shí)這個叫法是錯誤的,它的返回值不僅包含事件的類型,還包含手指的索引值。
假如MotionEvent.getAction()
返回一個值,用十六進(jìn)制表示為0X0100
,這個值的高八位的值是01
,用二進(jìn)制表示就是0000 0001
,它表示手指的索引,而低八位的值是00
,用二進(jìn)制表示就是0000 0000
,它才表示事件的類型。
事件類型
那么我們怎么獲取這個事件的類型呢,我想大家應(yīng)該都想到了事件類型的掩碼,MotionEvent.getActionMask()
就是通過事件類型掩碼獲取事件類型的。
那么,為什么大家一直說MotionEvent.getAction()
返回的就是事件類型呢?因?yàn)檫@是一個巧合,對于單手指操作,MotionEvent.getAction()
的返回值中,高八位的索引值是0,因此它正好與事件類型的值一樣。
對于支持多手指操作,MotionEvent.getAction()
返回值的事件索引就不再一直是0了,它會隨著手指的增加而改變,因此MotionEvent.getActionMask()
才是返回事件類型的正確操作。
那么我們來看下,多手指觸摸情況下所支持的事件類型
事件類型 | 事件說明 |
---|---|
ACTION_DOWN | 第一個手指按下 |
ACTION_POINTER_DOWN | 其它手指按下 |
ACTION_MOVE | 手指移動 |
ACTION_POINTER_UP | 不是最后一個手指抬起 |
ACTION_UP | 最后一個手指抬起 |
我們通過一個例子來解釋下這幾個事件的觸發(fā)時機(jī)。
- 當(dāng)?shù)谝粋€手指按下的時候,此時觸發(fā)的事件類型是
ACTION_DOWN
。 - 當(dāng)有第二個,甚至更多的手指按下的時候就會觸發(fā)
ACTION_POINTER_DOWN
事件。 - 當(dāng)任意一個手指滑動的時候,就會觸發(fā)
ACTION_MOVE
事件。 - 當(dāng)不是最后一個手指抬起時,會觸發(fā)
ACTION_POINTER_UP
事件。 - 當(dāng)最后一個手指擇時,會觸發(fā)
ACTION_UP
事件。
手指索引
MotionEvent.getAction()
返回值中還有個神秘的手指索引,它可以通過MotionEvent.getActionIndex()
獲取。那么它有啥用呢?對于單手指,沒有任何叼用,但是對于多手指,那它的作用就大了,這可以獲取手指的觸摸事件的信息,例如MotionEvent.getX(int pointerIndex)
獲取X坐標(biāo)值。
手指ID
剛才在事件類型部分,不知大家有沒有注意到,ACTION_MOVE
是不區(qū)分手指的,那么我們怎么知道是哪個手指觸發(fā)了ACTION_MOVE
的呢?你是不是第一時間想到了手指索引?請你放棄這個想法!
人可以通過眼睛觀察到手指的按下順序,但是硬件和軟件是無法做到的,而手指的索引在事件中可能會改變的。那么一個嚴(yán)峻的問題來了,如何跟蹤一個手指呢?用PointerId
!至于原理是什么,我也不太清楚。
那么怎么獲取一個手指的PointerId
呢?當(dāng)遇到ACTION_DOWN
和ACTION_POINTER_DOWN
的時候,通過如下代碼獲取
// 獲取手指的索引 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
事件中,我們要獲取某個手指的坐標(biāo)值,怎么辦呢?首先我們要保存在ACTION_DOWN
和ACTION_POINTER_DOWN
中保存手指PointerId
值,然后通過這個PointerId
調(diào)用MotionEvent.findPointerIndex(int pointerId)
獲取手指索引值,最后通過索引值獲取坐標(biāo)值,代碼如下
case MotionEvent.ACTION_MOVE: // 根據(jù)PointerId獲取某個手指的索引 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
兩個事件,那么這兩個事件在ViewGroup
中是如何分發(fā)處理的呢?如果要用源碼來分析呢,這篇文章的篇幅就太長了,但是呢,恰巧這兩個事件與ACTION_MOVE
的分發(fā)處理流程是一樣的。如果你還不懂ACTION_MOVE
是如何分發(fā)處理的,可以參考我之前寫的ViewGroup事件分發(fā)和處理源碼分析。
支持多手指的滑動控件
掌握了前面的基礎(chǔ)知識后,我們現(xiàn)在就又到了喜聞樂見的實(shí)戰(zhàn)環(huán)節(jié),在這一部分,我們要使一個滑動控件支持多手指滑動。
在實(shí)現(xiàn)這個功能之前,我們要明確實(shí)現(xiàn)思路
- 只有主手指能控制控件的滑動。
- 如果有手指按下,就認(rèn)為這個手指是主手指。
- 當(dāng)有手指抬起時,如果是主手指,那就必須重新找一個手指作為新的主手指。
首先我們需要一個可滑動的控件,這個控件取自手把手教你如何寫事件處理的代碼這篇文章的滑動控件,并且我需要大家對這篇文章的講的事件處理能理解清楚,因?yàn)橄旅鎸懙拇a,我不會去解釋這些基本知識。
我們前面說過,ACTION_POINTER_DOWN
和ACTION_POINTER_UP
的處理流程是和ACTION_MOVE
一樣的,那么要不要截斷呢?那就要看當(dāng)遇到這兩個事件的時候我們要做什么。
根據(jù)實(shí)現(xiàn)思路中的第二條,如果有手指按下,就認(rèn)為是主手指,因此在處理ACTION_POINTER_DOWN
時候只是簡單獲取手指的PointerId
,然后保存為主手指即可,所以不需要去截斷。
根據(jù)實(shí)現(xiàn)思路的第三條,如果抬起的是主手指,那么就要重新找一個替代的手指作為主手指,所以也不需要去截斷。
那么,在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)有新手指按下的時候,就認(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)有主手指抬起時,如何尋找替代的主手指
@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)主手指抬起時,尋找一個新的主手指,并且更新最新的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) { // 尋找一個已經(jīng)存在的手指索引 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; // 通過新的手指索引獲取手指ID mPrimaryPointerId = event.getPointerId(newPointerIndex); // 通過新的手指索引獲取坐標(biāo)值 mLastX = event.getX(newPointerIndex); } }
把這些問題解決后,那么在處理滑動的代碼的時候,就要通過這個主手指ID來獲取坐標(biāo)值,然后根據(jù)這些坐標(biāo)值來決定滑動,我這里用部分代碼來演示下
public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_MOVE: // 獲取主手指的坐標(biāo)值 PointF primaryPointerPoint = getPrimaryPointerPoint(ev); // 根據(jù)坐標(biāo)值判斷是否需要滑動 if (canScroll(primaryPointerPoint.x, primaryPointerPoint.y)) { mBeingDragged = true; getParent().requestDisallowInterceptTouchEvent(true); // 執(zhí)行一次滑動 performDrag(primaryPointerPoint.x); mLastX = primaryPointerPoint.x; // 可以滑動就截斷事件 return true; } break; } return super.onInterceptTouchEvent(ev); } /** * 獲取主手指在某個事件觸發(fā)時的坐標(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é)
要掌握多手指滑動,必須先得掌握其關(guān)鍵的概念,有了這些概念我們就可以知道事件何時觸發(fā),怎么跟蹤一個手指。然后我們需要掌握多手指事件的處理流程,巧合的是,只要知道ACTION_MOVE
的處理流程就明白了多手指事件的流程。最后我們要掌握為一個滑動控件添加多手指支持的實(shí)現(xiàn)思路。
有了這三步,基本上就可以實(shí)現(xiàn)一個支持多手指滑動的控件。不過請注意我的措辭,是基本上,是基本上,是基本上!
最后,我默默地留下一個github地址,供大家參考。
以上就是Android開發(fā)多手指觸控事件處理的詳細(xì)內(nèi)容,更多關(guān)于Android多手指觸控的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動回繞效果
這篇文章主要為大家詳細(xì)介紹了Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動回繞效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Android 架構(gòu)之?dāng)?shù)據(jù)庫框架搭建
這篇文章主要給大家介紹的是Android 架構(gòu)之?dāng)?shù)據(jù)庫框架搭建,在本篇中,將會讓你一點(diǎn)一滴從無到有創(chuàng)建一個不再為數(shù)據(jù)庫而煩惱的框架。需要的朋友可以參考下面文章的具體內(nèi)容2021-09-09Android?自定義view中根據(jù)狀態(tài)修改drawable圖片
這篇文章主要介紹了Android?自定義view中根據(jù)狀態(tài)修改drawable圖片的相關(guān)資料,需要的朋友可以參考下2023-07-07Flutter實(shí)現(xiàn)webview與原生組件組合滑動的示例代碼
這篇文章主要介紹了Flutter實(shí)現(xiàn)webview與原生組件組合滑動的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03詳解Android:向服務(wù)器提供數(shù)據(jù)之get、post方式
本篇文章主要介紹了詳解Android:向服務(wù)器提供數(shù)據(jù)之get、post方式,具有一定的參考價值,有興趣的可以了解一下。2017-03-03android studio 的下拉菜單Spinner使用詳解
這篇文章主要介紹了android studio 的下拉菜單Spinner使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Android開發(fā)數(shù)據(jù)結(jié)構(gòu)算法ArrayList源碼詳解
這篇文章主要為大家介紹了Android開發(fā)數(shù)據(jù)結(jié)構(gòu)算法ArrayList源碼詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10