天天看點

Android觸摸事件(四)-CropView裁剪工具的使用目錄

目錄

  • 目錄
    • 概述
    • 相關文章
    • 處理流程
      • 裁剪方式
      • 裁剪原理
      • 裁剪流程
        • 圖檔加載
        • 圖檔繪制
        • 擷取螢幕的大小
        • 計算圖檔繪制區域
        • 繪制圖檔
        • 計算裁剪框
        • 繪制裁剪框
    • 裁剪操作
        • 計算實際裁剪區域
        • 裁剪并儲存圖檔
    • 關于移動的限制
    • 關于縮放的限制
    • 使用方式
    • 源碼
      • 裁剪類源碼
      • 接口源碼
    • GitHub位址
    • 示例GIF
      • 示例圖檔裁剪
      • 修複代碼之前存在的問題

概述

相關文章

AbsTouchEventHandle

觸摸事件

TouchUtils

觸摸事件輔助工具類

此工具基于以上兩個類

處理流程

裁剪方式

圖檔的裁剪方式有很多種,最常見的有兩種吧.

但不管是哪什麼類型的裁剪方式,至少需要處理的有以下兩個點:

  • 圖檔顯示
  • 裁剪框顯示

裁剪方式的不同在于以上兩部分的處理方式不同而不同.

  • 系統預設方式

    圖檔加載滿螢幕,不可移動或縮放,裁剪框移動或者縮放選擇裁剪區域完成裁剪

  • 微信裁剪方式

    裁剪框加載螢幕,不可移動或者縮放,圖檔移動或者縮放選擇裁剪區域完成裁剪

以上兩種方式并沒很大的差别.但第二種方式可能會更好.因為在裁剪一個比較大的圖檔時,想更細節地裁剪一部分圖檔時,第二種方式更容易也更好地做到這一點.

第一種方式(系統預設的)隻能是裁剪框縮放,當裁剪的範圍很小時,就不容易控制了.

這裡我們使用第二種裁剪方式(微信的裁剪方式).

裁剪原理

裁剪原理其實在上面裁剪方式中也基本提及了,主要是通過圖檔或者裁剪框的縮放來确定裁剪的區域.

我們使用微信的裁剪方式,則是通過圖檔縮放來确定裁剪區域.

這裡圖檔需要完成的操作有移動和縮放.是以我們使用之前處理觸摸事件的類進行處理.

AbsTouchEventHandle

,觸摸事件處理類

TouchUtils

,移動與縮放輔助工具類

關于以上兩個類的使用,檢視相關的文章即可.

裁剪流程

關于流程,我們從圖檔加載開始到圖檔裁剪結束并儲存做一個完整的流程進行分析.

  • 圖檔加載
  • 圖檔繪制
  • 裁剪框繪制(裁剪框使用正方形)
  • 圖檔移動或者縮放(進而改變裁剪區域)
  • 裁剪圖檔
  • 儲存圖檔

這裡我們确定需要使用的一些變量.

//圖檔來源路徑
String inputPath;
//圖檔輸出存放路徑
String outputPath;
//裁剪框
RectF mCropRectf;
//圖檔繪制區域
RectF mBitmapRectf;
//裁剪的源圖檔
Bitmap mBitmap;
           

圖檔加載

圖檔的加載并沒有什麼特别的,通過圖檔的路徑

inputPath

加載出裁剪的源圖檔即可;

這裡需要注意的是,某些情況下,需要裁剪後的圖檔的品質和大小并不是很高(如頭像),這種情況下如果源圖檔很大的話,加載到記憶體是很容易導緻一些問題的.是以建議:

在不必要的情況下,盡量不要加載源圖檔進行裁剪,而是加載出适當的縮略圖進行裁剪

圖檔繪制

圖檔繪制和裁剪框的繪制是一個重點.當然圖檔的移動與縮放也是重點,但使用之前的兩個觸摸事件處理類已經處理了這些事件,是以在這裡我們并不需要去擔心這個問題.

首先,确定圖檔初始化時繪制的區域.在裁剪中,我們希望達到的效果是:

預設情況下,裁剪的圖檔全屏顯示,并且裁剪框應該居中同時也全屏顯示.

是以圖檔的繪制就很明确了.我們需要全屏顯示圖檔.

擷取螢幕的大小

全屏顯示我們需要擷取到目前螢幕的大小.

//存放螢幕寬高大小的全局變量
PointF mViewParmas;
//擷取螢幕寬高大小
private void getViewParams() {
    if (mIsFirstGetViewParams) {
        mViewParams.x = mDrawView.getWidth();
        mViewParams.y = mDrawView.getHeight();
    }
}
           

其中使用變量

mIsFirstGetViewParams

檢測是因為螢幕的大小不會改變,我們隻需要擷取一次就夠了.避免了反複擷取.

擷取了螢幕的大小,接下來就是計算圖檔繪制的區域并繪制圖檔了.

計算圖檔繪制區域

計算圖檔的繪制區域是一個比較複雜的過程.因為初始狀态圖檔必須是全屏顯示.

事實上我們并不需要去拉伸圖檔以填充螢幕,我們需要做的隻是放大或者縮放圖檔以适應整個螢幕.

基于這個原則,我們需要考慮圖檔本身大小與螢幕寬高大小的關系.

由于圖檔可以縮放,我們現在假設圖檔與螢幕寬高在同一個等級.則可能有以下情況:

  • 圖檔寬/高與螢幕寬/高相等(完美情況)
  • 圖檔寬=螢幕寬且高<=螢幕高(寬主導)
  • 圖檔高=螢幕高且寬<=螢幕寬(高主導)
  • 圖檔為正方形圖檔
由于圖檔需要填充螢幕,

當出現某一邊主導的情況時,我們需要以該邊為基礎填充螢幕,剩下的另一邊隻能是小于螢幕對應邊

;但我們可以讓圖檔保持居中,這樣會看起來更合理正常.

此處的計算方式如下:

  • 假設以寬填充螢幕,計算填充後的高度再與螢幕高度比較.(方式1)
  • 同理以高填充螢幕,計算填充後的寬度與螢幕寬度比較.(方式2)
  • 在任何一種方式中,隻要計算得到的高或者寬不超過目前螢幕允許的高或者寬,該方式即可用于圖檔填充螢幕
//計算最大繪制寬高
//此處不使用螢幕大小是為了四邊留白白
float largeDrawWidth = mViewParams.x * ;
float largeDrawHeight = mViewParams.y * ;
//擷取圖檔寬高,也作為最終圖檔繪制的寬高
int width = mBitmap.getWidth();
int height = mBitmap.getHeight();
//以圖檔高填充螢幕,計算填充的寬度
int tempDrawWidth = (int) ((largeDrawHeight / height) * width);
//以圖檔寬填充螢幕,計算填充的高度
int tempDrawHeight = (int) ((largeDrawWidth / width) * height);
//若填充寬度小于最大繪制寬度時
if (tempDrawWidth < largeDrawWidth) {
    //以高填充(填充後的寬不超過螢幕寬,圖檔可以完全顯示)
    width = tempDrawWidth;
    height = (int) largeDrawHeight;
} else {
    //否則,以寬填充,填充後的高不超過螢幕寬,圖檔可以完全顯示
    height = tempDrawHeight;
    width = (int) largeDrawWidth;
}
//不存在以下的情況:
//不管以高填充還是寬填充,填充後的另一邊都超過螢幕的長度,數學證明略
           

繪制圖檔

計算完圖檔的繪制區域後必然是繪制圖檔了.繪制圖檔時有一個需要注意的地方就是之前在

TouchUtils

類的使用中提及的,拖動本身的偏移量處理由

TouchUtils

類管理,而繪制元素隻保持自身的屬性.

是以在繪制時需要将偏移量添加到繪制區域中,生成一個實際的(帶偏移參數)繪制區域.

//以圖檔繪制區域為基礎建立實際的繪制區域
RectF moveRectf = new RectF(src);
//X/Y添加上偏移量
moveRectf.left += mTouch.getDrawOffsetX();
moveRectf.top += mTouch.getDrawOffsetY();
moveRectf.right += mTouch.getDrawOffsetX();
moveRectf.bottom += mTouch.getDrawOffsetY();
//傳回帶偏移參數的繪制區域
return moveRectf;
           

計算裁剪框

首先,裁剪框使用一個

RectF

對象來儲存需要顯示可裁剪的區域(透明區域,可以直接顯示後面的圖檔);

關于裁剪框的位置與大小是比較好确定的.

  • 裁剪框是正方形
  • 裁剪框螢幕居中
  • 裁剪框以螢幕最小邊為邊長

由于前面我們已經得到了圖檔的繪制區域(預設)的大小,此時圖檔繪制區域是填充整個螢幕的(嚴格來說是某一邊填充螢幕),其中的

mBitmapRectf.width()

mBitmapRectf.height()

分别是填充後的圖檔的寬高的縮放長度(與圖檔實際的寬高比例是一緻的).

是以可以取其中最小的一邊作為螢幕裁剪框的邊長大小.

//計算繪制區域的最小邊,與裁剪區域的大小進行比較
float smallDrawSize = ;
if (mBitmapRecf.width() > mBitmapRecf.height()) {
    smallDrawSize = mBitmapRecf.height();
} else {
    smallDrawSize = mBitmapRecf.width();
}
//建立裁剪區域對象
if (mCropRecf == null) {
    mCropRecf = new RectF();
}
//以螢幕中心建立裁剪框區域坐标
float centerX = mViewParams.x / ;
float centerY = mViewParams.y / ;
mCropRecf.left = centerX - smallDrawSize / ;
mCropRecf.top = centerY - smallDrawSize / ;
mCropRecf.right = mCropRecf.left + smallDrawSize;
mCropRecf.bottom =mCropRecf.top + smallDrawSize;
           

這裡需要注意的是,使用預設的圖檔繪制區域的寬/高中最小邊作為裁剪框的邊長而不是螢幕的最小邊作為裁剪框的邊長的原因:

可能存在一種情況,圖檔的寬高比例達到100:1(類似的極緻性比例),這種情況下必定以圖檔的長為填充邊,所得的高将很小很小;

如果裁剪以螢幕最小邊為邊長,則會裁剪到圖檔以外的區域(不存在資料,根本無法裁剪);

以圖檔繪制區域最小邊確定了裁剪框裁剪的區域必定在圖檔範圍内,也不造成裁剪到圖檔以外的區域進而導緻某些異常

繪制裁剪框

裁剪框的繪制并不是直接繪制裁剪框,因為我們需要保持裁剪區域的透明以明确裁剪的範圍.

裁剪框以外的區域使用半透明的黑色覆寫達到表示該部分區域不屬于裁剪範圍的效果.

是以,繪制裁剪區域實際上是

以半透明黑色繪制裁剪框以外的區域

,保持裁剪框的透明以達到裁剪框的效果.
//将裁剪框以外的區域分為四個矩形,并分别計算四個無效區域的坐标
RectF topRecf = new RectF(, , mViewParams.x, mCropRecf.top);
RectF bottomRecf = new RectF(, mCropRecf.bottom, mViewParams.x, mViewParams.y);
RectF leftRecf = new RectF(, mCropRecf.top, mCropRecf.left, mCropRecf.bottom);
RectF rightRecf = new RectF(mCropRecf.right, mCropRecf.top, mViewParams.x, mCropRecf.bottom);
//設定半透明畫筆并繪制無效區域
mPaint.setColor(Color.BLACK);
mPaint.setAlpha((int) ( * ));
canvas.drawRect(topRecf, mPaint);
canvas.drawRect(bottomRecf, mPaint);
canvas.drawRect(leftRecf, mPaint);
canvas.drawRect(rightRecf,mPaint);
           

裁剪操作

圖檔的裁剪操作其實就是在圖檔上指定區域的像素儲存下來.是以裁剪一張圖檔必須保證:

裁剪的區域在圖檔的有效範圍内(不可以超出圖檔區域)

裁剪的是基于圖檔本身的像素裁剪,是以需要将裁剪區域縮放到以圖檔自身大小等級進而确定裁剪區域

計算實際裁剪區域

由于圖檔是被縮放過的,對應的裁剪框中的位置也是不是真實圖檔中的裁剪位置.需要縮放裁剪框進而得到真實的裁剪區域.

計算裁剪框的實際位置時,需要

  • 計算裁剪框實際的寬高(裁剪框為正方形,僅計算一邊即可)
  • 計算裁剪框在真實圖檔中的位置
//圖檔實際寬高
int bmpWidth = bitmap.getWidth();
int bmpHeight = bitmap.getHeight();
//圖檔繪制的區域寬高
float drawWidth = bmpRectF.width();
float drawHeight = bmpRectF.height();

 //目前需要裁剪圖檔區域的寬高
float cropWidth = cropRectF.width();
float cropHeight = cropRectF.height();

//計算實際的裁剪寬高
float realCropWidth = (cropWidth / drawWidth) * bmpWidth;
float realCropHeight = realCropWidth;

//計算實際裁剪區域的坐标
//以目前左上角坐标在縮放圖檔的相對位置,計算出等比情況下在真實圖檔中的相對位置
float realCropTop = ((cropRectF.top - bmpRectF.top) / drawHeight) * bmpHeight;
float realCropLeft = ((cropRectF.left - bmpRectF.left) / drawWidth) * bmpWidth;

//建立實際裁剪區域
Rect srcRect = new Rect();
srcRect.top = (int) realCropTop;
srcRect.left = (int) realCropLeft;
srcRect.bottom = srcRect.top + (int) realCropWidth;
srcRect.right = srcRect.left + (int) realCropHeight;
//傳回實際的裁剪區域坐标
return srcRect;
           

裁剪并儲存圖檔

裁剪圖檔的很簡單,通過系統方法從圖檔中儲存下裁剪區域的像素即可.

//裁剪目前的圖檔
Bitmap cropBmp = Bitmap.createBitmap(mBitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
           

圖檔的儲存通過

Bitmap

提供的方法即可通過流儲存到檔案中.同時儲存時還可以指定輸出圖檔的品質,圖檔格式.

圖檔的大小由品質.大小及格式決定,壓縮率更高,品質越低,圖檔寬高越小的,圖檔大小也就越小.

關于移動的限制

前面使用

TouchUtils

時已經提到提供了回調接口用于确定是否允許界面移動.往往在實際的操作也功能需求中這是需要限定的條件.

在此處也是需要限定的.

圖檔的移動不可以超過裁剪框所在的區域.

必須確定任何時候裁剪框都在圖檔的範圍内

否則在裁剪時就可能出現裁剪到圖檔以外的區域,這會造成某些異常的抛出.

檢測方式也很簡單,由于裁剪框必須在圖檔内部,是以保證:

裁剪框的左邊界永遠在圖檔的左邊界右邊;

裁剪框的右邊界永遠在圖檔的右邊界左邊;

裁剪框的上邊界永遠在圖檔上邊界的下邊;

裁剪框的下邊界永遠在圖檔下邊界的上邊;

//此處僅列出 是否允許移動 的回調判斷
@Override
public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) {
    //mTempDstRectf是一個全局複用的暫存的rectf
    //重置為圖檔原始繪制區域
    mTempDstRectf.set(mBitmapRecf);
    //使用新偏移量
    mTempDstRectf.offset(newOffsetX, );
    //判斷目前新圖檔區域與裁剪區域left/right方向是否越界
    return (mTempDstRectf.left <=mCropRecf.left && mTempDstRectf.right >=mCropRecf.right);
}

@Override
public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) {
    //同理isCanMovedOnX()
    mTempDstRectf.set(mBitmapRecf);
    mTempDstRectf.offset(, newOffsetY);
    return (mTempDstRectf.top <= mCropRecf.top && mTempDstRectf.bottom >= mCropRecf.bottom);
}
           
以下為修複前的代碼,這部分判斷是錯誤的.因為忽略了一個小細節問題.
float moveDistance = Math.abs(moveDistacneY);
RectF drawRectf = this.getRectfAfterMove(mBitmapRecf);
if (moveDistacneY > ) {
    return moveDistance <= (mCropRecf.top - drawRectf.top);
} else {
    return moveDistance <= (drawRectf.bottom - mCropRecf.bottom);
}
           

分析以上的代碼可知,判斷的原則還不變.原理上來說是沒有錯誤的.但是存在的問題是:

moveDistance

是指觸摸點按下

ACTION_DOWN

事件時坐标與目前觸摸點坐标的距離.

而在以上錯誤代碼中,

getRectfAfterMove()

方法已經将最近一次

ACTION_MOVE

事件中偏移量添加到臨時的圖檔繪制區域對象中.

此時得到的

drawRectf

為上一次

ACTION_MOVE

事件後界面的位置;此時再添加上

moveDistance

并與裁剪區域比較時,實際上多了一部分的偏移量.

這個地方不應該使用

getRectfAfterMove()

去偏移圖檔繪制區域,而應該使用

newOffset

.

newOffset

變量為此次

ACTION_MOVE

事件與

ACTION_DOWN

事件之間坐标的距離,也就是我們需要判斷的距離.

不使用

ACTION_DOWN

事件時界面的位置加上

moveDistance

進行判斷的原因是,

TouchUtils

并沒有提供

ACTION_DOWN

時偏移量的方法.

盡管實際上

TouchUtils

是存在這個變量的(

見mTempDrawOffset

),但不對外開放是為了保證方法的明确性.同時提供移動偏移量(

mDrawOffset

)和移動前偏移量(

mTempDrawOffset

)會很容易使調用者混亂.

詳見TouchUtils類

關于縮放的限制

圖檔允許進行縮放,但裁剪框必須在圖檔内部.而裁剪框大小是固定的(在建立并顯示初始圖檔時已經确定了).

是以圖檔縮放是有限制的,圖檔放大可以由使用者自定義,此裁剪View中也沒有限制;但是縮小的極限是不能小于裁剪框.

圖檔在縮小時需要保持圖檔的最小邊要永遠大于裁剪框,這樣才能確定裁剪框内的區域是有效的圖檔區域.
//擷取圖檔的寬高
float oldWidth = mTempBmpRectF.width();
float oldHeigh = mTempBmpRectF.height();
//計算新縮放的寬高
float newWidth = oldWidth * newScaleRate;
float newHeight = oldHeigh * newScaleRate;
//擷取新縮放圖檔的最短邊
float smallSize = newWidth > newHeight ? newHeight : newWidth;
//最短邊必須大于裁剪區域的邊
//否則不允許縮放
return smallSize >mCropRecf.width();
           

以上是縮放圖檔時的限制,需要注意的這裡使用的繪制區域

mTempBmpRectf

,實際繪制時使用的變量是

mBmpRectf

.

此處是

TouchUtils

中提及的,每次一次縮放時,都會儲存縮放前的圖檔大小,縮放時不斷重繪界面使用的是

mBmpRectf

,此變量值會在縮放期間不斷改變.

mTempBmpRectf

為暫存值,暫存了每一次縮放前圖檔的大小.在縮放結束之後會儲存此次縮放後的大小.

(詳見TouchUtils)

使用方式

關于此類的使用方式,後面會有一個專門使用此類進行裁剪圖檔的

Activity

詳解,在一般情況下隻要調用該

Activity

就可以進行正常的圖檔裁剪了.

當然不管是此類還是裁剪的

Activity

,都可以根據自己的需要進行修改.

以下附上簡單的使用方式:

//将 CropView 添加到xml界面中
//加載一張圖檔,略
Bitmap mphoto;
//直接設定圖檔即可(在onCreate中)
mCropView.setImageBitmap(mphoto);
//若是在其它情況下需要修改圖檔時,應該手動調用一次重繪操作,裁剪View才會更新界面圖檔
//mCropView.postInvalidate();

//裁剪并儲存目前裁剪框中的圖檔
//outputPath為裁剪後圖檔輸出路徑(完整的檔案名哦,不隻是檔案夾)
//如:/storage/cropBmp.png
mCropView.restoreBitmap(outputPath);

//圖檔回收
//當裁剪完确定不再需要裁剪時,回收圖檔,請注意需要不再裁剪并不顯示圖檔時才回收(基本上是 Activity 退出時再回收)
mCropView.recycleBitmap();
           
關于圖檔的回收工作,其實由于裁剪View中使用的是指派時圖檔的引用對象,是以在外部直接回收圖檔也是可以的.

mPhoto.recycle();

但是

CropView

中作了安全處理,是以建議還不管有沒有回收圖檔,都進行一次

cropView.recycleBitmap();

源碼

源碼是裁剪工具View的源碼.需要注意View本身隻是個間接處理對象,僅僅是用于将界面顯示給使用者.實際的繪制和操作是在内部類

CropDrawUtils

處理

關于使用的接口并不需要特别關注,隻是為了友善View的部分方法與此裁剪處理類的方法簽名統一而使用的.

不使用接口的直接删除接口及 @Override 即可

裁剪類源碼

/**
 * Created by taro on 16/4/8.
 */
public class CropDraw extends AbsTouchEventHandle implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent, ICropDrawAction {
    //建立工具類
    private TouchUtils mTouch = new TouchUtils();
    private View mDrawView = null;
    public static final float DEFAULT_CROP_WIDTH = ;
    public static final float DEFAULT_CROP_HEIGHT = ;

    private Paint mPaint = null;
    private Bitmap mBitmap = null;
    private RectF mCropRecf = null;
    private RectF mTempDstRectf = null;
    private RectF mBitmapRecf = null;
    private PointF mViewParams = null;

    private float mCropWidth = DEFAULT_CROP_WIDTH;
    private float mCropHeight = DEFAULT_CROP_HEIGHT;

    //暫時性儲存的半徑(同理在繪制時也需要一個暫時性存放的資料)
    private RectF mTempBmpRectF = null;

    private boolean mIsFirstGetViewParams = true;

    //針對構造函數,可有不同的需求,在此例中,其實并不需要context
    //此參數是可有可無的,有時自定義繪制界面需要加載一些資源什麼的需要用到context,
    //這個時候就有用了,這個看需要
    public CropDraw(View drawView) {
        this.mDrawView = drawView;
        //設定工具類的監聽事件
        mTouch.setMoveEvent(this);
        mTouch.setScaleEvent(this);
        mTouch.setIsShowLog(false);
        //綁定view與觸摸事件
        this.mDrawView.setOnTouchListener(this);

        initial();
    }


    /**
     * 繪制界面
     *
     * @param canvas
     */
    public void onDraw(Canvas canvas) {
        getViewParams();
        drawBitmap(canvas);
        drawCropRecf(canvas);

        mPaint.setAlpha();
    }

    @Override
    public void setImageBitmap(Bitmap src) {
        if (src == null) {
            return;
        } else {
            mBitmap = src;
            mBitmapRecf.setEmpty();
            mDrawView.postInvalidate();
        }
    }

    /**
     * 設定裁剪區域的寬高大小,此方法應該在界面繪制之前調用有效
     *
     * @param eachSize 邊長
     * @return
     */
    public boolean setCropWidthAndHeight(int eachSize) {
        mCropWidth = eachSize;
        mCropHeight = eachSize;

        //調整裁剪區域寬高大小
        //若調整了傳回true,否則傳回false
        //此處傳回相反的值是因為:使用指定參數設定成功的話傳回true,若調整過了則使用不到參數,傳回False
        return !isAdjustCropWidthAndHeight();
    }

    /**
     * 初始化資料及對象
     */
    private void initial() {
        mPaint = new Paint();
        mBitmapRecf = new RectF();
        mViewParams = new PointF();
        mTempDstRectf = new RectF();
        mCropRecf = new RectF();

        this.setIsShowLog(false, null);
    }

    /**
     * 調整裁剪區域的寬高大小
     *
     * @return
     */
    private boolean isAdjustCropWidthAndHeight() {
        boolean isChanged = false;
        //預設裁剪區域的大小與圖檔最大邊大小相同,都是90%
        float largeCropSizeX = mViewParams.x * f;
        float largeCropSizeY = mViewParams.y * f;
        float largeCropSize = largeCropSizeX < largeCropSizeY ? largeCropSizeX : largeCropSizeY;
        if (mCropWidth > largeCropSize || mCropWidth <= ) {
            mCropWidth = largeCropSize;
            isChanged = true;
        }
        if (mCropHeight > largeCropSize || mCropHeight <= ) {
            mCropHeight = largeCropSize;
            isChanged = true;
        }
        return isChanged;
    }

    /**
     * 擷取目前view的寬高等參數
     */
    private void getViewParams() {
        if (mIsFirstGetViewParams) {
            mViewParams.x = mDrawView.getWidth();
            mViewParams.y = mDrawView.getHeight();
        }
    }

    /**
     * 繪制圖檔
     *
     * @param canvas
     * @return
     */
    private boolean drawBitmap(Canvas canvas) {
        if (mBitmap != null && !mBitmap.isRecycled() && canvas != null) {
            //第一次繪制先計算圖檔的繪制大小
            if (mBitmapRecf == null || mBitmapRecf.isEmpty()) {
                //計算圖檔縮放顯示的繪制區域
                int width = mBitmap.getWidth();
                int height = mBitmap.getHeight();
                //計算最大繪制區域
                //此處不使用螢幕大小是為了四邊留白白
                float largeDrawWidth = mViewParams.x * f;
                float largeDrawHeight = mViewParams.y * f;

                //以圖檔高填充螢幕,計算填充的寬度
                int tempDrawWidth = (int) ((largeDrawHeight / height) * width);
                //以圖檔寬填充螢幕,計算填充的高度
                int tempDrawHeight = (int) ((largeDrawWidth / width) * height);
                //若填充寬度小于最大繪制寬度時
                if (tempDrawWidth < largeDrawWidth) {
                    //以高填充(填充後的寬不超過螢幕寬,圖檔可以完全顯示)
                    width = tempDrawWidth;
                    height = (int) largeDrawHeight;
                } else {
                    //否則,以寬填充,填充後的高不超過螢幕寬,圖檔可以完全顯示
                    height = tempDrawHeight;
                    width = (int) largeDrawWidth;
                }
                //不存在以下的情況:
                //不管以高填充還是寬填充,填充後的另一邊都超過螢幕的長度,數學證明略

                //建立繪制區域
                mBitmapRecf.left = (mViewParams.x - width) / ;
                mBitmapRecf.right = mBitmapRecf.left + width;
                mBitmapRecf.top = (mViewParams.y - height) / ;
                mBitmapRecf.bottom = mBitmapRecf.top + height;

                //計算繪制區域的最小邊,與裁剪區域的大小進行比較
                float smallDrawSize = ;
                if (mBitmapRecf.width() > mBitmapRecf.height()) {
                    smallDrawSize = mBitmapRecf.height();
                } else {
                    smallDrawSize = mBitmapRecf.width();
                }
                //建立裁剪區域對象
                float centerX = mViewParams.x / ;
                float centerY = mViewParams.y / ;
                mCropRecf.left = centerX - smallDrawSize / ;
                mCropRecf.top = centerY - smallDrawSize / ;
                mCropRecf.right = mCropRecf.left + smallDrawSize;
                mCropRecf.bottom = mCropRecf.top + smallDrawSize;
                //更新裁剪區域的邊大小
                mCropWidth = mCropRecf.width();
                mCropHeight = mCropRecf.height();

                Log.i("bmpdraw", "bitmap\nleft=" + mBitmapRecf.left + "\nright=" + mBitmapRecf.right
                        + "\ntop=" + mBitmapRecf.top + "\nbottom=" + mBitmapRecf.bottom);
                Log.i("bmpcrop", "crop\nleft=" + mCropRecf.left + "\nright=" + mCropRecf.right
                        + "\ntop=" + mCropRecf.top + "\nbottom=" + mCropRecf.bottom);
            }

            RectF moveRectf = getRectfAfterMove(mBitmapRecf, mTempDstRectf);
            canvas.drawBitmap(mBitmap, null, moveRectf, mPaint);
//            Log.i("bmpmove", "bitmap\nleft=" + moveRectf.left + "\nright=" + moveRectf.right
//                    + "\ntop=" + moveRectf.top + "\nbottom=" + moveRectf.bottom);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 擷取移動後的實際繪制區域
     *
     * @param src 預設的繪制區域
     * @param dst
     * @return 傳回新的繪制區域對象
     */
    private RectF getRectfAfterMove(RectF src, RectF dst) {
        if (src != null) {
            if (dst == null) {
                dst = new RectF(src);
            } else {
                dst.set(src);
            }

            //X/Y添加上偏移量
            dst.left += mTouch.getDrawOffsetX();
            dst.top += mTouch.getDrawOffsetY();
            dst.right += mTouch.getDrawOffsetX();
            dst.bottom += mTouch.getDrawOffsetY();

            return dst;
        } else {
            return null;
        }
    }

    /**
     * 擷取目前裁剪圖檔區域在實際圖檔中的裁剪區域
     *
     * @param bitmap    原始圖檔對象
     * @param cropRectF 目前裁剪區域
     * @param bmpRectF  圖檔繪制區域
     * @return 傳回實際圖檔中的裁剪區域大小
     */
    private Rect getBitmapScaleRect(Bitmap bitmap, RectF cropRectF, RectF bmpRectF) {
        //圖檔存在且相應的區域都有效的情況下才能進行計算
        if (bitmap != null && cropRectF != null && bmpRectF != null) {
            //圖檔實際寬高
            int bmpWidth = bitmap.getWidth();
            int bmpHeight = bitmap.getHeight();
            //圖檔繪制的區域寬高
            float drawWidth = bmpRectF.width();
            float drawHeight = bmpRectF.height();

            //目前需要裁剪圖檔區域的寬高
            float cropWidth = cropRectF.width();
            float cropHeight = cropRectF.height();

            //計算實際的裁剪寬高
            float realCropWidth = (cropWidth / drawWidth) * bmpWidth;
            float realCropHeight = realCropWidth;

            //計算實際裁剪區域的坐标
            float realCropTop = ((cropRectF.top - bmpRectF.top) / drawHeight) * bmpHeight;
            float realCropLeft = ((cropRectF.left - bmpRectF.left) / drawWidth) * bmpWidth;

            //建立實際裁剪區域
            Rect srcRect = new Rect();
            srcRect.top = (int) realCropTop;
            srcRect.left = (int) realCropLeft;
            srcRect.bottom = srcRect.top + (int) realCropWidth;
            srcRect.right = srcRect.left + (int) realCropHeight;
            return srcRect;
        } else {
            return null;
        }
    }

    @Override
    public boolean restoreBitmap(String fileNameWithPath, Bitmap.CompressFormat bmpFormat, boolean isRecycleBmp, int bmpQuality) {
        if (!TextUtils.isEmpty(fileNameWithPath) && !mBitmap.isRecycled()) {
            //預設使用PNG
            if (bmpFormat == null) {
                String lowerPath = fileNameWithPath.toLowerCase();
                if (lowerPath.endsWith("png")) {
                    bmpFormat = Bitmap.CompressFormat.PNG;
                } else if (lowerPath.endsWith("jpg") || lowerPath.endsWith("jpeg")) {
                    bmpFormat = Bitmap.CompressFormat.JPEG;
                } else {
                    bmpFormat = Bitmap.CompressFormat.PNG;
                }
            }
            if (bmpQuality >  || bmpQuality < ) {
                bmpQuality = ;
            }
            //擷取圖檔裁剪區域
            Rect srcRect = getBitmapScaleRect(mBitmap, mCropRecf, getRectfAfterMove(mBitmapRecf, mTempDstRectf));
            //裁剪目前的圖檔
            Bitmap cropBmp = Bitmap.createBitmap(mBitmap, srcRect.left, srcRect.top, srcRect.width(), srcRect.height());

            try {
                File bitmapFile = new File(fileNameWithPath);
                if (!bitmapFile.exists()) {
                    bitmapFile.createNewFile();
                }
                //将圖檔儲存到檔案中
                FileOutputStream out = new FileOutputStream(bitmapFile);
                cropBmp.compress(bmpFormat, bmpQuality, out);
                if (isRecycleBmp) {
                    //回收圖檔
                    mBitmap.recycle();
                }
                return true;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return false;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            } finally {
                //不管是否裁剪成功,都回收裁剪後的圖檔,因為這部分是用不到的
                if (cropBmp != null && !cropBmp.isRecycled()) {
                    cropBmp.recycle();
                }
            }
        } else {
            return false;
        }
    }

    @Override
    public void recycleBitmap() {
        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
        }
    }

    /**
     * 繪制圖檔裁剪界面,實際上隻是繪制一個透明的蒙層
     *
     * @param canvas
     * @return
     */
    private boolean drawCropRecf(Canvas canvas) {
        if (canvas != null && mCropRecf != null) {
            RectF topRecf = new RectF(, , mViewParams.x, mCropRecf.top);
            RectF bottomRecf = new RectF(, mCropRecf.bottom, mViewParams.x, mViewParams.y);
            RectF leftRecf = new RectF(, mCropRecf.top, mCropRecf.left, mCropRecf.bottom);
            RectF rightRecf = new RectF(mCropRecf.right, mCropRecf.top, mViewParams.x, mCropRecf.bottom);

            mPaint.setColor(Color.BLACK);
            mPaint.setAlpha((int) ( * ));
            canvas.drawRect(topRecf, mPaint);
            canvas.drawRect(bottomRecf, mPaint);
            canvas.drawRect(leftRecf, mPaint);
            canvas.drawRect(rightRecf, mPaint);

            return true;
        } else {
            return false;
        }
    }

    @Override
    public void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) {
        //工具類預設處理的單點觸摸事件
        mTouch.singleTouchEvent(event, extraMotionEvent);
    }

    @Override
    public void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) {
        //工具類預設處理的多點(實際隻處理了兩點事件)觸摸事件
        mTouch.multiTouchEvent(event, extraMotionEvent);
    }

    @Override
    public void onSingleClickByTime(MotionEvent event) {
        //基于時間的單擊事件
        //按下與擡起時間不超過500ms
    }

    @Override
    public void onSingleClickByDistance(MotionEvent event) {
        //基于距離的單擊事件
        //按下與擡起的距離不超過20像素(與時間無關,若按下不動幾小時後再放開隻要距離在範圍内都可以觸發)
    }

    @Override
    public void onDoubleClickByTime() {
        //基于時間的輕按兩下事件
        //單擊事件基于clickByTime的兩次單擊
        //兩次單擊之間的時間不超過250ms
    }


    @Override
    public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) {
        mTempDstRectf.set(mBitmapRecf);
        mTempDstRectf.offset(newOffsetX, );
        return (mTempDstRectf.left <= mCropRecf.left && mTempDstRectf.right >= mCropRecf.right);
    }

    @Override
    public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) {
        mTempDstRectf.set(mBitmapRecf);
        mTempDstRectf.offset(, newOffsetY);
        return (mTempDstRectf.top <= mCropRecf.top && mTempDstRectf.bottom >= mCropRecf.bottom);
    }

    @Override
    public void onMove(int suggestEventAction) {
        mDrawView.postInvalidate();
    }

    @Override
    public void onMoveFail(int suggetEventAction) {

    }

    @Override
    public boolean isCanScale(float newScaleRate) {
        if (mTempBmpRectF == null) {
            mTempBmpRectF = new RectF(mBitmapRecf);
        }
        float oldWidth = mTempBmpRectF.width();
        float oldHeigh = mTempBmpRectF.height();
        float newWidth = oldWidth * newScaleRate;
        float newHeight = oldHeigh * newScaleRate;
        //擷取繪制區域的最短邊
        float smallSize = newWidth > newHeight ? newHeight : newWidth;
        //最短邊必須大于裁剪區域的邊
        return smallSize > mCropRecf.width();
    }

    @Override
    public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
        //更新目前的資料
        //newScaleRate縮放比例一直是相對于按下時的界面的相對比例,是以在移動過程中
        //每一次都是要與按下時的界面進行比例縮放,而不是針對上一次的結果
        //使用這種方式一方面在縮放時的思路處理是比較清晰的
        //另一方面是縮放的比例不會資料很小(若相對于上一次,每一次move移動幾個像素,
        //這種情況下縮放的比例相對上一次肯定是0.0XXXX,資料量一小很容易出現一些不必要的問題)
        if (mTempBmpRectF == null) {
            mTempBmpRectF = new RectF(mBitmapRecf);
        }

        float lastWidth = mTempBmpRectF.width();
        float lastHeight = mTempBmpRectF.height();

        float newWidth = lastWidth * newScaleRate;
        float newHeight = lastHeight * newScaleRate;

        mBitmapRecf.left = mTempBmpRectF.centerX() - newWidth / ;
        mBitmapRecf.top = mTempBmpRectF.centerY() - newHeight / ;
        mBitmapRecf.right = mBitmapRecf.left + newWidth;
        mBitmapRecf.bottom = mBitmapRecf.top + newHeight;
        //當傳回的标志為true時,提醒為已經到了up事件
        //此時應該把最後一次縮放的比例當做最終的資料儲存下來
        if (isNeedStoreValue) {
            mTempBmpRectF.set(mBitmapRecf);
        }
    }

    @Override
    public void onScale(int suggestEventAction) {
        if (suggestEventAction == MotionEvent.ACTION_POINTER_UP) {
            //調整縮放後圖檔的位置必須在裁剪框中
            RectF drawRectf = this.getRectfAfterMove(mBitmapRecf, mTempDstRectf);
            float offsetX = mTouch.getDrawOffsetX();
            float offsetY = mTouch.getDrawOffsetY();
            if (drawRectf.left > mCropRecf.left) {
                offsetX += mCropRecf.left - drawRectf.left;
            }
            if (drawRectf.top > mCropRecf.top) {
                offsetY += mCropRecf.top - drawRectf.top;
            }
            if (drawRectf.right < mCropRecf.right) {
                offsetX += mCropRecf.right - drawRectf.right;
            }
            if (drawRectf.bottom < mCropRecf.bottom) {
                offsetY += mCropRecf.bottom - drawRectf.bottom;
            }
            mTouch.setOffsetX(offsetX);
            mTouch.setOffsetY(offsetY);
        }
        mDrawView.postInvalidate();
    }

    @Override
    public void onScaleFail(int suggetEventAction) {

    }
}
           

接口源碼

另附上接口檔案

/**
 * Created by taro on 16/4/8.
 */
public interface ICropDrawAction {
    /**
     * 儲存圖檔
     *
     * @param fileNameWithPath 儲存路徑
     * @param bmpFormat        儲存圖檔的格式
     * @param isRecycleBmp     是否回收圖檔,若true則回收圖檔,若false則不回收圖檔
     * @param bmpQuality       儲存圖檔的品質,在0-100,格式為PNG時此參數無效.
     * @return 若成功儲存圖檔傳回true, 否則傳回false
     */
    public boolean restoreBitmap(String fileNameWithPath, Bitmap.CompressFormat bmpFormat, boolean isRecycleBmp, int bmpQuality);

    /**
     * 回收圖檔
     */
    public void recycleBitmap();

    /**
     * 設定裁剪區域的寬高大小,此方法應該在界面繪制之前調用有效
     *
     * @param eachSize 邊長
     * @return
     */
    public boolean setCropWidthAndHeight(int eachSize);

    /**
     * 設定imageBitmap
     *
     * @param src
     */
    public void setImageBitmap(Bitmap src);
}
           

GitHub位址

https://github.com/CrazyTaro/TouchEventHandle

以上為裁剪View的代碼,項目中

CropView

是使用于

Activity

中,後面會詳細說明該

Activity

并附帶簡單的圖檔輔助功能.

示例GIF

示例圖檔裁剪

Android觸摸事件(四)-CropView裁剪工具的使用目錄

修複代碼之前存在的問題

可以看到在圖檔移動時可能會将圖檔移動到界面裁剪區域以外,這種情況下下面會出現一條白條(背景色).這種情況下無法儲存裁剪的圖檔,會出錯.

Android觸摸事件(四)-CropView裁剪工具的使用目錄

回到目錄