手把手教你打造支援手勢放大縮小的ImageView

xiasuhuei321發表於2017-12-13

寫在前面

最近有了新的任務,學習的時間比以前少了不少,Java回爐的文估計是得緩緩了,不過每週一篇儘量保質保量。最近感覺我文寫的有點不好,因為我寫東西除非必要,不然概念性的東西我基本上都是一筆帶過……最近感覺這對看我文的人好像不是很友好,恩,我決定改一改,儘量寫的詳細而有趣一些。

1.jpg

好了廢話時間過了,前面也說了最近有了新任務,我現在是搞定使用者資訊這一塊。一般來說現在使用者都會有個頭像什麼的,光有個頭像還不夠,你還得能點選看個大圖吧?光看個大圖也不夠啊,不說多的,你最起碼得支援使用者手勢放大縮小什麼的吧?當時腦海裡第一個想到的是PhotoView,不過整個專案好像也只有這一塊涉及到使用者手勢放大縮小,算了,自己實現一個吧。當然了,經常刷hongyang大神部落格的我自然知道hongyang大神部落格裡有寫過這東西。所以趁週末有空果斷刷之~

實現思路

做一個東西之前我們肯定要分析需求,分析完之後我們就可以利用我們會的,或者知道可以實現但是現在不會的去嘗試解決這個需求。放大縮小圖片,腦子裡第一個反應就是矩陣,Android裡貌似有個可以通過矩陣處理影象的東西,不過說真的,以前也沒有用過幾次,不過好歹有個想法了。至於讓圖片跟隨使用者手勢放大縮小,肯定是需要支援手勢檢測了。恩,我的思路暫時就是這樣了,接下來先去了解一下手勢檢測。

手勢檢測

當使用者觸控螢幕時,會產生許多手勢,down、up、scroll、fling等。一般情況下我們通過實現OnTouchListener是可以滿足我們處理一般手勢的需求的,說實話,實現手勢放大縮小的ImageView是可以通過自己在OnTouch方法裡面處理距離,滑動什麼的去算縮放的。但是人總是要對自己好一點,如果有更簡單的實現方式為什麼不用呢?Android中提供了GestureDetector給程式設計師去判斷不同的手勢。另外也提供了** ScaleGestureDetector **來檢測縮放手勢。雖然後者很像前者的子類,但事實上並不是,後者也是一個獨立的類。下面用一個簡單的demo來演示一下這兩者的觸發。

package com.example.luo_pc.view;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,
        View.OnClickListener, ScaleGestureDetector.OnScaleGestureListener {

    //定義手勢檢測
    GestureDetector detector = null;
    //縮放檢測
    ScaleGestureDetector scDetector = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button testGet = (Button) findViewById(R.id.bt_test_ges);
        Button testScges = (Button) findViewById(R.id.bt_test_scges);
        testGet.setOnClickListener(this);
        testScges.setOnClickListener(this);
        detector = new GestureDetector(this, this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_test_ges:
                detector = new GestureDetector(this, this);
                scDetector = null;
                break;
            case R.id.bt_test_scges:
                scDetector = new ScaleGestureDetector(this, this);
                detector = null;
                break;
        }
    }

    //-------------------------implement OnGestureListener's method-----------------------//

    @Override
    public boolean onTouchEvent(MotionEvent me) {
        if (detector != null)
            return detector.onTouchEvent(me);
        else
            return scDetector.onTouchEvent(me);
    }

    //使用者按下螢幕就會觸發
    @Override
    public boolean onDown(MotionEvent arg0) {
        Toast.makeText(this, "onDown", Toast.LENGTH_SHORT).show();
        return false;
    }

    //使用者按下觸控式螢幕、快速移動後鬆開,由1個MotionEvent ACTION_DOWN,    
    //多個ACTION_MOVE, 1個ACTION_UP觸發    
    //e1:第1個ACTION_DOWN MotionEvent    
    //e2:最後一個ACTION_MOVE MotionEvent    
    //velocityX:X軸上的移動速度,畫素/秒    
    //velocityY:Y軸上的移動速度,畫素/秒    
    @Override
    public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2,float arg3) {
        Toast.makeText(this, "onFling", Toast.LENGTH_SHORT).show();
        return false;
    }

    //使用者長按觸控式螢幕,由多個MotionEvent ACTION_DOWN觸發   
    @Override
    public void onLongPress(MotionEvent arg0) {
        Toast.makeText(this, "onLongPress", Toast.LENGTH_SHORT).show();
    }

    //使用者按下觸控式螢幕,並拖動,由1個MotionEvent ACTION_DOWN, 多個ACTION_MOVE觸發   
    @Override
    public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2,
                            float arg3) {
        Toast.makeText(this, "onScroll", Toast.LENGTH_SHORT).show();
        return false;
    }

    //如果是按下的時間超過瞬間,而且在按下的時候沒有鬆開或者是拖動的,
    // 那麼onShowPress就會執行 
    @Override
    public void onShowPress(MotionEvent arg0) {
        Toast.makeText(this, "onShowPress", Toast.LENGTH_SHORT).show();
    }

    //使用者(輕觸觸控式螢幕後)鬆開,由一個1個MotionEvent ACTION_UP觸發    
    @Override
    public boolean onSingleTapUp(MotionEvent arg0) {
        Toast.makeText(this, "onSingleTapUp", Toast.LENGTH_SHORT).show();
        return true;
    }

    //-----------------------implement OnScaleGestureListener's method----------------------//

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScale", Toast.LENGTH_SHORT).show();
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScaleBegin", Toast.LENGTH_SHORT).show();
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScaleEnd", Toast.LENGTH_SHORT).show();
    }
}

複製程式碼

圖方便,我將整個MainActivity搬上來了,你可以直接複製,然後加上對應的佈局和導包就行了,接下來看一下執行現象。

GestureDetector

上面測試的是GestureDetector,接下來測試一下ScaleGestureDetector

ScaleGestureDetector

如果你想要測試更多,比如GestureDetector裡另外一個介面可以把我的程式碼複製一下改一改就好了,這了就不作過多的贅述了,程式碼會說話。

Matrix

這裡只對Matrix作簡單的介紹。Android中Matrix是一個3 x 3的矩陣(說到矩陣都是二維的,不要看到3 x 3就想到3維去了)。先看一下Matrix的getValues和setValues方法:

    /** Copy 9 values from the matrix into the array.
    */
    public void getValues(float[] values) {
        if (values.length < 9) {
            throw new ArrayIndexOutOfBoundsException();
        }
        native_getValues(native_instance, values);
    }

    /** Copy 9 values from the array into the matrix.
        Depending on the implementation of Matrix, these may be
        transformed into 16.16 integers in the Matrix, such that
        a subsequent call to getValues() will not yield exactly
        the same values.
    */
    public void setValues(float[] values) {
        if (values.length < 9) {
            throw new ArrayIndexOutOfBoundsException();
        }
        native_setValues(native_instance, values);
    }
複製程式碼

得到或者設定一個有9個元素的陣列,繼續往下看發現呼叫的是個native修飾方法,好吧,不繼續看了,瞭解以上也差不多夠了。其內部有

Matrix
Matrix的對影象的處理可分為四類基本變換: Translate           平移變換 Rotate                旋轉變換 Scale                  縮放變換 Skew                  錯切變換   從字面上理解,矩陣中的MSCALE用於處理縮放變換,MSKEW用於處理錯切變換,MTRANS用於處理平移變換,MPERSP用於處理透視變換。實際中當然不能完全按照字面上的說法去理解Matrix。

從字面上理解那9個量,什麼X軸縮放,什麼扭曲,什麼X軸偏移量,還帶不認識的,沒關係,我們現在做的操作比較簡單,不需要用到那麼多的引數。比如我們現在想設定偏移量(200,200) 我們可以

Matrix matrix = new Matrix();
martrix.postTranslate(200,200);
複製程式碼

實踐

寫完上面的東西,我已經差不多是個廢人了……

我已經差不多是個廢人了
畢竟當年線性代數學的不咋滴,加上之前雖然有用過Matrix但是並不是很多,接下來進入喜聞樂見的實戰時間。首先是不加任何限制,直接實現

package com.example.luo_pc.view.CustomView;

/**
 * Created by Luo_xiasuhuei321@163.com on 2016/9/24.
 * desc:
 */

import android.content.Context;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;

public class ZZoomImageView extends ImageView implements View.OnTouchListener, ScaleGestureDetector.OnScaleGestureListener {
    //suppress the unused warning because maybe it will be used sometime later
    @SuppressWarnings("unused")
    private static final String TAG = "ZZoomImageView";

    /**
     * 最大放大倍數
     */
//    public static final float SCALE_MAX = 4.0f;

    /**
     * 預設縮放
     */
//    private float initScale = 1.0f;

    /**
     * 手勢檢測
     */
    ScaleGestureDetector scaleGestureDetector = null;

    Matrix scaleMatrix = new Matrix();

    /**
     * 處理矩陣的9個值
     */
//    float[] martixValue = new float[9];

    public ZZoomImageView(Context context) {
        this(context, null);
    }

    public ZZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setScaleType(ScaleType.MATRIX);
        scaleGestureDetector = new ScaleGestureDetector(context, this);
        this.setOnTouchListener(this);
    }

    /**
     * 獲取當前縮放比例
     */
//    public float getScale() {
//        scaleMatrix.getValues(martixValue);
//        return martixValue[Matrix.MSCALE_X];
//    }

    //--------------------------implement OnTouchListener----------------------------//

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return scaleGestureDetector.onTouchEvent(event);
    }

    //----------------------implement OnScaleGestureListener------------------------//

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
//        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        if (getDrawable() == null)
            return true;
//        Log.e(TAG,"君甚鹹,此魚何能及君也?");
//        if (scaleFactor * scale < initScale)
//            scaleFactor = initScale / scale;
//        if (scaleFactor * scale > SCALE_MAX)
//            scaleFactor = SCALE_MAX / scale;
        //設定縮放比例
        scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2, getHeight() / 2);
        setImageMatrix(scaleMatrix);
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }
}
複製程式碼

看一下跑起來是啥樣的

GIF.gif

將圖片放到中心

嗯,我要是把這個用在專案裡,老大要是看到了估計我就沒有以後了……首先,是沒有限制,可以無限縮小放大,第二是縮放中心點,預設都是ImageView中心,最後是剛開始載入出來我的圖片有部分沒載入,而且圖片不在imageview的中心!我ImageView設定的可是倆match_parent啊。

坑爹.png
有問題沒事,我們一樣一樣,慢慢解決。首先是圖片位置,圖片位置的設定我們可以在圖片載入的時候將他放到ImageView的中心去,同樣在這個過程中,我們可以判斷圖片的大小,如果圖片大於ImageView尺寸則將其大小調整至ImageView的大小。首先我們在ImageView的構造器中可能是無法獲取到ImageView和圖片的真實尺寸的,我們可以通過ViewTreeObserver在佈局完成可以獲取真實尺寸的時候完成對圖片的調整。而OnGlobalLayoutListener是ViewTreeObserver的內部介面,當一個檢視樹的佈局發生改變時,可以被ViewTreeObserver監聽到。所以新增程式碼如下:

/**
 * 首先讓我們的類實現OnGlobalLayoutListener介面
public class ZZoomImageView extends ImageView implements View.OnTouchListener, ScaleGestureDetector.OnScaleGestureListener,
        ViewTreeObserver.OnGlobalLayoutListener
複製程式碼

然後我們在此控制元件的onAttachedToWindow中設定監聽,在onDetachedFromWindow移除這個監聽:

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    //suppress deprecate warning because i have dealt with it 
    @Override
    @SuppressWarnings("deprecation")
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }
複製程式碼

最後是最重要的,在回撥中對圖片進行處理:

    @Override
    public void onGlobalLayout() {
        if (!once)
            return;
        Drawable d = getDrawable();
        if (d == null)
            return;
        //獲取imageview寬高
        int width = getWidth();
        int height = getHeight();

        //獲取圖片寬高
        int imgWidth = d.getIntrinsicWidth();
        int imgHeight = d.getIntrinsicHeight();

        float scale = 1.0f;

        //如果圖片的寬或高大於螢幕,縮放至螢幕的寬或者高
        if (imgWidth > width && imgHeight <= height)
            scale = (float) width / imgWidth;
        if (imgHeight > height && imgWidth <= width)
            scale = (float) height / imgHeight;
        //如果圖片寬高都大於螢幕,按比例縮小
        if (imgWidth > width && imgHeight > height)
            scale = Math.min((float) imgWidth / width, (float) imgHeight / height);
        Log.e(TAG, "scale" + scale);
        //將圖片移動至螢幕中心
        scaleMatrix.postTranslate((width - imgWidth) / 2, (height - imgHeight) / 2);
        scaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
        setImageMatrix(scaleMatrix);
        once = false;
    }
複製程式碼

對圖片的處理核心思想就是判斷圖片尺寸和當前控制元件尺寸,圖片尺寸比控制元件大,就對圖片進行縮放處理,並且最後將圖片移動至控制元件中心處。程式碼上的註釋寫的都很詳細了,各位看官可以自行閱讀。現在來看看變成啥樣了

ZZ-改

限制縮放

很好圖是到中間去了,那現在的問題就是無限縮小和放大的問題。這個問題解決思路是很簡單的,做個限制就行了。

嗯,新增如下幾個變數:

    /**
     * 最大放大倍數
     */
    public static final float SCALE_MAX = 4.0f;

    /**
     * 預設縮放
     */
    private float initScale = 1.0f;

    /**
     * 處理矩陣的9個值
     */
    float[] martixValue = new float[9];
複製程式碼

上面費了那麼多口水講到的matrix的九個值啥的,終於要出現了,是不是很激動~(才怪),接下來搞個方法獲取縮放比例

    /**
     * 獲取當前縮放比例
     */
    public float getScale() {
        scaleMatrix.getValues(martixValue);
        return martixValue[Matrix.MSCALE_X];
    }
複製程式碼

之後為了獲取正確的初始縮放比例,在我們剛剛寫的** onGlobalLayout **中加句話:

initScale = scale;
複製程式碼

當然了,得是在獲取了scale值之後再添,因為我們雖然設定了初始縮放比例,但是實際中可能因為圖片大小發生了縮放行為,所以我們需要再次確定初始縮放比例。接下來就是對縮放行為進行限制了,修改onScale程式碼如下:

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        Log.e(TAG, "matrix scale---->" + scale);
        float scaleFactor = detector.getScaleFactor();
        Log.e(TAG, "scaleFactor---->" + scaleFactor);
        if (getDrawable() == null)
            return true;
//        Log.e(TAG,"君甚鹹,此魚何能及君也?");
        if ((scale < SCALE_MAX && scaleFactor > 1.0f)
                || (scale > initScale && scaleFactor < 1.0f)) {
            if (scaleFactor * scale < initScale)
                scaleFactor = initScale / scale;
            if (scaleFactor * scale > SCALE_MAX)
                scaleFactor = SCALE_MAX / scale;
            Log.e(TAG, "scaleFactor2---->" + scaleFactor);
            //設定縮放比例
            scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2, getHeight() / 2);
            setImageMatrix(scaleMatrix);
        }
        return true;
    }
複製程式碼

對於以上的程式碼,你可能會對兩個scale有所疑惑,一個scale是從matrix中獲得的,一個是從縮放檢測中獲得的。開始我看到hongyang大神的這段程式碼我也是有所疑惑的,但是之後我自己寫了一遍,打了一下log,發現前一個在到達我們設定的最大值時,值便會固定為4,後一個值會在1左右。那麼很明顯前一個值是圖片相對於初始尺寸的縮放,後一個是每一次縮放的實際比例。理解了這個之後便容易解決了,使用如上程式碼便可以限制縮放了。如果你對於縮放比例不滿意,嗯,自己設定就是了,反正也不復雜。效果圖就等下一個功能一起實現再放了。

以上一個簡單,還算能用的縮放ImageView就完成了,現在的問題是縮放中心是控制元件的中心,如果我想設定縮放中心是我按下去的地方呢?很簡單改一句程式碼:

scaleMatrix.postScale(scaleFactor, scaleFactor, 
            getWidth() / 2, getHeight() / 2);

scaleMatrix.postScale(scaleFactor, scaleFactor, 
            detector.getFocusX(), detector.getFocusY());
複製程式碼

但是這一改出事了……現在是能根據手勢縮放中心進行縮放了,但是縮放到最小時圖片位置可能發生了變化……現在還要解決的就是縮放時圖片位置變化,新增如下方法:

    /**
     * 在縮放時,控制範圍
     */
    private void checkBorderAndCenterWhenScale() {
        Matrix matrix = scaleMatrix;
        RectF rectF = new RectF();
        Drawable d = getDrawable();
        if (d != null) {
            rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rectF);
        }

        float deltaX = 0;
        float deltaY = 0;
        int width = getWidth();
        int height = getHeight();
        // 如果寬或高大於螢幕,則控制範圍
        if (rectF.width() >= width) {
            if (rectF.left > 0) {
                deltaX = -rectF.left;
            }
            if (rectF.right < width) {
                deltaX = width - rectF.right;
            }
        }
        if (rectF.height() >= height) {
            if (rectF.top > 0) {
                deltaY = -rectF.top;
            }
            if (rectF.bottom < height) {
                deltaY = height - rectF.bottom;
            }
        }
        // 如果寬或高小於螢幕,則讓其居中
        if (rectF.width() < width) {
            deltaX = width * 0.5f - rectF.right + 0.5f * rectF.width();
        }
        if (rectF.height() < height) {
            deltaY = height * 0.5f - rectF.bottom + 0.5f * rectF.height();
        }
        scaleMatrix.postTranslate(deltaX, deltaY);
    }
複製程式碼

然後在onScale方法裡呼叫以上檢測的方法:

        if ((scale < SCALE_MAX && scaleFactor > 1.0f)
                || (scale > initScale && scaleFactor < 1.0f)) {
            if (scaleFactor * scale < initScale)
                scaleFactor = initScale / scale;
            if (scaleFactor * scale > SCALE_MAX)
                scaleFactor = SCALE_MAX / scale;
            Log.e(TAG, "scaleFactor2---->" + scaleFactor);
            //設定縮放比例
            scaleMatrix.postScale(scaleFactor, scaleFactor,
                    detector.getFocusX(), detector.getFocusY());
            checkBorderAndCenterWhenScale();
            setImageMatrix(scaleMatrix);
        }
複製程式碼

最終成型

以上程式碼算出初步的能用了,不過還有一點值得注意的地方,如果你在onTouch這個方法裡的程式碼是這樣的:

return scaleGestureDetector.onTouchEvent(event);
複製程式碼

那麼所有的事件都會被消費,因為我點到scaleGestureDetector的onTouch方法裡,沒看到return false的東西,所以你設定的oncilck事件之類的都沒什麼卵用。

對於我來說這樣是不行的,因為我希望使用者點選一次之後可以退出當前介面,所以你可以呼叫sacleGestureDetector.onTouchEvent(event)但是返回false,不消耗這個事件,讓onClick來處理點選事件。當我想的很美的時候,卻發現這麼做雖然點選事件會被處理,而且縮放也正常,但是縮放的操作會被判斷為點選事件,也就是說這麼幹不行了。我的腦海中第二個想到的解決方案是回撥,既然系統的回撥不行了,那我自己設定一個時間,在這個時間之內就是click事件,我在這個事件的回撥裡把當前介面退出了不就行了。實現如下:

    private ClickCloseListener c;

    public interface ClickCloseListener {
        void close();
    }

    public void setClickCloseListener(ClickCloseListener c) {
        this.c = c;
    }

    /**
     * 按下的時間
     */
    long downTime;

    /**
     * down 和 up之間的間隔
     */
    long closeTime = 100L;

    /**
     * 設定按下的時間
     */
    //suppress unused warning for no reason
    @SuppressWarnings("unused")
    public void setClickTime(long time) {
        this.closeTime = time;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        scaleGestureDetector.onTouchEvent(event);
        //如果監聽為null,消費該事件,不讓onclick生效
        if (c == null)
            return true;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTime = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_UP:
                downTime = System.currentTimeMillis() - downTime;
                if (downTime < closeTime)
                    c.close();
                break;
            default:
                break;
        }
        return true;
    }
複製程式碼

最後看一下效果圖吧~

ZZ-最終打死都不改版

當然了,自己搞的點選事件有點不靠譜,時間間隔設定為100ms,有點短了,你可以自己設定,不過這篇文到這裡也就結束了。本來還想連什麼移動一起加上,嗯,現在發現好像篇幅超出了我的控制。暫且還是算了吧~而且這個姑且也算是能用了,只不過適用的場景只是檢視大圖的一個單獨的介面。這個簡單的小東西就寫到這了。完整程式碼就放在這把,也懶得上傳github了:

package com.example.luo_pc.view.CustomView;

/**
 * Created by Luo_xiasuhuei321@163.com on 2016/9/24.
 * desc:
 */

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

public class ZZoomImageView extends ImageView implements View.OnTouchListener, ScaleGestureDetector.OnScaleGestureListener,
        ViewTreeObserver.OnGlobalLayoutListener {
    //suppress the unused warning because maybe it will be used sometime later
    @SuppressWarnings("unused")
    private static final String TAG = "ZZoomImageView";

    /**
     * 最大放大倍數
     */
    public static final float SCALE_MAX = 4.0f;

    /**
     * 預設縮放
     */
    private float initScale = 1.0f;

    /**
     * 手勢檢測
     */
    ScaleGestureDetector scaleGestureDetector = null;

    Matrix scaleMatrix = new Matrix();

    /**
     * 處理矩陣的9個值
     */
    float[] martixValue = new float[9];

    public ZZoomImageView(Context context) {
        this(context, null);
    }

    public ZZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setScaleType(ScaleType.MATRIX);
        scaleGestureDetector = new ScaleGestureDetector(context, this);
        this.setOnTouchListener(this);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    //suppress deprecate warning because i have dealt with it 
    @Override
    @SuppressWarnings("deprecation")
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    /**
     * 獲取當前縮放比例
     */
    public float getScale() {
        scaleMatrix.getValues(martixValue);
        return martixValue[Matrix.MSCALE_X];
    }

    /**
     * 在縮放時,控制範圍
     */
    private void checkBorderAndCenterWhenScale() {
        Matrix matrix = scaleMatrix;
        RectF rectF = new RectF();
        Drawable d = getDrawable();
        if (d != null) {
            rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rectF);
        }

        float deltaX = 0;
        float deltaY = 0;
        int width = getWidth();
        int height = getHeight();
        // 如果寬或高大於螢幕,則控制範圍
        if (rectF.width() >= width) {
            if (rectF.left > 0) {
                deltaX = -rectF.left;
            }
            if (rectF.right < width) {
                deltaX = width - rectF.right;
            }
        }
        if (rectF.height() >= height) {
            if (rectF.top > 0) {
                deltaY = -rectF.top;
            }
            if (rectF.bottom < height) {
                deltaY = height - rectF.bottom;
            }
        }
        // 如果寬或高小於螢幕,則讓其居中
        if (rectF.width() < width) {
            deltaX = width * 0.5f - rectF.right + 0.5f * rectF.width();
        }
        if (rectF.height() < height) {
            deltaY = height * 0.5f - rectF.bottom + 0.5f * rectF.height();
        }
        scaleMatrix.postTranslate(deltaX, deltaY);
    }

    //--------------------------implement OnTouchListener----------------------------//
    private ClickCloseListener c;

    public interface ClickCloseListener {
        void close();
    }

    public void setClickCloseListener(ClickCloseListener c) {
        this.c = c;
    }

    /**
     * 按下的時間
     */
    long downTime;

    /**
     * down 和 up之間的間隔
     */
    long closeTime = 100L;

    /**
     * 設定按下的時間
     */
    //suppress unused warning for no reason
    @SuppressWarnings("unused")
    public void setClickTime(long time) {
        this.closeTime = time;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        scaleGestureDetector.onTouchEvent(event);
        //如果監聽為null,消費該事件,不讓onclick生效
        if (c == null)
            return true;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTime = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_UP:
                downTime = System.currentTimeMillis() - downTime;
                if (downTime < closeTime)
                    c.close();
                break;
            default:
                break;
        }
        return true;
    }

    //----------------------implement OnScaleGestureListener------------------------//

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        if (getDrawable() == null)
            return true;
//        Log.e(TAG,"君甚鹹,此魚何能及君也?");
        if ((scale < SCALE_MAX && scaleFactor > 1.0f)
                || (scale > initScale && scaleFactor < 1.0f)) {
            if (scaleFactor * scale < initScale)
                scaleFactor = initScale / scale;
            if (scaleFactor * scale > SCALE_MAX)
                scaleFactor = SCALE_MAX / scale;
            Log.e(TAG, "scaleFactor2---->" + scaleFactor);
            //設定縮放比例
            scaleMatrix.postScale(scaleFactor, scaleFactor,
                    detector.getFocusX(), detector.getFocusY());
            checkBorderAndCenterWhenScale();
            setImageMatrix(scaleMatrix);
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

    boolean once = true;

    @Override
    public void onGlobalLayout() {
        if (!once)
            return;
        Drawable d = getDrawable();
        if (d == null)
            return;
        //獲取imageview寬高
        int width = getWidth();
        int height = getHeight();

        //獲取圖片寬高
        int imgWidth = d.getIntrinsicWidth();
        int imgHeight = d.getIntrinsicHeight();

        float scale = 1.0f;

        //如果圖片的寬或高大於螢幕,縮放至螢幕的寬或者高
        if (imgWidth > width && imgHeight <= height)
            scale = (float) width / imgWidth;
        if (imgHeight > height && imgWidth <= width)
            scale = (float) height / imgHeight;
        //如果圖片寬高都大於螢幕,按比例縮小
        if (imgWidth > width && imgHeight > height)
            scale = Math.min((float) imgWidth / width, (float) imgHeight / height);
        initScale = scale;
        //將圖片移動至螢幕中心
        scaleMatrix.postTranslate((width - imgWidth) / 2, (height - imgHeight) / 2);
        scaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
        setImageMatrix(scaleMatrix);
        once = false;
    }
}
複製程式碼

參考資料:

相關文章