Android多点触控最佳实践
事故現場
??最近在對公司項目中的控件進行優化改造,其中一個是能夠上拉和下拉的彈性ScrollView。
??發現沒有,當使用一個手指的時候感覺還不錯。但是當我想用兩個手指交替不斷下拉想要把視圖內容往下“扒”的時候就辦不到了,因為他在onTouchEvent()方法里只是最簡單的實現了下拉的邏輯而沒有涉及到多指觸控,而我想要的效果是像QQ空間或者微信朋友圈那樣的。
?
?
預備知識
為了將控件改造成能夠支持多點觸控的,首先我們需要了解Android中關于多點觸控的基礎知識。這里我推薦先閱讀GcsSloop的兩篇文章
安卓自定義View進階-MotionEvent詳解
安卓自定義View進階-多點觸控詳解
??我現在假設你已經看了上面的那兩篇文章,下面我來劃重點:
1.多點觸控時必須使用getActionMasked()來獲取事件類型
2.Pointer:
MotionEvent中引入了Pointer的概念,一個pointer就代表一個觸摸點,每個pointer都有自己的事件類型,也有自己的橫軸坐標值。一個MotionEvent對象中可能會存儲多個pointer的相關信息,每個pointer都會有一個自己的id和index。pointer的id在整個事件流中是不會發生變化的,但是index會發生變化
3.PointerId:
每根手指從按下、移動到離開屏幕,每個手指都會擁有一個固定PointerId.PointerId的值,一般用它來區分是哪根手指
4.PointerIndex:
每根手指從按下、移動到離開屏幕,每根手指在每一個事件的Index可能是不固定的,因為受到其它手指的影響
5.PointerId和PointerIndex的變化規律
關于變化規律這里,可以看GcsSloop的第二篇文章,這里我不再贅述,只舉一個實際的例子:
| 依次按下三根手指 | 三根手指的id依次為0、1、2 | 三根手指的index依次為0、1、2 |
| 抬起第二根手指 | 第一根手指的id為0,第三根手指的id為2 | 第一根手指的index為0,第三根手指的index變為1 |
| 抬起第一根手指 | 第三根手指的id為2 | 第三根手指的index變為0 |
可見同一根手指的id是不會變化的,而index是會變化的,但總是以0、1或者0、1、2這樣的形式出現,而不可能出現0、2這樣間隔了一個的或者1、2這樣的沒有0索引在內的形式
6.多點觸控相關事件
| ACTION_DOWN | 第一個手指初次接觸到屏幕時觸發 |
| ACTION_MOVE | 手指在屏幕上滑動時觸發,會多次觸發。 |
| ACTION_UP | 最后一個手指離開屏幕時觸發 |
| ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已經有手指在屏幕上) |
| ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上) |
7.多點觸控相關的方法:
| getActionMasked() | 與 getAction() 類似,多點觸控需要使用這個方法獲取事件類型 |
| getActionIndex() | 獲取該事件是哪個指針(手指)產生的 |
| getPointerId(int pointerIndex) | 獲取一個指針(手指)的唯一標識符ID,在手指按下和抬起之間ID始終不變 |
| getX(int pointerIndex) | 獲取某一個指針(手指)的X坐標 |
| getY(int pointerIndex) | 獲取某一個指針(手指)的Y坐標 |
| findPointerIndex(int pointerId) | 通過PointerId獲取到當前狀態下PointIndex,之后通過PointIndex獲取其他內容 |
| getPointerCount() | 獲取在屏幕上手指的個數 |
如何使用多點觸控
對于多點觸控的處理,一般是這樣:
記錄活動手指的id(mActivePointerId),通過此id獲取move事件的坐標
我這里還是舉一個GcsSloop文章中的例子:
如果我們需要一個可以用單指拖動的圖片。假如我們不進行多指觸控的判斷,像下面這樣:
沒有針對多指觸控處理版本:
這個版本非常簡單,當然了,如果正常使用(只使用一個手指)的話也不會出問題,但是當使用多個手指,且有抬起和按下的時候就可能出問題
?
?
??注意在第二個手指按下,第一個手指抬起時,此時原本的第二個手指會被識別為第一個,所以圖片會直接跳動到第二個手指位置。原因是event.getX()和event.getY中沒有傳入pointerIndex的參數, 那么默認追蹤的就是pointerIndex = 0的手指,當第二個手指按下,第一個手指抬起的時候,觸發了move事件,event.getX()和event.getY()此時是獲取第二個手指的數據,而lastPoint.x和lastPoint.y并沒有在第二個手指按下的時候進行更新,記錄的是第一個手指抬起時候的坐標,和evet.getX()、event.getY()有較大的距離, 所以postTranslate了很大一段距離, 發生了跳動的情況。
??為了不出現這種情況,我們可以判斷一下 pointId 并且只獲取第一個手指的數據,這樣就能避免這種情況發生了,如下。
針對多指觸控處理后版本:
這個也是GcsSloop的代碼,因為這里只追蹤第一根手指(pointerId = 0的手指),第二根手指的活動全都無視,所以不會再出現跳動的情況
?
?
??但是我覺得依舊不夠,因為我想要的是當第二根手指放下的時候就可以靠第二根手指來移動圖片,而不是無視第二根手指。這是就產生了最終的版本,是我在GcsSloop的代碼基礎上加以改進的,如下:
public class DragViewFinal extends View {String TAG = "DragViewFinal";Bitmap mBitmap; // 圖片RectF mBitmapRectF; // 圖片所在區域Matrix mBitmapMatrix; // 控制圖片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);private Paint mDeafultPaint;public DragViewFinal(Context context) {this(context, null);}public DragViewFinal(Context context, AttributeSet attrs) {super(context, attrs);mDeafultPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);// 調整圖片大小BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960 / 2;options.outHeight = 800 / 2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.poly_test, options);mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}private int mActivePointerId;/*** A null/invalid pointer ID.*/private final int INVALID_POINTER = -1;// 記錄活動手指的id(activePointerId),通過此ID獲取move事件的坐標。// 在手指按下的時候,記錄下activePointerId// 第二根手指按下的時候,更新activePointerId。(我們讓第二根手指作為活動手指,忽略第一個手指的move)// 當其中一根手指抬起時,如果是第一根手指,那么不做處理,如果是第二根手指抬起,也就是活動手指抬起的話,將活動手指改回第一根。@Overridepublic boolean onTouchEvent(MotionEvent event) {final int action = event.getActionMasked();final int actionIndex = event.getActionIndex();switch (action) {case MotionEvent.ACTION_DOWN:// 判斷按下位置是否包含在圖片區域內if (mBitmapRectF.contains((int) event.getX(), (int) event.getY())) {mActivePointerId = event.getPointerId(0);Log.d("ACTION_DOWN", "mActivePointerId = " + mActivePointerId);canDrag = true;lastPoint.set(event.getX(0), event.getY(0));}break;case MotionEvent.ACTION_POINTER_DOWN:// 將新落下來那根手指作為活動手指mActivePointerId = event.getPointerId(actionIndex);lastPoint.set(event.getX(actionIndex), event.getY(actionIndex));Log.d("ACTION_POINTER_DOWN", "mActivePointerId = " + mActivePointerId);break;case MotionEvent.ACTION_POINTER_UP:if (mActivePointerId == event.getPointerId(actionIndex)) { // 如果松開的是活動手指, 讓還停留在屏幕上的最后一根手指作為活動手指// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.// pointerIndex都是像0, 1, 2這樣連續的final int newPointerIndex = actionIndex == 0 ? 1 : 0;mActivePointerId = event.getPointerId(newPointerIndex);lastPoint.set(event.getX(newPointerIndex), event.getY(newPointerIndex));Log.d("ACTION_POINTER_UP", "松開的是活動手指");}Log.d("ACTION_POINTER_UP", "mActivePointerId = " + mActivePointerId);break;case MotionEvent.ACTION_UP: // 代表用戶的最后一個手指離開了屏幕mActivePointerId = INVALID_POINTER;canDrag = false;Log.d("ACTION_UP", "mActivePointerId = " + mActivePointerId);case MotionEvent.ACTION_MOVE:if (mActivePointerId == INVALID_POINTER) {Log.e("ACTION_MOVE", "Got ACTION_MOVE event but don't have an active pointer id.");return false;}if (canDrag) {final int pointerIndex = event.findPointerIndex(mActivePointerId);mBitmapMatrix.postTranslate(event.getX(pointerIndex) - lastPoint.x, event.getY(pointerIndex) - lastPoint.y);// 更新上一次點位置lastPoint.set(event.getX(pointerIndex), event.getY(pointerIndex));// 更新圖片區域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);} }注意MotionEvent.ACTION_POINTER_DOWN和MotionEvent.ACTION_POINTER_UP事件:在新的一根手指落下來的時候,將這根新的手指作為活動手指,記錄它的pointerId并且更新lastPoint的坐標;在其中一根手指抬起的時候進行判斷,如果抬起的是非活動手指,那就不要管,如果抬起的時候活動手指,那就把其他的手指作為活動手指。選擇新的活動手指的時候,我這里簡單粗暴的用了final int newPointerIndex = actionIndex == 0 ? 1 : 0這樣的形式,因為我暫時想不到更好的方法了,如果你有,請告訴我。效果如下:
?
多點觸控了解的差不多了,接下來我就對那個只支持單點觸控的彈性ScrollView進行改進了,但是因為這里涉及到了一些其他的知識,就不貼代碼了,但對于多點觸控的處理還是核心的那幾步。其實寫代碼都是那樣,關鍵在于理解,理解了之后便能夠靈活運用,萬變不離其宗。這里只貼出一個改進后的控件的效果:
?
github地址
最后是國際慣例,給出demo的github地址MutiTouchDemo
作者:mundane
鏈接:https://www.jianshu.com/p/f8ef2685716d
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。
總結
以上是生活随笔為你收集整理的Android多点触控最佳实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电子硬件3.杜邦线
- 下一篇: 经典的机器人入门资料