已经有很多开源的缩放控件了,实际做项目没有必要重复造轮子,但对于学习来说自己亲自实现一个缩放的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
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
而拖动则在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=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 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) ;
}
}