一、提要
最近在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; @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; } }
这里的图像的位置和大小的变化都用到了矩阵运算,不太清楚的话可以先补充一下线性代数的知识。
拖拽的实现就是用矩阵记录手指移动的距离;缩放的时候,首先要记录两只手指最开始的距离,然后当手指移动的时候,实时计算出手指的距离,与之前的距离相除得到缩放的比例,然后用矩阵的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; @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还往上粘,唉.....所以,最好还是自己踏踏实实研究。