一、提要
最近在iPhone有一款應用非常火,較做Clear,這個是示範視頻:http://v.youku.com/v_show/id_XMzUyNjQ2NDk2.html
實施上它的功能非常的簡單,類似一個ToDoList,但它將使用者體驗做到了極致,其中一個最大的特點就是將手勢和多點觸控成功得融入到了應用之中。
這篇文章要探究的就是在Android中的手勢和多點觸控的原理及實現。
二、最原始的單點拖拽和兩點縮放
原理:對於常規的控制項觸控操作,在setOnTouchListener()介面中,實現
onTouchEvent()方法來處理。
效果:
代碼清單:
package com.example.multitouch;import android.os.Bundle;import android.annotation.SuppressLint;import android.annotation.TargetApi;import android.app.Activity;import android.graphics.Matrix;import android.graphics.PointF;import android.view.GestureDetector;import android.view.Menu;import android.view.MotionEvent;import android.view.View;import android.view.View.OnTouchListener;import android.widget.ImageView;import android.widget.Toast;import android.view.GestureDetector.OnGestureListener; public class MainActivity extends Activity implements OnTouchListener{public ImageView myImageView;private static final int NONE = 0; private static final int DRAG = 1; private static final int ZOOM = 2; private int mode = NONE; private Matrix tmpMatrix=new Matrix();;private Matrix savedMatrix = new Matrix(); private PointF startPoint = new PointF(); private PointF endPoint=new PointF();private PointF midPoint = new PointF(); private float oldDistance; @Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);myImageView=(ImageView)findViewById(R.id.myImageView);myImageView.setOnTouchListener(this);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.activity_main, menu);return true;}@Overridepublic boolean onTouch(View v, MotionEvent event) {//擷取觸控的點數int pointCount = event.getPointerCount(); switch(event.getAction() & MotionEvent.ACTION_MASK){ //單手指按下 case MotionEvent.ACTION_DOWN: //將當前的座標儲存為起始點 startPoint.set(event.getX(), event.getY()); tmpMatrix.set(myImageView.getImageMatrix()); savedMatrix.set(tmpMatrix); mode = DRAG; break; //第二根手指按下case MotionEvent.ACTION_POINTER_DOWN: oldDistance = (float) Math.sqrt((event.getX(0) - event.getX(1)) * (event.getX(0) - event.getX(1)) + (event.getY(0) - event.getY(1)) * (event.getY(0) - event.getY(1))); if (oldDistance > 10f) { savedMatrix.set(tmpMatrix); midPoint.set((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2);mode = ZOOM; } break; //指點杆保持按下,並且進行位移 case MotionEvent.ACTION_MOVE: //拖拽模式if (mode == DRAG) { tmpMatrix.set(savedMatrix); tmpMatrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y); } //縮放模式else if (mode == ZOOM) { float newDist = (float) Math.sqrt((event.getX(0) - event.getX(1)) * (event.getX(0) - event.getX(1)) + (event.getY(0) - event.getY(1)) * (event.getY(0) - event.getY(1))); if (newDist > 10f){ tmpMatrix.set(savedMatrix); float scale = newDist / oldDistance; tmpMatrix.postScale(scale, scale, midPoint.x, midPoint.y); } } break; //有手指抬起,將模式設為NONEcase MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: mode = NONE; break; default:} myImageView.setImageMatrix(tmpMatrix); return true; }}
代碼解釋:MainActivity實現OnTouchLietener的介面,將ImageView的觸控 監聽器設定為this,在重載函數OnTouch中實現對觸控事件的處理。
這裡的映像的位置和大小的變化都用到了矩陣運算,不太清楚的話可以先補充一下線性代數的知識。
拖拽的實現就是用矩陣記錄手指移動的距離;縮放的時候,首先要記錄兩隻手指最開始的距離,然後當手指移動的時候,Realtime Compute出手指的距離,與之前的距離相除得到縮放的比例,然後用矩陣的scale方法儲存。
函數的最後調用 setImageMatrix()來實現對TextView的縮放或移動。
二、手勢識別
上面的例子雖然實現了基本的觸控功能,而且低版本的系統也能很好的支援,但如果遇到了進階的觸控事件,比如雙擊,長按之類,實現起來就非常麻煩了!
好在後續版本的api提供了更加棒的介面,我們可以很簡單地來實現想要的效果。
這裡要用到的是Android給我們提供的手勢識別工具GestureDetector,需要2.2及以上的系統版本。
下面的例子實現的效果是:單點拖拽,滑動切換imageView的內容,兩點縮放,雙擊映像改變映像顯示狀態。
效果:
package com.example.gesture;import java.util.Random;import android.os.Bundle;import android.app.Activity;import android.graphics.Matrix;import android.graphics.PointF;import android.view.GestureDetector;import android.view.GestureDetector.SimpleOnGestureListener;import android.view.Menu;import android.view.MotionEvent;import android.view.ScaleGestureDetector;import android.view.ScaleGestureDetector.OnScaleGestureListener;import android.view.View;import android.widget.ImageView;import android.widget.Toast;public class MainActivity extends Activity {private GestureDetector myDetector;private Matrix matrix; private ImageView myImageView;private Random random;private ScaleGestureDetector mScaleGestureDetector;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);myDetector=new GestureDetector(this,new MyGestureListener()); mScaleGestureDetector=new ScaleGestureDetector(this,new MyScaleGestureListener());matrix=new Matrix(); myImageView=(ImageView)findViewById(R.id.myImageView);random=new Random();}@Override public boolean onTouchEvent(MotionEvent event) { int pointCount = event.getPointerCount(); if(pointCount==1)return myDetector.onTouchEvent(event); else return mScaleGestureDetector.onTouchEvent(event);} private class MyGestureListener extends SimpleOnGestureListener{Matrix mMatrix=new Matrix(); PointF startPoint=new PointF();@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // TODO Auto-generated method stub mMatrix.set(myImageView.getImageMatrix());System.out.println("distanceX:"+distanceX+"distanceY:"+distanceY); startPoint.set(e1.getRawX(), e1.getRawY());mMatrix.postTranslate(-distanceX,-distanceY); myImageView.setImageMatrix(mMatrix);return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){final int FLING_MIN_DISTANCE = 100, FLING_MIN_VELOCITY = 200; if (e1.getX() - e2.getX() > FLING_MIN_DISTANCE && Math.abs(velocityX) > FLING_MIN_VELOCITY) { // Fling left myImageView.setImageResource(R.drawable.pic0);Toast.makeText(getApplicationContext(), "Fling Left", Toast.LENGTH_SHORT).show(); } else if (e2.getX() - e1.getX() > FLING_MIN_DISTANCE && Math.abs(velocityX) > FLING_MIN_VELOCITY) { // Fling right switch(random.nextInt(5)){case 0:myImageView.setImageResource(R.drawable.pic2);break;case 1:myImageView.setImageResource(R.drawable.pic3);break;case 2:myImageView.setImageResource(R.drawable.pic7);break;case 3:myImageView.setImageResource(R.drawable.pic5);break;case 4:myImageView.setImageResource(R.drawable.pic6);break;default:}Toast.makeText(getApplicationContext(), "Fling Right", Toast.LENGTH_SHORT).show(); } return false;} // 使用者輕觸觸控螢幕,由1個MotionEvent ACTION_DOWN觸發 public boolean onDown(MotionEvent arg0) { Toast.makeText(getApplicationContext(), "onDown", Toast.LENGTH_SHORT).show(); return true; } @Overridepublic boolean onDoubleTap(MotionEvent e){if(myImageView.isShown())myImageView.setVisibility(View.INVISIBLE);else myImageView.setVisibility(View.VISIBLE);return false;}}private class MyScaleGestureListener implements OnScaleGestureListener{private float oldDist;private float newDist;Matrix mMatrix = new Matrix(); @Overridepublic boolean onScale(ScaleGestureDetector detector) {// TODO Auto-generated method stubnewDist=detector.getCurrentSpan();mMatrix.set(myImageView.getImageMatrix()); //縮放比例//float scale = detector.getScaleFactor()/3;float scale=newDist/oldDist;System.out.println("scale:"+scale);//mMatrix.setScale(scale, scale,detector.getFocusX(),detector.getFocusY());mMatrix.postScale(scale, scale,detector.getFocusX(),detector.getFocusY());myImageView.setImageMatrix(mMatrix); oldDist=newDist;return false;}@Overridepublic boolean onScaleBegin(ScaleGestureDetector detector) {// TODO Auto-generated method stuboldDist=detector.getCurrentSpan();newDist=detector.getCurrentSpan();return true;}@Overridepublic void onScaleEnd(ScaleGestureDetector detector) {// TODO Auto-generated method stub}}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {getMenuInflater().inflate(R.menu.activity_main, menu);return true;}}
代碼解釋:
這裡我定義了兩個GestrueListener,一個專門用於處理縮放的ScaleOnGestrueListener一個SimpleOnGestrueListener,當觸控的點數為2的時候調用前者來處理,一般常用的手勢用後者來處理。
原理和前面的差不多,只是調用不同的介面和不同的方法來實現,但是更加方便也更加清晰.
三、一點後記
學習Andorid中的某個類的時候,其實最好的方法是去看官方的API,有時候網上雖然有現成的代碼給你,但實際運用的時候還是會有各種各樣的問題,很多文章大都有雷同,甚至代碼本身就有bug還往上粘,唉.....所以,最好還是自己踏踏實實研究。