【朝花夕拾】Android自定義View篇之(九)多點觸控(下)實踐出真知
前言
在上一篇文章中,已經總結了MotionEvent以及多點觸控相關的基礎理論知識和常用的函式。本篇將通過實現單指拖動圖片,多指拖動圖片的實際案例來進行練習並實現一些效果,來理解前面的理論知識。要理解本文的程式碼,需要先掌握上一篇的理論知識,事件處理基礎,以及一定的自定義View基礎,這些我也在本系列文章的前幾篇中講過,有興趣的可以按照本系列的順序依次閱讀學習,相信您一定會有不小的收穫。
本文的主要內容如下:
一、實現單指拖動圖片
要實現單指拖動圖片,大致思路就是監控手指的ACTION_MOVE事件。手指移動過程中,獲取事件的座標,讓圖片根據座標的變化來進行移動。具體程式碼實現如下,先自定義一個View,在其中處理單指拖動邏輯。
1 public class SingleTouchDragView extends View { 2 private static final String TAG = "songzheweiwang"; 3 private Bitmap mBitmap; 4 private RectF mRectF; 5 private Matrix mMatrix; 6 private Paint mPaint; 7 private PointF mLstPointF; 8 private boolean mCanDrag = false; 9 10 public SingleTouchDragView(Context context, @Nullable AttributeSet attrs) { 11 super(context, attrs); 12 init(); 13 } 14 15 private void init() { 16 mPaint = new Paint(); 17 mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog); 18 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 19 mMatrix = new Matrix(); 20 mLstPointF = new PointF(); 21 } 22 23 @Override 24 public boolean onTouchEvent(MotionEvent event) { 25 switch (event.getAction()) { 26 case MotionEvent.ACTION_DOWN: 27 //判斷按下位置是否在圖片區域內 28 if (mRectF.contains(event.getX(), event.getY())) { 29 mCanDrag = true; 30 mLstPointF.set(event.getX(), event.getY()); 31 } 32 break; 33 case MotionEvent.ACTION_UP: 34 mCanDrag = false; 35 case MotionEvent.ACTION_MOVE: 36 if (mCanDrag) { 37 //移動圖片 38 mMatrix.postTranslate(event.getX() - mLstPointF.x, event.getY() - mLstPointF.y); 39 //更新觸控位置 40 mLstPointF.set(event.getX(), event.getY()); 41 // 更新圖片區域 42 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 43 mMatrix.mapRect(mRectF); 44 //重新整理 45 invalidate(); 46 } 47 break; 48 } 49 //注意這裡需要返回true,因為當前自定義view繼承自基類View,預設是無法消費觸控事件的 50 return true; 51 } 52 53 @Override 54 protected void onDraw(Canvas canvas) { 55 super.onDraw(canvas); 56 canvas.drawBitmap(mBitmap, mMatrix, mPaint); 57 } 58 }
程式碼邏輯比較簡單,關鍵處也有這注釋說明,這裡就不多說了。使用該自定義View的佈局如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <com.example.demos.customviewdemo.SingleTouchDragView 7 android:layout_width="match_parent" 8 android:layout_height="match_parent" /> 9 </RelativeLayout>
用一根手指在圖片上進行拖動,效果如下左圖所示,圖片隨著手指在滑動:
效果很完美,但上述程式碼存在一個問題,就是在單指操作的情況下,可以正常被拖動,但是如果是多指操作的時候,就會混亂了。右圖為兩根手指滑動的圖片的效果,因為兩根手指都在移動, 導致ACTION_MOVE事件中,一會兒以第一根手指的觸控點為座標,一會兒又以第二根手指的觸控點為座標,這就導致圖片頻繁跳躍。
二、實現多指操作時只有第一根手指可以拖動圖片
這一節我們在上述程式碼基礎上,實現第一根手指在拖動圖片時,另一根手指繼續按下並拖動時無效,也就是第二根手指無法拖動,對第一根手指沒有干擾。由於是多點觸控,需要使用getActionMasked()來獲取事件,並監聽ACTION_POINTER_DOWN和ACTION_POINTER_UP事件。
1 public class MultiTouchDragView extends View { 2 private static final String TAG = "songzheweiwang"; 3 private Bitmap mBitmap; 4 private RectF mRectF; 5 private Matrix mMatrix; 6 private Paint mPaint; 7 private PointF mLstPointF; 8 private boolean mCanDrag = false; 9 10 public MultiTouchDragView(Context context, @Nullable AttributeSet attrs) { 11 super(context, attrs); 12 init(); 13 } 14 15 private void init() { 16 mPaint = new Paint(); 17 mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog); 18 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 19 mMatrix = new Matrix(); 20 mLstPointF = new PointF(); 21 } 22 23 @Override 24 public boolean onTouchEvent(MotionEvent event) { 25 switch (event.getActionMasked()) { 26 case MotionEvent.ACTION_DOWN: 27 case MotionEvent.ACTION_POINTER_DOWN: 28 //pointerId為0的手指(即我們定義的第一根手指)按下在指定區域內 29 if (event.getPointerId(event.getActionIndex()) == 0 && mRectF.contains(event.getX(), event.getY())) { 30 mCanDrag = true; 31 //getX()和getY()沒有傳入引數時,預設傳入的0 32 mLstPointF.set(event.getX(), event.getY()); 33 } 34 break; 35 case MotionEvent.ACTION_MOVE: 36 if (mCanDrag) { 37 int pointerIndex = event.findPointerIndex(0);//第一根手指的pointerId為0 38 //這裡需要注意,多手指頻繁按下和擡起時可能會出現pointerIndex為-1的情況,如不處理,後面會報錯 39 if (pointerIndex == -1) { 40 break; 41 } 42 mMatrix.postTranslate(event.getX(pointerIndex) - mLstPointF.x, 43 event.getY(pointerIndex) - mLstPointF.y); 44 mLstPointF.set(event.getX(pointerIndex), event.getY(pointerIndex)); 45 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 46 mMatrix.mapRect(mRectF); 47 invalidate(); 48 } 49 break; 50 case MotionEvent.ACTION_POINTER_UP: 51 case MotionEvent.ACTION_UP: 52 if (event.getPointerId(event.getActionIndex()) == 0) { 53 mCanDrag = false; 54 } 55 break; 56 } 57 return true; 58 } 59 60 @Override 61 protected void onDraw(Canvas canvas) { 62 super.onDraw(canvas); 63 canvas.drawBitmap(mBitmap, mMatrix, mPaint); 64 } 65 }
由於我們要實現的效果是隻有第一根手指可以拖動圖片,所以在第29行和52行中,根據pointerId是否為0來判斷是否需要更新介面。上一篇文章中說過,在處理多點觸控事件時,要用pointerId來跟蹤手指事件。由於第一根手指的pointerId為0,所以通過pointerId是否為0來判斷是否為第一根手指。當有多根手指在螢幕上時,第一根手指擡起再按下,它仍然被認為是第一跟手指,此時觸發的是ACTION_POINTER_DOWN事件,所以第一根手指按下,ACTION_POINTER_DOWN和ACTION_DOWN都有可能觸發。如果判斷是第一根手指按下了,就記錄下它按下時的座標,並設定mCanDrag為true,表示可以滑動。而手指擡起時,可能是最後一根擡起的手指,也可能不是,所以ACTION_POINTER_UP和ACTION_UP也都可能觸發。如果檢測到第一根手指擡起了,就設定mCanDrag為false,表示圖片不能夠再滑動了。在ACTION_MOVE事件中,第37行是固定使用,都需要根據findPointerIndex(int pinterId)來得到pointerIndex,因為獲取指定手指事件座標的函式傳入的引數都是它。結合程式碼中的註釋,剩下的邏輯應該就比較容易看懂了。
效果圖如下,用兩根手指來依次按下並拖動圖片:
我們發現,只有第一根手指在滑動時,圖片才會跟著移動,第二根手指(右邊的手指)的滑動無效,完美!!!
三、實現兩根手指共同拖動圖片
上面實現的效果還不夠,使用者在使用中,第二根手指滑動時也能接替第一根手指繼續滑動。基本思路大致是,記錄當前活動手指的pointerId,ACTION_MOVE中以活動手指為基礎來確定滑動操作。仍然在上述程式碼基礎上修改來實現。
1 public class MultiTouchDragView2 extends View { 2 private static final String TAG = "songzheweiwang"; 3 private Bitmap mBitmap; 4 private RectF mRectF; 5 private Matrix mMatrix; 6 private Paint mPaint; 7 private PointF mLstPointF; 8 private boolean mCanDrag = false; 9 private int mActivePointerId; 10 private final int INVALID_POINTER = -1; 11 12 public MultiTouchDragView2(Context context, @Nullable AttributeSet attrs) { 13 super(context, attrs); 14 init(); 15 } 16 17 private void init() { 18 mPaint = new Paint(); 19 mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog); 20 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 21 mMatrix = new Matrix(); 22 mLstPointF = new PointF(); 23 } 24 25 @Override 26 public boolean onTouchEvent(MotionEvent event) { 27 int actionIndex = event.getActionIndex(); 28 switch (event.getActionMasked()) { 29 case MotionEvent.ACTION_DOWN: 30 //getX()和getY()沒有傳入引數時,預設傳入的0 31 if (mRectF.contains(event.getX(), event.getY())) { 32 mActivePointerId = 0; //第一根手指按下時,pointerId和pointerIndex都為0 33 mCanDrag = true; 34 mLstPointF.set(event.getX(), event.getY()); 35 } 36 break; 37 case MotionEvent.ACTION_POINTER_DOWN: 38 //有新落下的手指,則將新落下的手指作為活動手指,儲存下活動手指的座標 39 mActivePointerId = event.getPointerId(actionIndex); 40 mLstPointF.set(event.getX(actionIndex), event.getY(actionIndex)); 41 break; 42 case MotionEvent.ACTION_MOVE: 43 if (mActivePointerId == INVALID_POINTER) { 44 break; 45 } 46 if (mCanDrag) {
47 //這裡根據活動手指的pointerId來找到pointerIndex,而不再是固定的手指的pointerId了 48 int pointerIndex = event.findPointerIndex(mActivePointerId); 49 if (pointerIndex == -1) { 50 break; 51 } 52 mMatrix.postTranslate(event.getX(pointerIndex) - mLstPointF.x, 53 event.getY(pointerIndex) - mLstPointF.y); 54 mLstPointF.set(event.getX(pointerIndex), event.getY(pointerIndex)); 55 mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 56 mMatrix.mapRect(mRectF); 57 invalidate(); 58 } 59 break; 60 case MotionEvent.ACTION_POINTER_UP: 61 //如果當前擡起的手指為活動手指,那麼活動手指就傳給留下的手指中pointerIndex最前面的一個 62 if (mActivePointerId == event.getPointerId(actionIndex)) { 63 int newPointerIndex = actionIndex == 0 ? 1 : 0; 64 mActivePointerId = event.getPointerId(newPointerIndex); 65 mLstPointF.set(event.getX(newPointerIndex), event.getY(newPointerIndex)); 66 } 67 break; 68 case MotionEvent.ACTION_UP: 69 //最後一根手指也擡起來了 70 mActivePointerId = INVALID_POINTER; 71 mCanDrag = false; 72 break; 73 } 74 return true; 75 } 76 77 @Override 78 protected void onDraw(Canvas canvas) { 79 super.onDraw(canvas); 80 canvas.drawBitmap(mBitmap, mMatrix, mPaint); 81 } 82 }
由於需要依據活動的手指來拖動圖片,所以需要實時記錄下活動手指的座標,如第40、54、65行所示。依然用兩根手指依次拖動圖片,效果如下所示:
現在可以看到,兩根手指輪流正常拖動圖片了,毫無違和感。
結語
到目前為止,多點觸控相關的內容,我想講的已經講完了,上一篇講理論,這一篇講案例,難點其實主要就是pointerIndex和pointerId的理解和使用。希望通過這兩篇文章,對讀者理解多點觸控有所幫助。由於文中程式碼結構比較簡單,就沒有必要提供原始碼了,讀者自己建立好專案,把這些程式碼依次拷貝過去就可以了,非常簡單。還有就是筆者比較窮,使用的免費軟體,所以文中gif圖上都打了水印,以後掙錢了也去享受一下付費服務,把水印給去掉。文中如果有描述不準確或不妥的地方,歡迎來拍磚,萬分感謝,Bye!!!
參考文章
【Android多點觸控最佳實踐】
【安卓自定義View進階-多點觸控詳解】