标签:
已经有很多开源的缩放控件了,实际做项目没有必要重复造轮子,但对于学习来说自己亲自实现一个缩放的ImageView是大有益处的。所以这里分享一下自己学习的心得。
1、创建一个类继承ImageView。
public class GestureImageView extends ImageView { public GestureImageView(Context context) { super(context); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
2、既然要实现手势缩放,那么首先应该取得控件的触控事件,包括多点触摸。这里在控件直接通过View的OnTouchListener来获取所需事件。
注意:如果想要实现用ViewPager和手势缩放控件做相册应用的话,最好将事件封装在控件外部,否则会跟父控件事件冲突出现莫名其妙的Bug。
public void GestureImageViewInit(){ this.setOnTouchListener(this); } public GestureImageView(Context context) { super(context); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); GestureImageViewInit(); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //手指按下事件 Log.e("TouchEvent","ActionDown"); break; case MotionEvent.ACTION_POINTER_DOWN: //屏幕上已经有一个点按住 再按下一点时触发该事件 Log.e("TouchEvent","ActionPointerDown"); break; case MotionEvent.ACTION_POINTER_UP: //屏幕上已经有两个点按住 再松开一点时触发该事件 Log.e("TouchEvent","ActionPointerUp"); break; case MotionEvent.ACTION_MOVE: //手指移动时触发事件 Log.e("TouchEvent","ActionMove"); break; case MotionEvent.ACTION_UP: //手指松开时触发事件 Log.e("TouchEvent","ActionUp"); break; } //注意这里return 的一定要是true 否则只会触发按下事件 return true; }
3、实现ImageView缩放和位移基本操作
public voidsetImageMatrix(Matrix matrix);
最基本的操作就是通过Matrix来实现ImageView的缩放和位移。由于后面会有大量的坐标操作,坐标变量统一为PointF类型。而且要实现Matrix操作,ScaleType也需要设置成Matrix。声明一个变量matrix用于记录当前的matrix操作。
为了便于初始化,将初始化的东西统一放在GestureImageViewInit()中,后面会持续将东西放进ImageView中。
private Matrix matrix; public void GestureImageViewInit(){ this.setOnTouchListener(this); this.setScaleType(ScaleType.MATRIX); matrix=new Matrix(); } public GestureImageView(Context context) { super(context); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); GestureImageViewInit(); }
/** * 根据缩放因子缩放图片 * @param scale */ public void setImageScale(PointF scale){ matrix.setScale(scale.x, scale.y); this.setImageMatrix(matrix); }
/** * 根据偏移量改变图片位置 * @param offset */ public void setImageTranslation(PointF offset){ matrix.postTranslate(offset.x, offset.y); this.setImageMatrix(matrix); }
先上一张无任何操作设置的Demo,我们会发现图片显示不正常,因为我们没有对图片进行任何操作,所以图片为原图大小,并且在安卓中,坐标系原点是左上角,因此我们看到的是原图的左上部分,一般相册浏览都会对图片进行自适应显示,这里我们先实现图片最开始的自适应显示。
要获取view的宽高,对onMeasure函数重写即可。同时,由于放大仍然是以左上角为坐标原点的,所以放大之后需要进行唯一操作将图片移动至view的中心。这里需要保存放大前原始图像的大小imageSize和缩放操作后的scaleSize,所以对setImageScale进行修改。
private PointF viewSize; private PointF imageSize; private PointF scaleSize; private PointF originScale; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width=MeasureSpec.getSize(widthMeasureSpec); int height=MeasureSpec.getSize(heightMeasureSpec); viewSize=new PointF(width,height); Log.e("view size",viewSize.toString()); //获取当前Drawable的大小 Drawable drawable=getDrawable(); if(drawable==null){ Log.e("no drawable","drawable is nullPtr"); }else { imageSize=new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight()); Log.e("drawable size",imageSize.toString()); } FitCenter(); } /** * 使图片保存在中央 */ public void FitCenter(){ float scaleH=viewSize.y/imageSize.y; float scaleW=viewSize.x/imageSize.x; //选择小的缩放因子确保图片全部显示在视野内 float scale =scaleH<scaleW?scaleH:scaleW; //根据view适应大小 setImageScale(new PointF(scale, scale)); originScale.set(scale,scale); //根据缩放因子大小来将图片中心调整到view 中心 if(scaleH<scaleW) setImageTranslation(new PointF(viewSize.x/2-scaleSize.x/2,0)); else setImageTranslation(new PointF(0,viewSize.y/2-scaleSize.y/2)); } /** * 根据缩放因子缩放图片 * @param scale */ public void setImageScale(PointF scale){ matrix.setScale(scale.x, scale.y); scaleSize.set(scale.x*imageSize.x,scale.y*imageSize.y); this.setImageMatrix(matrix); }
4、实现双击放大
完成了前面的准备工作之后,先来实现第一个小功能,双击放大,这里放大倍数为2倍。双击间隔为280ms,可以感觉自己的感觉调。
long doubleClickTimeSpan=280; long lastClickTime=0; int rationZoomIn=2;将ActionDown事件处理修改成下面这样。双击放大和复原这一步就完成了。
case MotionEvent.ACTION_DOWN: //手指按下事件 if(event.getPointerCount()==1){ if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){ //双击事件触发 Log.e("TouchEvent","DoubleClick"); if(curMode==0) { curMode=1; setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn)); }else { curMode=0; FitCenter(); } }else { lastClickTime=event.getEventTime(); } }
5、实现根据点击位置放大。(这里暂且不考虑边界检测问题)
要根据点击位置为中心进行放大,那么首先就要记录双击位置。
PointF start;
假设点击点坐标为(x1,y1) 在图片上归一化坐标即以图片左上角为原点的坐标为
((x1-curPoint.x)/scaleSize.x,(y1-curPoint.y)/scaleSize.y),记录为
relativePoint(x2,y2)。(如果点击位置超出了图片范围,那么结果需要另行处理。)
那么经过缩放操作之后这一点在图片上的归一化坐标是不变的,但绝对坐标变成了(x2*scaleSize.x,y2*scaleSize.y)。
只要将绝对坐标移动至(x1,y1)处就可以实现以点击中心放大了。
修改如下
case MotionEvent.ACTION_DOWN: start.set(event.getX(),event.getY()); //手指按下事件 if(event.getPointerCount()==1){ if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){ //双击事件触发 Log.e("TouchEvent", "DoubleClick"); if(curMode==ZoomMode.Ordinary) { curMode=ZoomMode.ZoomIn; relativePoint=new PointF(); //计算归一化坐标 relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y); setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn)); setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y)); }else { curMode=ZoomMode.Ordinary; FitCenter(); } }else { lastClickTime=event.getEventTime(); } } break;
curPoint变量在setImageTranslation中获取。
/** * 根据偏移量改变图片位置 * @param offset */ public void setImageTranslation(PointF offset){ matrix.postTranslate(offset.x, offset.y); curPoint.set(offset); this.setImageMatrix(matrix); }
6、实现双指缩放和拖动操作
双指缩放就需要用到ActionPointer这个事件。
首先添加变量用于记录双指中心点,双指距离。记录
private PointF center; private float doubleFingerDistance=0;
public void FitCenter(){ float scaleH=viewSize.y/imageSize.y; float scaleW=viewSize.x/imageSize.x; //选择小的缩放因子确保图片全部显示在视野内 float scale =scaleH<scaleW?scaleH:scaleW; //根据view适应大小 setImageScale(new PointF(scale, scale)); originScale.set(scale, scale); //根据缩放因子大小来将图片中心调整到view 中心 if(scaleH<scaleW) { setImageTranslation(new PointF(viewSize.x / 2 - scaleSize.x / 2, 0)); fitMode=1; } else { fitMode=0; setImageTranslation(new PointF(0, viewSize.y / 2 - scaleSize.y / 2)); } //记录缩放因子 下次继续从这个比例缩放 scaleDoubleZoom=originScale.x; }
而拖动则在ActionMove里判断偏移量,将偏移量附加到ImageView上即可。
代码如下
case MotionEvent.ACTION_MOVE: //手指移动时触发事件 if(event.getPointerCount()==1){ if(curMode==ZoomMode.ZoomIn){ setImageTranslation(new PointF(event.getX() - start.x, event.getY() - start.y)); start.set(event.getX(),event.getY()); } }else { //双指缩放时判断是否满足一定距离 if (Math.abs(getDoubleFingerDistance(event) - doubleFingerDistance) > 50 && curMode != ZoomMode.DoubleZoomIn) { //获取双指中点 center.set((event.getX(0) + event.getX(1)) / 2, (event.getY(0) + event.getY(1)) / 2); //设置起点 start.set(center); curMode = ZoomMode.DoubleZoomIn; doubleFingerDistance = getDoubleFingerDistance(event); relativePoint = new PointF(); //根据图片当前坐标值计算归一化坐标 relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y); } if(curMode==ZoomMode.DoubleZoomIn) { float scale =scaleDoubleZoom*getDoubleFingerDistance(event)/doubleFingerDistance; setImageScale(new PointF(scale, scale)); setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y)); } } break;
这里已经可以缩放并且跟随手指移动了。还有很多事情需要做,比如双指缩放时也可以进行移动等等这类细节。
写着写着,发现这个控件其实并不难,但是需要注意处理的细节特别多,尤其是边界条件这一块。
原来的项目中已经实现了,贴上一部分作为参考。就是判断上下左右边界进行偏移量调整。
/** *边界修正处理函数 使图片一直在可视范围内,根据margin可以适当将黑边显示出来 * @param offset 偏移量 * @param margin 超出图片边界的余量 */ public void boundaryCorrect(Vector2 offset,float margin){ Vector2 XandY=getMatrixTranslation(matrix); float xOver; float yOver; //设置上下左右的边界 if(currentBitmapSize.x>=viewSize.x){ xLeft=0; xRight=-currentBitmapSize.x+viewSize.x; }else { //图片的宽度比视图小时,则应处在中间位置 xLeft=(viewSize.x-currentBitmapSize.x)/2; xRight=viewSize.x-xLeft-currentBitmapSize.x; } if(currentBitmapSize.y>=viewSize.y){ yTop=0; yBottom=-currentBitmapSize.y+viewSize.y; } else { //图片的高度比视图小时,则应处在中间位置 yTop=(viewSize.y-currentBitmapSize.y)/2; yBottom=viewSize.y-yTop-currentBitmapSize.y; } //修正offset //左边界 xOver=XandY.x+offset.x-xLeft; if(XandY.x+offset.x>xLeft) offset.setX((float) Math.pow((margin-xOver) / margin, 2) * offset.x); //右边界 xOver=xRight-XandY.x-offset.x; if(XandY.x+offset.x<xRight) offset.setX((float) Math.pow((margin-xOver) / margin, 2) * offset.x); //上边界 yOver=XandY.y+offset.y-yTop; if(XandY.y+offset.y>=yTop) offset.setY((float) Math.pow((margin-yOver) / margin, 2) * offset.y); //下边界 yOver=yBottom-XandY.y-offset.y; if(XandY.y+offset.y<= yBottom) offset.setY((float) Math.pow((margin-yOver) / margin, 2) * offset.y); }
同样这里也是非常繁杂的步骤,不再继续写了。
关于控件动画,有很多方式可以实现,这里我用了子线程定时回调刷新位置,大概意思就是将一步操作细分为多步操作,细分的程度可以自己选择,不过不推荐我这种实现方式......
贴下完整的Demo代码吧,有时间会继续优化完成的= =
package com.qtree.gestureimageview; import android.content.Context; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; /** * Created by John on 2016/5/9. */ public class GestureImageView extends ImageView implements View.OnTouchListener { public class ZoomMode{ public final static int Ordinary=0; public final static int ZoomIn=1; public final static int DoubleZoomIn=2; } private int curMode=0; private Matrix matrix; private PointF viewSize; private PointF imageSize; private PointF scaleSize; //记录图片当前坐标 private PointF curPoint; private PointF originScale; //0:宽度适应 1:高度适应 private int fitMode=0; private PointF start; private PointF center; private float scaleDoubleZoom=0; private PointF relativePoint; private float doubleFingerDistance=0; long doubleClickTimeSpan=280; long lastClickTime=0; int rationZoomIn=2; public void GestureImageViewInit(){ this.setOnTouchListener(this); this.setScaleType(ScaleType.MATRIX); matrix=new Matrix(); originScale=new PointF(); scaleSize=new PointF(); start=new PointF(); center=new PointF(); curPoint=new PointF(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width=MeasureSpec.getSize(widthMeasureSpec); int height=MeasureSpec.getSize(heightMeasureSpec); viewSize=new PointF(width,height); //获取当前Drawable的大小 Drawable drawable=getDrawable(); if(drawable==null){ Log.e("no drawable","drawable is nullPtr"); }else { imageSize=new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight()); } FitCenter(); } /** * 使图片保存在中央 */ public void FitCenter(){ float scaleH=viewSize.y/imageSize.y; float scaleW=viewSize.x/imageSize.x; //选择小的缩放因子确保图片全部显示在视野内 float scale =scaleH<scaleW?scaleH:scaleW; //根据view适应大小 setImageScale(new PointF(scale, scale)); originScale.set(scale, scale); //根据缩放因子大小来将图片中心调整到view 中心 if(scaleH<scaleW) { setImageTranslation(new PointF(viewSize.x / 2 - scaleSize.x / 2, 0)); fitMode=1; } else { fitMode=0; setImageTranslation(new PointF(0, viewSize.y / 2 - scaleSize.y / 2)); } //记录缩放因子 下次继续从这个比例缩放 scaleDoubleZoom=originScale.x; } public GestureImageView(Context context) { super(context); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); GestureImageViewInit(); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: start.set(event.getX(),event.getY()); //手指按下事件 if(event.getPointerCount()==1){ if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){ //双击事件触发 Log.e("TouchEvent", "DoubleClick"); if(curMode==ZoomMode.Ordinary) { curMode=ZoomMode.ZoomIn; relativePoint=new PointF(); //计算归一化坐标 relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y); setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn)); setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y)); }else { curMode=ZoomMode.Ordinary; FitCenter(); } }else { lastClickTime=event.getEventTime(); } } break; case MotionEvent.ACTION_POINTER_DOWN: //屏幕上已经有一个点按住 再按下一点时触发该事件 doubleFingerDistance=getDoubleFingerDistance(event); break; case MotionEvent.ACTION_POINTER_UP: //屏幕上已经有两个点按住 再松开一点时触发该事件 curMode=ZoomMode.ZoomIn; scaleDoubleZoom=scaleSize.x/imageSize.x; if(scaleSize.x<viewSize.x&&scaleSize.y<viewSize.y){ curMode=ZoomMode.Ordinary; FitCenter(); } break; case MotionEvent.ACTION_MOVE: //手指移动时触发事件 if(event.getPointerCount()==1){ if(curMode==ZoomMode.ZoomIn){ setImageTranslation(new PointF(event.getX() - start.x, event.getY() - start.y)); start.set(event.getX(),event.getY()); } }else { //双指缩放时判断是否满足一定距离 if (Math.abs(getDoubleFingerDistance(event) - doubleFingerDistance) > 50 && curMode != ZoomMode.DoubleZoomIn) { //获取双指中点 center.set((event.getX(0) + event.getX(1)) / 2, (event.getY(0) + event.getY(1)) / 2); //设置起点 start.set(center); curMode = ZoomMode.DoubleZoomIn; doubleFingerDistance = getDoubleFingerDistance(event); relativePoint = new PointF(); //根据图片当前坐标值计算归一化坐标 relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y); } if(curMode==ZoomMode.DoubleZoomIn) { float scale =scaleDoubleZoom*getDoubleFingerDistance(event)/doubleFingerDistance; setImageScale(new PointF(scale, scale)); setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y)); } } break; case MotionEvent.ACTION_UP: //手指松开时触发事件 break; } //注意这里return 的一定要是true 否则只会触发按下事件 return true; } /** * 根据缩放因子缩放图片 * @param scale */ public void setImageScale(PointF scale){ matrix.setScale(scale.x, scale.y); scaleSize.set(scale.x*imageSize.x,scale.y*imageSize.y); this.setImageMatrix(matrix); } /** * 根据偏移量改变图片位置 * @param offset */ public void setImageTranslation(PointF offset){ matrix.postTranslate(offset.x, offset.y); curPoint.set(offset); this.setImageMatrix(matrix); } public static float getDoubleFingerDistance(MotionEvent event){ float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float)Math.sqrt(x * x + y * y) ; } }
标签:
原文地址:http://blog.csdn.net/qq402335257/article/details/51356772