一、提要
實施上它的功能非常的簡單,類似一個ToDoList,但它將用戶體驗做到瞭極致,其中一個最大的特點就是將手勢和多點觸控成功得融入到瞭應用之中。
這篇文章要探究的就是在Android中的手勢和多點觸控的原理及實現。
二、最原始的單點拖拽和兩點縮放
原理:對於常規的控件觸控操作,在setOnTouchListener()接口中,實現 onTouchEvent()方法來處理。
效果:
[java] view plain
代碼清單:
[java]
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;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myImageView=(ImageView)findViewById(R.id.myImageView);
myImageView.setOnTouchListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public 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;
//有手指抬起,將模式設為NONE
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
default:
}
myImageView.setImageMatrix(tmpMatrix);
return true;
}
}
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;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myImageView=(ImageView)findViewById(R.id.myImageView);
myImageView.setOnTouchListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public 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;
//有手指抬起,將模式設為NONE
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
default:
}
myImageView.setImageMatrix(tmpMatrix);
return true;
}
}
代碼解釋:MainActivity實現OnTouchLietener的接口,將ImageView的觸控 監聽器設置為this,在重載函數OnTouch中實現對觸控事件的處理。
這裡的圖像的位置和大小的變化都用到瞭矩陣運算,不太清楚的話可以先補充一下線性代數的知識。
拖拽的實現就是用矩陣記錄手指移動的距離;縮放的時候,首先要記錄兩隻手指最開始的距離,然後當手指移動的時候,實時計算出手指的距離,與之前的距離相除得到縮放的比例,然後用矩陣的scale方法存儲。
函數的最後調用 setImageMatrix()來實現對TextView的縮放或移動。
[java]
二、手勢識別
上面的例子雖然實現瞭基本的觸控功能,而且低版本的系統也能很好的支持,但如果遇到瞭高級的觸控事件,比如雙擊,長按之類,實現起來就非常麻煩瞭!
好在後續版本的api提供瞭更加棒的接口,我們可以很簡單地來實現想要的效果。
這裡要用到的是Android給我們提供的手勢識別工具GestureDetector,需要2.2及以上的系統版本。
下面的例子實現的效果是:單點拖拽,滑動切換imageView的內容,兩點縮放,雙擊圖像改變圖像顯示狀態。
效果:
[java]
<SPAN style="FONT-FAMILY: Arial">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;
@Override
public 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;
}
@Override
public 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();
@Override
public boolean onScale(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
newDist=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;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
oldDist=detector.getCurrentSpan();
newDist=detector.getCurrentSpan();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
</SPAN>
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;
@Override
public 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;
}
@Override
public 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();
@Override
public boolean onScale(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
newDist=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;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
oldDist=detector.getCurrentSpan();
newDist=detector.getCurrentSpan();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// TODO Auto-generated method stub
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
代碼解釋:
這裡我定義瞭兩個GestrueListener,一個專門用於處理縮放的ScaleOnGestrueListener一個SimpleOnGestrueListener,當觸控的點數為2的時候調用前者來處理,一般常用的手勢用後者來處理。
原理和前面的差不多,隻是調用不同的接口和不同的方法來實現,但是更加方便也更加清晰.
三、一點後記
學習Andorid中的某個類的時候,其實最好的方法是去看官方的API,有時候網上雖然有現成的代碼給你,但實際運用的時候還是會有各種各樣的問題,很多文章大都有雷同,甚至代碼本身就有bug還往上粘,唉…..所以,最好還是自己踏踏實實研究。