Android 多點觸控與圖片縮放

上一章,我們學習了手勢 GestureDecetor 的基本使用 Android 手勢學習 GestureDetector,這一次,我們來學習使用 ScaleGestureDetector 來實現一個圖片縮放的效果,如下:

文章參考鴻洋大大的圖片縮放

scale.gif
代碼 ScaleImageview

使用

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

然後在你的 module 中添加:

 implementation 'com.github.LillteZheng:ScaleImageview:1.0'

然後添加控件即可:

<com.zhengsr.mylibrary.ScaleImageView
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_margin="10dp"
    app:scale_limit_board="false"
    app:scale_max_factor="5"
    app:scale_auto_time="5"
    app:scale_double_factor="2"
    android:src="@mipmap/a1"/>

一、分析:

  1. 通過 ScaleGestureDetector 拿到縮放中心和縮放因子
  2. 通過 matrix 進行圖片的縮放和平移
  3. 雙擊進行放大和縮小
  4. 結合 GestureDetector 進行平移
  5. 使用 getParent().requestDisallowInterceptTouchEvent(true); 進行事件發發截取

二、講解

2.1 平移與縮放

首先,當加載一張圖片的時候,並不清楚圖片的大小,如果想要它居中且全部顯示出來,則需要通過平移和縮放的形式,這裏可以通過 Imageview 的 scaleTyle 設置成 matrix 的形式;比如,在初始化中:

setScaleType(ScaleType.MATRIX);

這樣,則保證了圖片的縮放模式是以 matrix 的。假設需要加載的圖片比較小,在左上角,這時,需要移動到屏幕中心,如下圖:
在這裏插入圖片描述
此時,需要將它先移動到屏幕中心,只需要屏幕的中心點減去圖片的中心點座標即可,dw 和dh 爲圖片的寬高:

mMatrix = new Matrix();
float dx = width * 1.0f / 2 - dw * 1.0f / 2;
float dy = height * 1.0f / 2 - dh * 1.0f / 2;
mMatrix.postTranslate(dx,dy);

接着進行比例縮放,目前來說,有兩種方式,假如整個屏幕只填放一張圖片,那麼,縮放只需要保證寬或者高一個比例縮放即可,只要保證了其中一個,以適配最小寬高來,那麼另一個寬或者高也能適配,如下:

//控件寬高都比圖片大,即放大了多少倍,拿到寬高的最小放大比例
 if (width > dw && height > dh){
     scale = Math.min((width * 1.0f / dw),(height * 1.0f / dh));
 }
 //圖片寬高比控件大,即縮小了多少倍,,拿到寬高的最大放小比例
 if (dw > width && dh > height){
     scale = Math.max((width * 1.0f / dw),(height * 1.0f / dh));
 }
 //圖片高比控件的高大,即應該縮放多少
 if (dw < width && dh > height){
     scale =  height * 1.0f / dh;
 }
 //圖片寬比控件的寬大,即應該縮放多少
 if (dw > width && dh < height){
     scale = width * 1.0f / dw;
 }

這樣拿到縮放比例後,直接用 matrix 即可:

mMatrix.postScale(scale,scale,width * 1.0f / 2, height * 1.0f / 2);

但如果你想效果圖中,指定了寬高呢?這樣如果只縮放其中一個寬或高,則另一個,就有可能存在不能完全適配的問題,所以,需要拿到寬和高的縮放比例,如:

scale = width * 1.0f / dw;
scaley = height * 1.0f / dh;

然後調用 matrix:

mMatrix.postScale(scale, scaley, width * 1.0f / 2, height * 1.0f / 2);

接着調用 imageview 自帶的方法,即可完成移動中心點和縮放問題:

setImageMatrix(mMatrix);

可以使用

 app:scale_autofit="true"

來使用哪種比例,一個隨便指定高度爲200dp 的張方形圖片不同效果如下:

scale_autofit=true,這樣是寬高等比例縮放
scale_autofit=true,只以其中款或者高作爲縮放
所以,如果你是指定高度,且圖片是長方形的,建議 scale_autofit true,如果是 match_parent 且正方形的則 false 即可。

2.2 使用 ScaleGestureDetector 進行縮放

在進行平移縮放之後,可以使用 ScaleGestureDetector 進行縮放即可,它的三個方法如下:

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //拿到縮放比例
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        //拿到縮放中心
        float focusX = detector.getFocusX();
        float focusY = detector.getFocusY();
        return true;
    }


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

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

需要注意的是 onScaleBegin return true,這樣事件才能繼續傳遞下去。接着在 onScale 拿到縮放比例中心和當前的縮放比例因子;整體代碼如下:

 //拿到縮放比例
 float scale = getScale();
 float scaleFactor = detector.getScaleFactor();
 //拿到縮放中心
 float focusX = detector.getFocusX();
 float focusY = detector.getFocusY();
 //默認不能縮放至屏幕控件大小
 if (mIsLimitBroad) {
     //重新複製,讓它逼近於最小值
     if (scale * scaleFactor < mDeflautScale) {
         scaleFactor = mDeflautScale / scale;
     }
     if (scale * scaleFactor > mMaxScale) {
         scaleFactor = mMaxScale / scale;
     }
 }
 mMatrix.postScale(scaleFactor,scaleFactor,focusX,focusY);
 checkBroad();
 setImageMatrix(mMatrix);

mIsLimitBroad 即 app:scale_limit_board 表示是否縮放能否能小於控件,如果爲 true,則表示不能縮小到控件大小,可以通過比例換算去是實現,如果 scale * scaleFactor < mDeflautScale ,則讓 scaleFactor = mDeflautScale / scale 即可。
其中 getScale()方法表示拿到當前的縮放比例:

 /**
  * 拿到縮放比例,默認拿到 x 即可
  * @return
  */
 private float getScale(){
     float[] floats = new float[9];
     mMatrix.getValues(floats);
     return floats[Matrix.MSCALE_X];
 }

checkBroad() 則是,在縮放時,如果不對中心點進行校正,不對圖片進行平移校正,則會出現縮小時,圖片跑到屏幕外側或者中心點不對的問題,如下圖這樣:
在這裏插入圖片描述
所以,需要修正一下:

/**
     * 檢查邊界,讓它不能有空白和讓它一直保在屏幕中心
     */
    private void checkBroad() {
        RectF rectF = getMatrixRectF();
        float dx = 0;
        float dy = 0;
        //檢測邊界,不能讓它留空白
        if (rectF.width() >= getWidth()) {
            if (rectF.right < getWidth()) {
                dx = getWidth() - rectF.right;
            }
            if (rectF.left > 0) {
                dx = -rectF.left;
            }
        }else {
            //保證在屏幕中心
            dx = getWidth() * 1.0f /2 - rectF.right + rectF.width() * 1.0f / 2;
        }
        //檢測邊界,不能讓它留空白
        if (rectF.height() >= getHeight()){
            if (rectF.top > 0){
                dy = -rectF.top;
            }
            if (rectF.bottom < getHeight()){
                dy = getHeight() - rectF.bottom;
            }
        }else{
            //保證在屏幕中心
            dy = getHeight() * 1.0f /2 - rectF.bottom + rectF.height() * 1.0f / 2;
        }

        mMatrix.postTranslate(dx,dy);

    }

getMatrixRectF() 爲拿到當前縮放後的圖片大小:

    /**
     * 拿到縮放後的圖片大小
     * @return
     */
    private RectF getMatrixRectF(){
        Matrix matrix = new Matrix(mMatrix);
        RectF rectF = new RectF();
        Drawable d = getDrawable();
        if (d != null){
            rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
            matrix.mapRect(rectF);
        }
        return rectF;
    }

2.3 移動放大的圖片

圖片移動,在 onTouchEvent 中進行處理,需要考慮的是,當用兩個手指拖動,然後其中一個手指擡起,此時中心點是有變動的,這時需要改變中心點的座標,不然會出現一下子跳動的現象,具體代碼如下:

 //防止多指變動,中心點改變,而出現拖動問題
        int pointerCount = event.getPointerCount();
        float x = 0;
        float y = 0;
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x = x / pointerCount;
        y = y / pointerCount;

        if (mLastPointCount != pointerCount){
            mLastx = x;
            mLasty = y;
        }
        //記錄上一次的手指個數
        mLastPointCount = pointerCount;

        switch (event.getActionMasked()){
    
            case MotionEvent.ACTION_MOVE:
                requestFocusViewpager();
                float dx = x - mLastx;
                float dy = y - mLasty;
                mMatrix.postTranslate(dx,dy);
                checkBroad();
                setImageMatrix(mMatrix);
                mLastx = x;
                mLasty = y;
                break;
            case MotionEvent.ACTION_UP:
                mLastPointCount = 0;
                break;

            default:break;
        }

三、雙擊放大

雙擊的方法,我們也可以通過 GestureDetector 手勢這個類拿到雙擊的方法:

    /**
     * 指監聽 雙擊的方法
     */
    class SimpleGesture extends GestureDetector.SimpleOnGestureListener{

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            float x = e.getX();
            float y = e.getY();
            if (isCanDoubleScale) {
                //放大
                if (getScale() < mDoubleScale) {
                    post(new AutoScale(mDoubleScale, x, y));
                } else { //縮放到自身大小
                    post(new AutoScale(mDeflautScale, x, y));
                }
            }
            return true;

        }
    }

而AutoScale就是表示 縮放的類:

    /**
     * 雙擊縮放比例
     */
    class AutoScale implements Runnable {
        private final float LARGE = 1.07f;
        private final float SMALL = 0.93f;
        float targetScale;
        float x;
        float y;
        float tempScale;
        public AutoScale(float targetScale,float x, float y) {
            this.targetScale = targetScale;

            this.x = x;
            this.y = y;
            isCanDoubleScale = false;
            //放大
            if (targetScale > getScale()){
                tempScale = LARGE;
            }else{
                tempScale = SMALL;
            }
        }

        @Override
        public void run() {

            mMatrix.postScale(tempScale, tempScale , x, y);
            checkBroad();
            setImageMatrix(mMatrix);
            float currentScale = getScale();
            boolean isCanLarge = currentScale < targetScale && tempScale > 1.0f;
            boolean isCanSmall = currentScale > targetScale && tempScale < 1.0f;
            if (isCanLarge || isCanSmall) {
                //若還未到達縮放的目標值,則繼續縮放
                postDelayed(this, mDoubleAutoTime);
            }else{
                //達到表示了,但可能有偏差,還需要調整一下
                mMatrix.postScale(targetScale/currentScale, targetScale/currentScale , x, y);
                checkBroad();
                setImageMatrix(mMatrix);
                isCanDoubleScale = true;
            }


        }
    }

這樣,關鍵知識點就講解完了,自定義屬性如下:

縮放控件 ScaleImageView

名稱 類型 說明
scale_auto_time reference,integer 雙擊時,達到放大的時間
scale_limit_board boolean 是否限制邊界,即不能縮放到比控件小
scale_autofit boolean 自動適配縮放值,有些圖片是正方形,如果你的高度沒設定好,建議設置爲false,不能會變形
scale_double_factor integer 雙擊時放大倍數
scale_max_factor integer 可放大的最大倍數
scale_interrupt_parent_touch boolean 是否截獲父控件觸摸事件,放大時,需要截取,不然無法移動
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章