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
,它才表示事件的類型。
事件類型
那么我們?cè)趺传@取這個(gè)事件的類型呢,我想大家應(yīng)該都想到了事件類型的掩碼,MotionEvent.getActionMask()
就是通過事件類型掩碼獲取事件類型的。
那么,為什么大家一直說MotionEvent.getAction()
返回的就是事件類型呢?因?yàn)檫@是一個(gè)巧合,對(duì)于單手指操作,MotionEvent.getAction()
的返回值中,高八位的索引值是0,因此它正好與事件類型的值一樣。
對(duì)于支持多手指操作,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()
獲取。那么它有啥用呢?對(duì)于單手指,沒有任何叼用,但是對(duì)于多手指,那它的作用就大了,這可以獲取手指的觸摸事件的信息,例如MotionEvent.getX(int pointerIndex)
獲取X坐標(biāo)值。
手指ID
剛才在事件類型部分,不知大家有沒有注意到,ACTION_MOVE
是不區(qū)分手指的,那么我們?cè)趺粗朗悄膫€(gè)手指觸發(fā)了ACTION_MOVE
的呢?你是不是第一時(shí)間想到了手指索引?請(qǐng)你放棄這個(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;
多手指事件處理
對(duì)于多手指觸摸事件呢,其實(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)控件,并且我需要大家對(duì)這篇文章的講的事件處理能理解清楚,因?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)的控件。不過請(qǐng)注意我的措辭,是基本上,是基本上,是基本上!
最后,我默默地留下一個(gè)github地址,供大家參考。
以上就是Android開發(fā)多手指觸控事件處理的詳細(xì)內(nèi)容,更多關(guān)于Android多手指觸控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動(dòng)回繞效果
這篇文章主要為大家詳細(xì)介紹了Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動(dòng)回繞效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android 架構(gòu)之?dāng)?shù)據(jù)庫框架搭建
這篇文章主要給大家介紹的是Android 架構(gòu)之?dāng)?shù)據(jù)庫框架搭建,在本篇中,將會(huì)讓你一點(diǎn)一滴從無到有創(chuàng)建一個(gè)不再為數(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與原生組件組合滑動(dòng)的示例代碼
這篇文章主要介紹了Flutter實(shí)現(xiàn)webview與原生組件組合滑動(dòng)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03詳解Android:向服務(wù)器提供數(shù)據(jù)之get、post方式
本篇文章主要介紹了詳解Android:向服務(wù)器提供數(shù)據(jù)之get、post方式,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-03-03android studio 的下拉菜單Spinner使用詳解
這篇文章主要介紹了android studio 的下拉菜單Spinner使用詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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-10Android仿QQ個(gè)人標(biāo)簽添加與刪除功能
這篇文章主要為大家詳細(xì)介紹了Android仿QQ個(gè)人標(biāo)簽添加與刪除功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12