1. 程式人生 > >Android ImageView手勢縮放完整的實現

Android ImageView手勢縮放完整的實現

已經有很多開源的縮放控制元件了,實際做專案沒有必要重複造輪子,但對於學習來說自己親自實現一個縮放的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) ;
    }



}