圖片操作系列 —(1)手勢縮放圖片功能

青蛙要fly發表於2017-11-07

概述

專案開發中,大家APP開發一般都會用到上傳圖片,比如是上傳了自己的生活照,然後在某個介面處檢視上傳的圖片,這時候一般在這個檢視詳情的介面,會有手勢放大縮小功能,手勢進行旋轉功能,雙擊放大圖片等等。

不巧,我以前也有需要這個需求的時候,而且特別指出了要用手勢進行圖片的選擇功能。

於是我檢視了BiliBili的開源庫:

Boxing

使用了這個Demo後發現裡面有手勢控制圖片大小,手勢控制圖片旋轉等功能,看了程式碼後我發現BiliBili這個demo中也是用了第三方的庫:

RotatePhotoView


我們可以看到介紹:在PhotoView的基礎上新增了通過二個手指來旋轉圖片的功能,所以這個庫又是用了其他的第三方庫:

PhotoView

我們可以看到這個PhotoView的庫有一萬多個star了。說明還是很不錯的。

所以通過這次。我就來看PhotoView如何進行實現那麼多功能。


正題

大家在看正文之前如果對於Matrix不是很瞭解的,可以先看看:
android matrix 最全方法詳解與進階(完整篇)
Android Matrix
Float中的那些常量 Infinity、NaN

本來是想直接拿著PhotoView 的原始碼,貼上原始碼分析一個個具體的功能,但是因為原始碼是考慮到很多功能,所以有很多程式碼量,而且太多看著很亂,所以我的方案是直接自己寫個demo,然後根據我們要講解的功能,仿照PhotoView的原始碼,在自己一個個具體的功能demo分別實現。所以本文我先來實現實現根據手勢來實現圖片的縮放功能:

1.新增圖片佈局

PhotoView是繼承了ImageView,然後直接在layout中使用PhotoView,為了更方便的講解,我就直接還是使用ImageView,然後讓大家看到是如何對ImageView做處理實現相應的功能。

先新增我們要的demo佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.dialog.photoviewdemo.MainActivity">

    <ImageView
        android:id="@+id/photo_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black"
        />

</LinearLayout>複製程式碼

2. 對圖片設定手勢監聽

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //對我們的ImageView設定相應的一張圖片
    ivPhoto = (ImageView) findViewById(R.id.photo_view);
    drawable = ContextCompat.getDrawable(this, R.mipmap.ic_launcher);
    ivPhoto.setImageDrawable(drawable);

    //對我們的ImageView設定觸控事件監聽,並且把監聽交給了GestureDetector.
    ivPhoto.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
           return scaleGestureDetector.onTouchEvent(event);
        }
    }); 

    //GestureDetector的例項生成
    scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scaleFactor = detector.getScaleFactor();
            float focusX = detector.getFocusX();
            float focusY = detector.getFocusY();
            if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
                return false;
            }

            mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
            if(checkMatrixBounds()) {
                ivPhoto.setImageMatrix(getDrawMatrix());
            }

            return true;
        }

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

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
        }
    });
}複製程式碼

根據上面的程式碼我們一樣樣來分析:

1.GestureDetector和ScaleGestureDetector

當使用者觸控螢幕的時候,會產生許多手勢,例如down,up,scroll,filing等等。
一般情況下,我們知道View類有個View.OnTouchListener內部介面,通過重寫他的onTouch(View v, MotionEvent event)方法,我們可以處理一些touch事件,但是這個方法太過簡單,如果需要處理一些複雜的手勢,用這個介面就會很麻煩(因為我們要自己根據使用者觸控的軌跡去判斷是什麼手勢)。
Android sdk給我們提供了GestureDetector(Gesture:手勢Detector:識別)類,通過這個類我們可以識別很多的手勢,主要是通過他的onTouchEvent(event)方法完成了不同手勢的識別。雖然他能識別手勢,但是不同的手勢要怎麼處理,應該是提供給程式設計師實現的。
具體具體可以看這篇文章,寫的很詳細:使用者手勢檢測-GestureDetector使用詳解

而此處我們因為做的功能是通過手勢來縮放圖片,所以我們就要監聽二個手指頭縮放動作,所以我們使用的是ScaleGestureDetector

ScaleGestureDetector介紹:
用於處理縮放的工具類,用法與GestureDetector類似,都是通過onTouchEvent()關聯相應的MotionEvent的。使用該類時,使用者需要傳入一個完整的連續不斷地motion事件(包含ACTION_DOWN,ACTION_MOVE和ACTION_UP事件)。

我們看上面的程式碼就會發現ScaleGestureDetector有三個方法:

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

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

@Override
public void onScaleEnd(ScaleGestureDetector detector) {}複製程式碼

onScaleBegin:縮放開始會執行的方法,但是我們發現這個方法需要返回一個Boolean值,這個值決定是否處理後繼的縮放事件,返回false時,不會執行onScale()

onScaleEnd:縮放結束執行

onScale:縮放時候執行的方法,用來做具體的邏輯處理。

我們具體來看看onScale方法:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    return true;
}複製程式碼

我們可以看到這裡是返回Boolean值,那這裡返回true和false有什麼區別呢。

float scaleFactor = detector.getScaleFactor();複製程式碼

我們可以通過這個方法獲取到縮放因子,縮放因子會根據你的手勢的變大會越來越大,如果你返回了true,那就說明這次的縮放行為就已經結束了,如果你返回了false,那就說明沒有結束,然後縮放因子越來越大。

public boolean onScale(ScaleGestureDetector detector) {  
    if(detector.getScaleFactor()< 2){  
        return false;  
    }  
    return true;  
}複製程式碼


我們可以看到,我們設定了大於2才返回true,(前提二個手指是做放大手勢)那麼縮放因子就會一直變大到2,才會認為這次縮放行為結束了,就再次從1開始了。

(PS:如果二個手指做縮小的手勢,那麼這個縮放因子就會小於1,如果返回false,那麼就會從1開始越來越小。)

2.圖片初始化呈現狀態

假設我們現在的ImageView設定的是全屏,我們有個小圖片,ImageView設定了圖片後是這樣的:

我們發現預設是在左上角,而且因為我們的ImageView設定的是全屏,而圖片又特別小,這樣的初步呈現方式很不友好。
所以我們要做如下操作:
<1>把圖片居中顯示。
<2>圖片和ImageView相適應(我們這裡是把圖片適當的放大,來適應這麼大的ImageView.)

所以也就是我們上面提到過的程式碼:

drawableWidth = drawable.getIntrinsicWidth();
drawableHeight = drawable.getIntrinsicHeight();

viewWidth = ivPhoto.getWidth() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();
RectF mTempScr = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
mBaseMatrix.setRectToRect(mTempScr, mTempDst, Matrix.ScaleToFit.CENTER);
mDrawableMatrix.set(mBaseMatrix);
ivPhoto.setImageMatrix(mDrawableMatrix);複製程式碼

獲取圖片的真實寬高和ImageView用來顯示圖片的寬高我就不多說了。重點是setRectToRect方法:

public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf)複製程式碼

將rect變換成rect,通過stf引數來控制。

ScaleToFit 有如下四個值:
FILL: 可能會變換矩形的長寬比,保證變換和目標矩陣長寬一致。
START:保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。左上對齊。
CENTER: 保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。
END:保持座標變換前矩形的長寬比,並最大限度的填充變換後的矩形。至少有一邊和目標矩形重疊。右下對齊。

這裡使用谷歌的api demo的圖片作為例子:

我們很明顯發現,那個藍色的小球的變化不就是我們想要的變化麼,並且我們是要居中,所以用的是Matrix.ScaleToFit.CENTER

我們看下我們最終的效果:

3.圖片實時手勢縮放

我們前面已經知道了。手勢變化的時候會觸發onScale方法,所以我們只要把圖片的具體的放大縮小的邏輯放在onScale裡面即可。

@Override
public boolean onScale(ScaleGestureDetector detector) {
    //縮放因子
    float scaleFactor = detector.getScaleFactor();
    //返回組成該手勢的兩個觸點的中點在元件上的x和y軸座標,單位為畫素。
    float focusX = detector.getFocusX();
    float focusY = detector.getFocusY();
    //如果為nan或者無強大,則無效
    if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
        return false;
    }
    //進行縮放,傳入x軸縮放比例,y軸縮放比例,縮放中心點的x和y值
    mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
    if(checkMatrixBounds()) {
        ivPhoto.setImageMatrix(getDrawMatrix());
    }

    return true;
}複製程式碼

大家應該看到了我這邊有個checkMatrixBounds方法,本來其實單純的縮放就是先postScale然後在直接setImageMatrix就可以了。

mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
ivPhoto.setImageMatrix(getDrawMatrix());複製程式碼

但是這樣有什麼不好的地方呢。我來具體跟大家說下:

  • 縮放跟手勢的二個觸點的中心有關,而且圖片會隨著那個方向移動

比如我是二個紅點分別是我的手指,然後不停的縮小圖片動作,圖片不僅變小,而且會隨著那個方向做平移。放大則相反。這不是我們想要的,我們想要的是同樣是做縮放,同時,圖片還在中間。

既然我們知道了圖片在做縮小放大的同時還在平移,那我們就做相應的反方向的平移處理不就好了

我們分為二種情況:

1— 圖片在縮放過程中,寬或者高沒有超過ImageView的寬或者高:

如果圖片再縮放過程中沒超過ImageView的大小。我們只需要讓圖片一直居中現實即可。所以比較簡單:

只要算出我們在前面第二個大步裡面的初始化後的圖片的初始狀態後(即和ImageView相適應並且居中),相應的圖片的矩陣的寬和高是不是超過ImageView。如果沒有超過,我們可以看到我們希望的圖片放大和縮小都是希望在正中間的位置,但是現在變成了綠色的地方,我們只需要把綠色的地方移動到咖啡色的地方就行。

以Y軸為例(X軸同樣處理):


看到距離是(實際圖片的Top值) - (2分之一的ImageView的高度) + (2分之一的實際圖片高度),因為是往上移動,所以Y軸實際上是要減少值的,所以最終我們只要讓實際的圖片減去相應的距離值即可。

  • 實際圖片的TOP值(先獲取相應的實際圖片的矩陣Rect,在獲取top屬性):

    private RectF getDisplayRect(Matrix matrix) {
      Drawable d = drawable;
      if (d != null) {
          mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
          matrix.mapRect(mDisplayRect);
          return mDisplayRect;
      }
      return null;
    }複製程式碼
  • ImageView的高度:

    viewHeight = ivPhoto.getHeight() - ivPhoto.getPaddingLeft() - ivPhoto.getPaddingRight();複製程式碼
  • 實際變化後的圖片的高度(rect為上面獲取的實際圖片的Rect):
    final float height = rect.height(), width = rect.width();

所以我們這裡只需要:

private boolean checkMatrixBounds() {
    RectF rect = getDisplayRect(getDrawMatrix());
    if (rect == null) {
        return false;
    }

    final float height = rect.height(), width = rect.width();
    float deltaX = 0, deltaY = 0;

    if (height <= viewHeight) {
        deltaY = (viewHeight - height) / 2 - rect.top;
    }

    if (width <= viewWidth) {
        deltaX = (viewWidth - width) / 2 - rect.left;
    }

     mSuppMatrix.postTranslate(deltaX, deltaY);
    return true;
}複製程式碼

2— 圖片在縮放過程中,寬或者高超過ImageView的寬或者高:

這個時候我們就不行簡單的在中心位置就可以了。因為這時候不能反而不讓他在中心位置,為什麼????我們現在的圖片是一個安卓機器人,比如我現在要放大它的圖片檢視它的右眼,我們在右上角用手機不挺放大。變成這樣:

這時候就說了。那我什麼都不處理,放大這邊就是這個效果啊。說的沒錯的確這樣,但是比如現在已經放大成這個樣子了。我縮小它,但是我不是從右上角來進行縮小,而是在左邊進行縮小,大家知道我們不做處理,這時候縮小的時候是按我們手勢的位置進行,所以頭像在縮小時候先是往左邊方向,然後當小於ImageView的高度時候,又突然居中,效果很不好。

所以我們這個例子裡面處理方式是:如果寬度都大於ImageView並且圖片的右邊界還沒出現在ImageView中的時候,先按照自己原來的方式縮小,當圖片的右邊界出現在了ImageView的範圍內了,讓它慢慢往右邊移動(也就是ImageView的寬度 - Rect.right的距離),這時候就會很和諧。最後寬度小於ImageView的時候居於中間。

PS:還有一種正好反過來。我們放大的圖片是左眼!!(這時候移動的距離是 -rect.left)

所以最終變成這樣:

private boolean checkMatrixBounds() {
    RectF rect = getDisplayRect(getDrawMatrix());
    if (rect == null) {
        return false;
    }

    final float height = rect.height(), width = rect.width();
    float deltaX = 0, deltaY = 0;

    if (height <= viewHeight) {
        deltaY = (viewHeight - height) / 2 - rect.top;
    } else if (rect.top > 0) {
        deltaY = -rect.top;
    } else if (rect.bottom < viewHeight) {
        deltaY = viewHeight - rect.bottom;
    }


    if (width <= viewWidth) {
        deltaX = (viewWidth - width) / 2 - rect.left;
    } else if (rect.left > 0) {
        deltaX = -rect.left;
    } else if (rect.right < viewWidth) {
        deltaX = viewWidth - rect.right;
    }

    mSuppMatrix.postTranslate(deltaX, deltaY);
    return true;
}複製程式碼

結尾

還是老樣子,希望大家不要吐槽。有問題留言哈哈。。O(∩_∩)O哈哈~

附上Demo地址:ScaleImageVewDemo

相關文章