定义ImageView,实现功能如下:
1.初始化时图片垂直居中显示,拉伸图片宽度至ImageView宽度。
2.使用两根手指放大缩小图片,可设置最大放大倍数,当图片小于ImageView宽度时,在手指离开屏幕时恢复到ImageView宽度。
3.支持双击放大缩小。当图片处于未放大状态时,双击放大至指定倍数,当图片处于放大状态时,双击恢复至未放大状态。
4.图片拖动效果。当图片处于未放大状态时,不可拖动。
5.图片拖动效果。当放大后的高度不超过ImageView时,不可垂直拖动。(由于默认设置拉伸宽度至ImageView宽度,水平方向可不判断)。
6.图片拖动效果。当图片向右拖动时,若左边缘超出左边界,则停止水平拖动。同理上下右边缘,即拖动后不会看到背景留白。
Android中可以通过使用Matrix类实现图片的缩放,为了实现通过手势控制需要监听onTouch事件,所以原理简单来说就是通过监听onTouch的各种事件来控制Matrix类。其中具体控制方式如下:
onTouch | Matrix | 辅助操作 |
ACTION_DOWN | 无 | 记录初始点,设置本次模式为拖动模式,ScaleType设置成Matrix |
ACTION_POINTER_DOWN | 无 | 设置本次模式为缩放模式 |
ACTION_MOVE | 根据模式执行postScale或postTranslate | |
ACTION_UP | 根据当前缩放级别决定是否重置Matrix | |
双击 | postScale | |
ACTION_CANCEL | 同UP |
以下将按功能点一一说明:
1、首先创建自定义视图MatrixImageView继承自ImageView,并且添加自定义onTouch手势监听和双击手势监听。
public class MatrixImageView extends ImageView{ private final static String TAG="MatrixImageView"; private GestureDetector mGestureDetector; /** 模板Matrix,用以初始化 */ private Matrix mMatrix=new Matrix(); /** 图片长度*/ private float mImageWidth; /** 图片高度 */ private float mImageHeight; public MatrixImageView(Context context, AttributeSet attrs) { super(context, attrs); MatrixTouchListener mListener=new MatrixTouchListener(); setOnTouchListener(mListener); mGestureDetector=new GestureDetector(getContext(), new GestureListener(mListener)); //背景设置为balck setBackgroundColor(Color.BLACK); //将缩放类型设置为FIT_CENTER,表示把图片按比例扩大/缩小到View的宽度,居中显示 setScaleType(ScaleType.FIT_CENTER); }
2、之后重写setImageBitmap方法初始化模板以及图片的宽度和长度。
@Override public void setImageBitmap(Bitmap bm) { // TODO Auto-generated method stub super.setImageBitmap(bm); //设置完图片后,获取该图片的坐标变换矩阵 mMatrix.set(getImageMatrix()); float[] values=new float[9]; mMatrix.getValues(values); //图片宽度为屏幕宽度除缩放倍数 mImageWidth=getWidth()/values[Matrix.MSCALE_X]; mImageHeight=(getHeight()-values[Matrix.MTRANS_Y]*2)/values[Matrix.MSCALE_Y]; }
3、接下来自定义监听器重写onTouch事件:当我们按下一个点时,会触发Down事件,而按下第二个点后,又会触发Action_Pointer_Down事件,在MatrixTouchListener中我们把按下一个点标记为拖动事件,按下两个点标记为缩放事件。
public class MatrixTouchListener implements OnTouchListener{ /** 拖拉照片模式 */ private static final int MODE_DRAG = 1; /** 放大缩小照片模式 */ private static final int MODE_ZOOM = 2; /** 不支持Matrix */ private static final int MODE_UNABLE=3; /** 最大缩放级别*/ float mMaxScale=6; /** 双击时的缩放级别*/ float mDobleClickScale=2; private int mMode = 0;// /** 缩放开始时的手指间距 */ private float mStartDis; /** 当前Matrix*/ private Matrix mCurrentMatrix = new Matrix(); /** 用于记录开始时候的坐标位置 */ private PointF startPoint = new PointF(); @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: //设置拖动模式 mMode=MODE_DRAG; startPoint.set(event.getX(), event.getY()); // isMatrixEnable(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: reSetMatrix(); break; case MotionEvent.ACTION_MOVE: if (mMode == MODE_ZOOM) { setZoomMatrix(event); }else if (mMode==MODE_DRAG) { setDragMatrix(event); } break; case MotionEvent.ACTION_POINTER_DOWN: if(mMode==MODE_UNABLE) return true; mMode=MODE_ZOOM;
// 计算两个手指间的距离mStartDis = distance(event); break; default: break; } return mGestureDetector.onTouchEvent(event); }
3.1、在事件方法setZoomMatrix(event)中,首先我们判断是否点击了两个点,如果不是则直接返回;之后使用distance方法计算出两个点之间移动后的距离并以之计算缩放倍数,因为我规定了图片缩放的最大倍数最后还需要验证这个缩放倍数是否越界了:
/** * 设置缩放Matrix * @param event */ private void setZoomMatrix(MotionEvent event) { //只有同时触屏两个点的时候才执行 if(event.getPointerCount()<2) return; float endDis = distance(event);// 结束距离 if (endDis > 10f) { // 两个手指并拢在一起的时候像素大于10 float scale = endDis / mStartDis;// 得到缩放倍数 mStartDis=endDis;//重置距离 mCurrentMatrix.set(getImageMatrix());//初始化Matrix float[] values=new float[9]; mCurrentMatrix.getValues(values);
// 检验scale,使图像缩放后不会超出最大倍数 scale = checkMaxScale(scale, values); setImageMatrix(mCurrentMatrix); } }
/** * 检验scale,使图像缩放后不会超出最大倍数 * @param scale * @param values * @return */ private float checkMaxScale(float scale, float[] values) { if(scale*values[Matrix.MSCALE_X]>mMaxScale) scale=mMaxScale/values[Matrix.MSCALE_X]; mCurrentMatrix.postScale(scale, scale,getWidth()/2,getHeight()/2); return scale; }
PS:为了防止经过缩放的图片小于屏幕大小,在手指离开屏幕后需要判断当前累计的缩放倍数,在当前累计的缩放倍数小于初始的倍数,需要重置缩放倍数使图片充满屏幕。方法reSetMatrix如下:
首先获取当前X轴缩放级别(由于默认拉伸宽度至ImageView宽度,缩放级别以X轴为准),再通过模板Matrix得到原始的X轴缩放级别,判断当前缩放级别是否小于模板缩放级别,若小于,则重置成模板缩放级别。
/** * 重置Matrix */ private void reSetMatrix() { if(checkRest()){ mCurrentMatrix.set(mMatrix); setImageMatrix(mCurrentMatrix); } } /** * 判断是否需要重置 * @return 当前缩放级别小于模板缩放级别时,重置 */ private boolean checkRest() { // TODO Auto-generated method stub float[] values=new float[9]; getImageMatrix().getValues(values); //获取当前X轴缩放级别 float scale=values[Matrix.MSCALE_X]; //获取模板的X轴缩放级别,两者做比较 mMatrix.getValues(values); return scale<values[Matrix.MSCALE_X]; }
3.2、在事件方法setDragMatrix(event)中,首先是通过isZoomChanged方法判断是否缩放过,若未缩放过则不可拖动(这种情况下图片全貌都可以看到,不需要拖动)。接着,拿当前坐标和按下时记录的startPoint坐标进行计算,得出拖动的距离。需要注意的是,在此需要对拖动距离做一个判断,当其小于10f时不进行拖动,否则会和双击事件冲突(在双击事件前同样会触发Move事件,两者一同执行的话,双击的缩放无法正常工作):
public void setDragMatrix(MotionEvent event) { if(isZoomChanged()){ float dx = event.getX() - startPoint.x; // 得到x轴的移动距离 float dy = event.getY() - startPoint.y; // 得到x轴的移动距离 //避免和双击冲突,大于10f才算是拖动 if(Math.sqrt(dx*dx+dy*dy)>10f){ startPoint.set(event.getX(), event.getY()); //在当前基础上移动 mCurrentMatrix.set(getImageMatrix()); float[] values=new float[9]; mCurrentMatrix.getValues(values); dx=checkDxBound(values,dx); dy=checkDyBound(values,dy); mCurrentMatrix.postTranslate(dx, dy); setImageMatrix(mCurrentMatrix); } } }
PS:为了防止将图片拖动至越界,当确定开始拖动的之后,先重置startPoint的坐标,接着,开始验证当前移动的位移量是否合法。以Y轴为例,首先获取ImageView高度,再通过sitImageBitmap方法中获取的图片真实高度和当前Y轴缩放级别计算出当前Y轴的显示高度。如果显示高度小于ImageView高度,表示当前显示的图片还没有ImageView高,在Y轴不需要移动都可看清全貌,Y轴位移量直接返回0。假如显示高度超过了ImageView高度,获取图片当前在Y轴的位移量(values[Matrix.MTRANS_Y]值),将其加上计算出的位移量后是否大于0,若大于0,表示图片上边缘将会离开ImageView上边缘,需要重新计算位移量。若上述条件不成立,判断当前位移量加上计算后的位移量,是否小于图片显示高度-屏幕高度,若小于表示图片下边缘将离开ImageView下边缘,同样需要重新计算。最后返回计算的Y轴偏移量。X轴同理。最后使用验证过的X、Y轴偏移量,在当前图片Matrix的基础上行进行偏移。
/** *和当前矩阵对比,检验dx,使图像移动后不会超出ImageView边界 * @param values * @param dx * @return */ private float checkDxBound(float[] values,float dx) { float width=getWidth(); if(mImageWidth*values[Matrix.MSCALE_X]<width) return 0; if(values[Matrix.MTRANS_X]+dx>0) dx=-values[Matrix.MTRANS_X]; else if(values[Matrix.MTRANS_X]+dx<-(mImageWidth*values[Matrix.MSCALE_X]-width)) dx=-(mImageWidth*values[Matrix.MSCALE_X]-width)-values[Matrix.MTRANS_X]; return dx; } /** * 和当前矩阵对比,检验dy,使图像移动后不会超出ImageView边界 * @param values * @param dy * @return */ private float checkDyBound(float[] values, float dy) { float height=getHeight(); if(mImageHeight*values[Matrix.MSCALE_Y]<height) return 0; if(values[Matrix.MTRANS_Y]+dy>0) dy=-values[Matrix.MTRANS_Y]; else if(values[Matrix.MTRANS_Y]+dy<-(mImageHeight*values[Matrix.MSCALE_Y]-height)) dy=-(mImageHeight*values[Matrix.MSCALE_Y]-height)-values[Matrix.MTRANS_Y]; return dy; }
4、最后是双击放大缩小图片效果,该功能在手势接口GestureListener中完成,在构造函数中将onTouchListner传递来进来。在此只重写两个方法:Down和onDoubleTap,只有在Down事件中返回true,onDoubleTap才能正常触发。在onDoubleClick事件中,首先通过isZoomChanged方法判断当前的缩放级别是否是模板Matrix的缩放级别,是的话将缩放倍数设置为2倍,否的话设置成1倍。主要代码如下:
private class GestureListener extends SimpleOnGestureListener{ private final MatrixTouchListener listener; public GestureListener(MatrixTouchListener listener) { this.listener=listener; } @Override public boolean onDown(MotionEvent e) { //捕获Down事件 return true; } @Override public boolean onDoubleTap(MotionEvent e) { //触发双击事件 listener.onDoubleClick(); return true; }
/** * 双击时触发 */ public void onDoubleClick(){ float scale=isZoomChanged()?1:mDobleClickScale; mCurrentMatrix.set(mMatrix);//初始化Matrix mCurrentMatrix.postScale(scale, scale,getWidth()/2,getHeight()/2); setImageMatrix(mCurrentMatrix); } /** * 判断缩放级别是否是改变过 * @return true表示非初始值,false表示初始值 */ private boolean isZoomChanged() { float[] values=new float[9]; getImageMatrix().getValues(values); //获取当前X轴缩放级别 float scale=values[Matrix.MSCALE_X]; //获取模板的X轴缩放级别,两者做比较 mMatrix.getValues(values); return scale!=values[Matrix.MSCALE_X]; }
最后贴下完整代码。(DEMO地址:照相机Demo)
package com.linj.album.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.PointF; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; /** * @ClassName: MatrixImageView * @Description: 带放大、缩小、移动效果的ImageView * @author LinJ * @date 2015-1-7 上午11:15:07 * */ public class MatrixImageView extends ImageView{ private final static String TAG="MatrixImageView"; private GestureDetector mGestureDetector; /** 模板Matrix,用以初始化 */ private Matrix mMatrix=new Matrix(); /** 图片长度*/ private float mImageWidth; /** 图片高度 */ private float mImageHeight; public MatrixImageView(Context context, AttributeSet attrs) { super(context, attrs); MatrixTouchListener mListener=new MatrixTouchListener(); setOnTouchListener(mListener); mGestureDetector=new GestureDetector(getContext(), new GestureListener(mListener)); //背景设置为balck setBackgroundColor(Color.BLACK); //将缩放类型设置为FIT_CENTER,表示把图片按比例扩大/缩小到View的宽度,居中显示 setScaleType(ScaleType.FIT_CENTER); } @Override public void setImageBitmap(Bitmap bm) { // TODO Auto-generated method stub super.setImageBitmap(bm); //设置完图片后,获取该图片的坐标变换矩阵 mMatrix.set(getImageMatrix()); float[] values=new float[9]; mMatrix.getValues(values); //图片宽度为屏幕宽度除缩放倍数 mImageWidth=getWidth()/values[Matrix.MSCALE_X]; mImageHeight=(getHeight()-values[Matrix.MTRANS_Y]*2)/values[Matrix.MSCALE_Y]; } public class MatrixTouchListener implements OnTouchListener{ /** 拖拉照片模式 */ private static final int MODE_DRAG = 1; /** 放大缩小照片模式 */ private static final int MODE_ZOOM = 2; /** 不支持Matrix */ private static final int MODE_UNABLE=3; /** 最大缩放级别*/ float mMaxScale=6; /** 双击时的缩放级别*/ float mDobleClickScale=2; private int mMode = 0;// /** 缩放开始时的手指间距 */ private float mStartDis; /** 当前Matrix*/ private Matrix mCurrentMatrix = new Matrix(); /** 用于记录开始时候的坐标位置 */ private PointF startPoint = new PointF(); @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: //设置拖动模式 mMode=MODE_DRAG; startPoint.set(event.getX(), event.getY()); isMatrixEnable(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: reSetMatrix(); break; case MotionEvent.ACTION_MOVE: if (mMode == MODE_ZOOM) { setZoomMatrix(event); }else if (mMode==MODE_DRAG) { setDragMatrix(event); } break; case MotionEvent.ACTION_POINTER_DOWN: if(mMode==MODE_UNABLE) return true; mMode=MODE_ZOOM; mStartDis = distance(event); break; default: break; } return mGestureDetector.onTouchEvent(event); } public void setDragMatrix(MotionEvent event) { if(isZoomChanged()){ float dx = event.getX() - startPoint.x; // 得到x轴的移动距离 float dy = event.getY() - startPoint.y; // 得到x轴的移动距离 //避免和双击冲突,大于10f才算是拖动 if(Math.sqrt(dx*dx+dy*dy)>10f){ startPoint.set(event.getX(), event.getY()); //在当前基础上移动 mCurrentMatrix.set(getImageMatrix()); float[] values=new float[9]; mCurrentMatrix.getValues(values); dx=checkDxBound(values,dx); dy=checkDyBound(values,dy); mCurrentMatrix.postTranslate(dx, dy); setImageMatrix(mCurrentMatrix); } } } /** * 判断缩放级别是否是改变过 * @return true表示非初始值,false表示初始值 */ private boolean isZoomChanged() { float[] values=new float[9]; getImageMatrix().getValues(values); //获取当前X轴缩放级别 float scale=values[Matrix.MSCALE_X]; //获取模板的X轴缩放级别,两者做比较 mMatrix.getValues(values); return scale!=values[Matrix.MSCALE_X]; } /** * 和当前矩阵对比,检验dy,使图像移动后不会超出ImageView边界 * @param values * @param dy * @return */ private float checkDyBound(float[] values, float dy) { float height=getHeight(); if(mImageHeight*values[Matrix.MSCALE_Y]<height) return 0; if(values[Matrix.MTRANS_Y]+dy>0) dy=-values[Matrix.MTRANS_Y]; else if(values[Matrix.MTRANS_Y]+dy<-(mImageHeight*values[Matrix.MSCALE_Y]-height)) dy=-(mImageHeight*values[Matrix.MSCALE_Y]-height)-values[Matrix.MTRANS_Y]; return dy; } /** *和当前矩阵对比,检验dx,使图像移动后不会超出ImageView边界 * @param values * @param dx * @return */ private float checkDxBound(float[] values,float dx) { float width=getWidth(); if(mImageWidth*values[Matrix.MSCALE_X]<width) return 0; if(values[Matrix.MTRANS_X]+dx>0) dx=-values[Matrix.MTRANS_X]; else if(values[Matrix.MTRANS_X]+dx<-(mImageWidth*values[Matrix.MSCALE_X]-width)) dx=-(mImageWidth*values[Matrix.MSCALE_X]-width)-values[Matrix.MTRANS_X]; return dx; } /** * 设置缩放Matrix * @param event */ private void setZoomMatrix(MotionEvent event) { //只有同时触屏两个点的时候才执行 if(event.getPointerCount()<2) return; float endDis = distance(event);// 结束距离 if (endDis > 10f) { // 两个手指并拢在一起的时候像素大于10 float scale = endDis / mStartDis;// 得到缩放倍数 mStartDis=endDis;//重置距离 mCurrentMatrix.set(getImageMatrix());//初始化Matrix float[] values=new float[9]; mCurrentMatrix.getValues(values); scale = checkMaxScale(scale, values); setImageMatrix(mCurrentMatrix); } } /** * 检验scale,使图像缩放后不会超出最大倍数 * @param scale * @param values * @return */ private float checkMaxScale(float scale, float[] values) { if(scale*values[Matrix.MSCALE_X]>mMaxScale) scale=mMaxScale/values[Matrix.MSCALE_X]; mCurrentMatrix.postScale(scale, scale,getWidth()/2,getHeight()/2); return scale; } /** * 重置Matrix */ private void reSetMatrix() { if(checkRest()){ mCurrentMatrix.set(mMatrix); setImageMatrix(mCurrentMatrix); } } /** * 判断是否需要重置 * @return 当前缩放级别小于模板缩放级别时,重置 */ private boolean checkRest() { // TODO Auto-generated method stub float[] values=new float[9]; getImageMatrix().getValues(values); //获取当前X轴缩放级别 float scale=values[Matrix.MSCALE_X]; //获取模板的X轴缩放级别,两者做比较 mMatrix.getValues(values); return scale<values[Matrix.MSCALE_X]; } /** * 判断是否支持Matrix */ private void isMatrixEnable() { //当加载出错时,不可缩放 if(getScaleType()!=ScaleType.CENTER){ setScaleType(ScaleType.MATRIX); }else { mMode=MODE_UNABLE;//设置为不支持手势 } } /** * 计算两个手指间的距离 * @param event * @return */ private float distance(MotionEvent event) { float dx = event.getX(1) - event.getX(0); float dy = event.getY(1) - event.getY(0); /** 使用勾股定理返回两点之间的距离 */ return (float) Math.sqrt(dx * dx + dy * dy); } /** * 双击时触发 */ public void onDoubleClick(){ float scale=isZoomChanged()?1:mDobleClickScale; mCurrentMatrix.set(mMatrix);//初始化Matrix mCurrentMatrix.postScale(scale, scale,getWidth()/2,getHeight()/2); setImageMatrix(mCurrentMatrix); } } private class GestureListener extends SimpleOnGestureListener{ private final MatrixTouchListener listener; public GestureListener(MatrixTouchListener listener) { this.listener=listener; } @Override public boolean onDown(MotionEvent e) { //捕获Down事件 return true; } @Override public boolean onDoubleTap(MotionEvent e) { //触发双击事件 listener.onDoubleClick(); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { // TODO Auto-generated method stub return super.onSingleTapUp(e); } @Override public void onLongPress(MotionEvent e) { // TODO Auto-generated method stub super.onLongPress(e); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return super.onScroll(e1, e2, distanceX, distanceY); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO Auto-generated method stub return super.onFling(e1, e2, velocityX, velocityY); } @Override public void onShowPress(MotionEvent e) { // TODO Auto-generated method stub super.onShowPress(e); } @Override public boolean onDoubleTapEvent(MotionEvent e) { // TODO Auto-generated method stub return super.onDoubleTapEvent(e); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { // TODO Auto-generated method stub return super.onSingleTapConfirmed(e); } } }
PS:当该ImageView在其他ViewGroup中如ViewPager中时,Move事件会和ViewGroup事件冲突导致在划屏进行ImageView的拖动时,Viewpager将会通过onInterceptTouchEvent方法拦截掉,返回给ImageView一个Cancel事件,这种情况下需要重写ViewGroup的onInterceptTouchEvent方法(这将在下一篇文章中说明)。