码迷,mamicode.com
首页 > 移动开发 > 详细

Android ImageView手势缩放完整的实现

时间:2016-05-12 12:48:03      阅读:272      评论:0      收藏:0      [点我收藏+]

标签:

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

当双指距离大于一定距离时进入双指缩放模式,此时根据双指距离的相对变化修改matrix,同时校正图片位置到双指中心(这里和上面关于中心缩放的原理是一样的)。因为双指缩放时会在之前的基础上再次放大,因此需要一个变量来保存当前的缩放比例,在FitCenter初始化为初始的scale。

    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;

技术分享


这里已经可以缩放并且跟随手指移动了。还有很多事情需要做,比如双指缩放时也可以进行移动等等这类细节。


7、实现边界检测

写着写着,发现这个控件其实并不难,但是需要注意处理的细节特别多,尤其是边界条件这一块。

原来的项目中已经实现了,贴上一部分作为参考。就是判断上下左右边界进行偏移量调整。

/**
     *边界修正处理函数 使图片一直在可视范围内,根据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);

    }

8、完善控件,加上边界回弹、缩放、复位等动画。

同样这里也是非常繁杂的步骤,不再继续写了。

关于控件动画,有很多方式可以实现,这里我用了子线程定时回调刷新位置,大概意思就是将一步操作细分为多步操作,细分的程度可以自己选择,不过不推荐我这种实现方式...... 




贴下完整的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) ;
    }



}













Android ImageView手势缩放完整的实现

标签:

原文地址:http://blog.csdn.net/qq402335257/article/details/51356772

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!