Android裁剪旋轉(zhuǎn)CropView從入門到放棄

CropView從入門到放棄

本篇我會帶你去從零設(shè)計一款裁剪旋轉(zhuǎn)的View

2022 年有感

本篇是 2018 年寫的,現(xiàn)在重新 review 了下,思路是好的,就是代碼寫的不夠優(yōu)雅,太不優(yōu)雅了,各種重復(fù)代碼,各位讀者朋友們,不要照抄代碼噢,重復(fù)代碼應(yīng)該封裝,有些可以放到枚舉里簡化取值操作,并且繪制邏輯里不要重復(fù) new 對象,會影響性能喔,共勉?。?!


你需要準(zhǔn)備的

裁剪的View最關(guān)鍵的是裁剪框的繪制和手勢的調(diào)整,另外還有最核心的裁剪功能就是調(diào)用方法createBitmap去裁剪得到目標(biāo)圖片。

//圖片裁剪的核心功能
Bitmap.createBitmap(originalBitmap,//原圖
                 cropX,//圖片裁剪橫坐標(biāo)開始位置
                 cropY,//圖片裁剪縱坐標(biāo)開始位置
                 cropWidth,//要裁剪的寬度
                 cropHeight);//要裁剪的高度
  • 1、裁剪框的繪制無非就是一個透明的方形和灰色透明度的方形之外的區(qū)域(如下圖紅色箭頭所指區(qū)域)、裁剪框的繪制關(guān)鍵的地方在于裁剪的Rect區(qū)域的確定
示意圖
  • 2、手勢調(diào)整裁剪框的邊角、四周邊。裁剪控件可以拖動的邊界為上圖的紫色箭頭區(qū)域,并且有一個點擊所在點區(qū)域的周邊也可以正常去控制邊界的拖動,說明有一個動態(tài)的范圍

目錄

為了能正常的顯示圖片的縮放比例和旋轉(zhuǎn)、居中等,可以考慮自己控制Bitmap的各項繪制參數(shù)。

一、測量

主要是Padding值的處理

二、布局
  • 1、Bitmap中心點的確定
  • 2、圖片比例的確定
  • 3、圖片Bitmap參數(shù)的確定(比如旋轉(zhuǎn)。放大)
  • 4、圖片顯示區(qū)域的確定
  • 5、裁剪框區(qū)域的確定
三、繪制
  • 1、畫背景
  • 2、畫Bitmap
  • 3、畫裁剪框
  • 4、畫邊界線
  • 5、畫指導(dǎo)線
  • 6、畫定制四邊角的圖案
四、添加MotionEvent控制
  • 1、按下時計算點擊的區(qū)域位置
  • 2、根據(jù)第1步點擊的區(qū)域去判斷需不需要對邊界進(jìn)行Move處理
  • 3、抬起時恢復(fù)默認(rèn)的處理狀態(tài)
五、旋轉(zhuǎn)翻轉(zhuǎn)處理
  • 1、左右翻轉(zhuǎn)
  • 2、旋轉(zhuǎn)處理
六、裁剪
  • 1、對獲取的bitmap進(jìn)行旋轉(zhuǎn)處理
  • 2、計算裁剪的原圖
  • 3、對圖片進(jìn)行翻轉(zhuǎn)處理
  • 4、拿到裁剪后的圖
七、開放屬性Style
八、使用

額外需要首先考慮的

首先思考我們的控件大概需要什么動態(tài)的屬性,為了以后方便擴(kuò)展業(yè)務(wù)或者功能的精確控制

  • 1、裁剪框有一個最小的矩形大小,說明要限制裁剪框的長度寬度,一般為正方形(本例中設(shè)計為了正方形,當(dāng)然解析到該塊內(nèi)容時我會順便說下如何實現(xiàn)長寬不一樣的裁剪框)。此處需要屬性為裁剪框最小邊長
  • 2、裁剪框外圍除了畫Bitmap之外,會存在除了Bitmap之外的空白區(qū)域,這里希望能控制他們的顏色。此處需要屬性為窗口背景顏色
  • 3、由于我們要自定義Bitmap區(qū)域的繪制,類似ImageView控件,我們需要src屬性來引入圖片內(nèi)容。此處需要屬性為src
  • 4、上面講過,手勢點擊邊界或者角時,點擊所在點區(qū)域的周邊也可以正常去控制邊界的拖動,說明有一個動態(tài)的范圍。這個范圍也希望是可以控制的。此處需要屬性為觸摸邊界的大小范圍
  • 5、顏色的比如有:裁剪內(nèi)框的顏色,裁剪外框的顏色邊界線的顏色,裁剪框指導(dǎo)線的顏色,四周邊角自定義點的圖案的顏色
  • 6、尺寸的比如:邊框線的寬度,指導(dǎo)線的寬度四周邊角線寬度、長度(或者圓圈的半徑)
  • 7、圖片初始化的比例
  • 8、根據(jù)默認(rèn)打開裁剪控件時的狀態(tài)確定開關(guān)類屬性:裁剪框的是否繪制
  • 9、如果說有時候指導(dǎo)線是想要點擊才拖動裁剪框才顯示,那么還要添加指導(dǎo)線的繪制開關(guān)
  • 10、同理,四邊角自定義圖案也需要開關(guān)
  • 11、如果你想的夠多,那么這些邊角圖案有時候可能還會加入類似button的點擊之后放大圖案的需求,那么這個時候你還需要添加一個開關(guān)用來控制要不要開啟邊角動畫(逃)


一、測量

測量View的大小,主要是處理Padding的值,甚至某些情況要專門對wrap_content屬性做專門的處理。對Padding進(jìn)行處理如下

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        final int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(viewWidth, viewHeight);
        mViewWidth = viewWidth - getPaddingLeft() - getPaddingRight();
        mViewHeight = viewHeight - getPaddingTop() - getPaddingBottom();
    }

對左右Padding進(jìn)行了處理,最終得到的是CropView內(nèi)容的區(qū)域的實際寬高


二、布局

這一部分只要是對Bitmap中心點、圖片比例、裁剪框區(qū)域的確定,布局代碼如下,為了提高性能,這里只有在圖片已設(shè)置到View之后getDrawable()有值的時候才進(jìn)行布局,

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (getDrawable() != null) {
            doLayout(mViewWidth, mViewHeight);
        }
    }
/**
     * 對圖片進(jìn)行布局
     *
     * @param viewWidth
     * @param viewHeight
     */
    private void doLayout(int viewWidth, int viewHeight) {
        if (viewHeight == 0 || viewWidth == 0) {
            return;
        }
        //計算中心點
        PointF pointF = new PointF(getPaddingLeft() + viewWidth * 0.5f, getPaddingTop() + viewHeight * 0.5f);
        //保存中心點
        setCenter(pointF);
        //計算放大比例和保存比例
        setScale(calcScale(viewWidth, viewHeight, mImgAngle));
        //Bitmap的舉證變換
        setMatrix();
        RectF rectF = new RectF(0, 0, mImgWidth, mImgHeight);
        //圖片Bitmap的實際區(qū)域
        mImageRectF = calcImageRect(rectF, mMatrix);
        //計算裁剪框的Rect區(qū)域
        mFrameRectF = calculateFrameRect(mImageRectF);
        //標(biāo)記為初始化完成
        mIsInitialized = true;
        invalidate();
    }
1、Bitmap中心點的確定

這個沒什么好說的,就是View除了Padding區(qū)域外x,y取中點

2、圖片比例的確定

這里開始會用到一些變量(或者是屬性)

// 旋轉(zhuǎn)角度
private float mImgAngle = 0.0f;
 // 圖片寬度
private float mImgWidth = 0.0f;
// 圖片高度
private float mImgHeight = 0.0f;
// 放大比例
private float mCropScale = 1.0f;

計算比例的代碼如下,其實就是圖片的寬高之比

private float calcScale(int viewWidth, int viewHeight, float angle) {
        mImgWidth = getDrawable().getIntrinsicWidth();
        mImgHeight = getDrawable().getIntrinsicHeight();
        if (mImgWidth <= 0)
            mImgWidth = viewWidth;
        if (mImgHeight <= 0)
            mImgHeight = viewHeight;
        float viewRatio = (float) viewWidth / (float) viewHeight;
        //旋轉(zhuǎn)的情況
        float imgRatio = getRotatedWidth(angle) / getRotatedHeight(angle);
        float scale = 1.0f;
        if (imgRatio >= viewRatio) {
            scale = viewWidth / getRotatedWidth(angle);
        } else if (imgRatio < viewRatio) {
            scale = viewHeight / getRotatedHeight(angle);
        }
        return scale;
    }
    

首先拿到Drawable的固有寬寬高,接著對圖片的寬高mImgWidth、mImgHeight進(jìn)行邊界控制,然后計算出圖片的長寬比例,要是有旋轉(zhuǎn)的情況那么viewRatio的值要拿到旋轉(zhuǎn)后的寬高來判斷,這里關(guān)鍵就是怎么樣拿到旋轉(zhuǎn)后的寬高,getRotatedWidth方法如下,傳入旋轉(zhuǎn)的角度,如果是旋轉(zhuǎn)了180度,那么原來旋轉(zhuǎn)前的寬也是旋轉(zhuǎn)后的寬。如果旋轉(zhuǎn)了90或者270,那么旋轉(zhuǎn)后的寬是原來的高度。同樣getRotatedHeight方法原理也是一樣的。

private float getRotatedWidth(float angle) {
        return getRotatedWidth(angle, mImgWidth, mImgHeight);
    }
    
private float getRotatedWidth(float angle, float width, float height) {
        return angle % 180 == 0 ? width : height;
    }

最后會把放大當(dāng)前的圖片比例保存下來

    /**
     * 保存比例
     *
     * @param scale
     */
    private void setScale(float scale) {
        mCropScale = scale;
    }
3、圖片Bitmap參數(shù)的確定(比如旋轉(zhuǎn)。放大)

接下來會對圖片的平移或者放大比例進(jìn)行調(diào)整

用到屬性如下

// 圖形的矩陣類
private Matrix mMatrix = null;
// 居中中點的矩型區(qū)域
private PointF mCenter = new PointF();

并且矩陣類我們會在構(gòu)造方法里進(jìn)行初始化

 mMatrix = new Matrix();

一張Bitmap圖片加載到View默認(rèn)情況下一般是這樣



那我們需要做的是將它平移到中點,并且比例放大到我們想要的比例

所以,Matrix變化如下,先對其做平移操作,平移距離是Bitmap顯示區(qū)域中點到屏幕中點的。放大是以屏幕中心點為放大原點,然后X,Y進(jìn)行整比例的放大,后面考慮到我們要用到旋轉(zhuǎn)操作,也加了旋轉(zhuǎn)的Matrix操作。

    /**
     * 重設(shè)矩陣,平移縮放旋轉(zhuǎn)操作
     */
    private void setMatrix() {
        mMatrix.reset();
        mMatrix.setTranslate(mCenter.x - mImgWidth * 0.5f, mCenter.y - mImgHeight * 0.5f);
        mMatrix.postScale(mCropScale, mCropScale, mCenter.x, mCenter.y);
        mMatrix.postRotate(mImgAngle, mCenter.x, mCenter.y);
        
    }
4、圖片顯示區(qū)域的確定

接下來會用到很多關(guān)于裁剪框的區(qū)域和圖片顯示區(qū)域的Rect變量,我們可以一次性全部定義了它們


    // 圖形的矩陣類
    private Matrix mMatrix = null;
    // 圖片的rectf區(qū)域
    private RectF mImageRectF;
    // 裁剪框的RectF
    private RectF mFrameRectF;
    RectF rectF = new RectF(0, 0, mImgWidth, mImgHeight);
        mImageRectF = calcImageRect(rectF, mMatrix);
         /**
     * 將Matrix映射到rect
     *
     * @param rect
     * @param matrix
     * @return
     */
    private RectF calcImageRect(RectF rect, Matrix matrix) {
        RectF applied = new RectF();
        matrix.mapRect(applied, rect);
        return applied;
    }

根據(jù)Img長寬初始化一個新的Rect區(qū)域,然后將變化后的Matrix應(yīng)用到Rect

5、裁剪框區(qū)域的確定

裁剪框的繪制會涉及到多種比例的變化,因此這部分會是我們最復(fù)雜的一塊,首先我們先定義裁剪的模式,一般情況下,裁剪模式分為充滿View四周不能控制型、自由控制型、比例型三種。

定義一個枚舉類來標(biāo)記裁剪模式,這里比例型有7種,其實定義多少都可以,什么比例都可以,最為關(guān)鍵是需要在拿到比例后進(jìn)行通用的處理

    /**
     * 裁剪框的比例模式
     *
     * 
     */
    public enum CropModeEnum {
        FIT_IMAGE(0), RATIO_2_3(1), RATIO_3_2(2), RATIO_4_3(3), RATIO_3_4(4), SQUARE(5), RATIO_16_9(6), RATIO_9_16(7), FREE(
            8);
        private final int ID;

        CropModeEnum(int id) {
            ID = id;
        }

        public int getID() {
            return ID;
        }
    }

需要定義變量

// 裁剪模式,默認(rèn)比例是自由模式
private CropModeEnum mCropMode = CropModeEnum.FREE;

計算裁剪框的代碼如下:

private RectF calculateFrameRect(RectF imageRect) {
        float frameW = getRatioX(imageRect.width());
        float frameH = getRatioY(imageRect.height());
        float frameRatio = frameW / frameH;
        float imgRatio = imageRect.width() / imageRect.height();

        float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
        if (frameRatio >= imgRatio) {
            //寬比長比例大于img圖寬高比的情況
            l = imageRect.left;
            r = imageRect.right;
            //圖的中點
            float hy = (imageRect.top + imageRect.bottom) * 0.5f;
            //中點到上下頂點坐標(biāo)的距離
            float hh = (imageRect.width() / frameRatio) * 0.5f;
            t = hy - hh;
            b = hy + hh;
        } else if (frameRatio < imgRatio) {
            //寬比長比例大于img圖寬高比的情況
            t = imageRect.top;
            b = imageRect.bottom;
            float hx = (imageRect.left + imageRect.right) * 0.5f;
            float hw = imageRect.height() * frameRatio * 0.5f;
            l = hx - hw;
            r = hx + hw;
        }
        //裁剪框?qū)挾?        float w = r - l;
        //高度
        float h = b - t;
        //中心點
        float cx = l + w / 2;
        float cy = t + h / 2;
        //放大后的裁剪框的寬高
        float sw = w * mInitialFrameScale;
        float sh = h * mInitialFrameScale;
        return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);
    }

其中計算比例的getRatioX()如下,通過Switch當(dāng)前的裁剪模式,返回比例或者當(dāng)前的圖片寬度,getRatioY()原理是也是一樣的

private float getRatioX(float w) {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRectF.width();
            case FREE:
                return w;
            case RATIO_2_3:
                return 2;
            case RATIO_3_2:
                return 3;
            case RATIO_4_3:
                return 4;
            case RATIO_3_4:
                return 3;
            case RATIO_16_9:
                return 16;
            case RATIO_9_16:
                return 9;
            case SQUARE:
                return 1;
            default:
                return w;
        }
    }

接著我們通過分別計算獲取裁剪框后的比例和原drawable圖的比例,拿到他們的比例為了判斷裁剪的框要處于圖片框中的什么位置

        //獲取裁剪模式后計算的比例
        float frameRatio = frameW / frameH;
        //圖片原始比例
        float imgRatio = imageRect.width() / imageRect.height();

現(xiàn)在我們要做的是,計算出裁剪框的坐標(biāo)位置,裁剪框的位置可能性有很多,我們可以假設(shè)一種來看一下


假設(shè)我們現(xiàn)在的圖為1.5:1的圖,如上圖的1圖。現(xiàn)在裁剪框的設(shè)定比例是2:1,那么正常比例下裁剪框是無法充滿img的,需要對裁剪框做縮放然后放入img框中,如上圖的3圖。
根據(jù)這種情況,我們要對裁剪框做Rect坐標(biāo)的定位處理。

        float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
        if (frameRatio >= imgRatio) {
            //寬比長比例大于img圖寬高比的情況
            l = imageRect.left;
            r = imageRect.right;
            //圖的中點
            float hy = (imageRect.top + imageRect.bottom) * 0.5f;
            //中點到上下頂點坐標(biāo)的距離
            float hh = (imageRect.width() / frameRatio) * 0.5f;
            t = hy - hh;
            b = hy + hh;
        } else if (frameRatio < imgRatio) {
            //寬比長比例大于img圖寬高比的情況
            t = imageRect.top;
            b = imageRect.bottom;
            float hx = (imageRect.left + imageRect.right) * 0.5f;
            float hw = imageRect.height() * frameRatio * 0.5f;
            l = hx - hw;
            r = hx + hw;
        }

首先定義裁剪框的四個邊角參數(shù)左上角left、top,右下角right、bottom坐標(biāo)分別為l,t,r,b。
frameRatio >= imgRatio的情況其實就是上面的示意圖的情況,左右left和right等同于原img圖,top/bottom可以根據(jù)計算得到的裁剪框的高度imageRect.width() / frameRatio去確認(rèn)。

寬度/高度=frameRatio

最后計算得到top和bottom的坐標(biāo)

            t = hy - hh;
            b = hy + hh;

同理,frameRatio < imgRatio的情況原理和這個類似。

最后根據(jù)這些參數(shù)確認(rèn)實際的裁剪框的區(qū)域

        //裁剪框?qū)挾?        float w = r - l;
        //高度
        float h = b - t;
        //中心點
        float cx = l + w / 2;
        float cy = t + h / 2;
        //放大后的裁剪框的寬高
        float sw = w * mInitialFrameScale;
        float sh = h * mInitialFrameScale;
        return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);

mInitialFrameScale這個是初始狀態(tài)下給裁剪框的默認(rèn)放大比例參數(shù),0-1,設(shè)置為1,則充滿img的邊界,設(shè)置為其他值,比如0.2,那會以圖片中心點為基準(zhǔn)進(jìn)行縮小。


三、繪制

以上全部準(zhǔn)備工作做完之后,就可以開始繪制我們最關(guān)鍵的裁剪框了

 @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(mBackgroundColor);
        //只有初始化完畢了才繪制
        if (mIsInitialized) {
            setMatrix();//這里一開始繪制裁剪框是沒有想到的,后面刷新視圖的時候,設(shè)置了參數(shù)之后需要計算bitmap的情況,需要重新對Bitmap進(jìn)行矩陣處理
            Bitmap bitmap = getBitmap();
            if (bitmap != null) {
                canvas.drawBitmap(bitmap, mMatrix, mBitmapPaint);
                // 畫裁剪框
                drawCropFrame(canvas);
            }
        }
    }
1、畫背景

畫ImageView外圍的背景(圖片顯示之外會存在的空白部分)

canvas.drawColor(mBackgroundColor);
2、畫Bitmap
canvas.drawBitmap(bitmap, mMatrix, mBitmapPaint);

將bitmap畫到已確定位置的mMatrix矩陣中

3、畫裁剪框
private void drawCropFrame(Canvas canvas) {
        drawOverlay(canvas);
        drawFrame(canvas);
        drawGuidelines(canvas);
        drawHandleLines(canvas);    
   }

接下來裁剪框會有覆蓋層,透明層,邊界線和指導(dǎo)線的繪制

 /**
     * 畫裁剪框的半透明覆蓋層
     *
     * @param canvas
     */
    private void drawOverlay(Canvas canvas) {
        mTranslucentPaint.setStyle(Paint.Style.FILL);
        mTranslucentPaint.setFilterBitmap(true);
        mTranslucentPaint.setColor(mOverlayColor);
        Path path = new Path();
        RectF overlayRectF = new RectF();
        overlayRectF.set(mImageRectF);
        
        path.addRect(overlayRectF, Path.Direction.CW);
        path.addRect(mFrameRectF, Path.Direction.CCW);
        canvas.drawPath(path, mTranslucentPaint);
    }

第二部分拿到了圖片的Rect區(qū)域,我們這里畫半透明覆蓋層只要調(diào)用路徑來處理,這里我們調(diào)用了路徑了兩種不同的方向來添加,以使最終得到的路徑是裁剪框之外。(當(dāng)然這里也可以用路徑合成方式的處理)

        path.addRect(mFrameRectF, Path.Direction.CW);
        path.addRect(overlayRectF, Path.Direction.CCW);
4、畫邊界線

屬性變量

    // 裁剪外框線框?qū)挾?    private float mFrameStrokeWeight = 2f;
    // 外框的顏色
    private int mFrameColor;

直接拿到FrameRect邊界來繪制邊界線

     /**
     * 畫裁剪框邊界線
     *
     * @param canvas
     */
    private void drawFrame(Canvas canvas) {
        mFramePaint.setStyle(Paint.Style.STROKE);
        mFramePaint.setFilterBitmap(true);
        mFramePaint.setColor(mFrameColor);
        mFramePaint.setStrokeWidth(mFrameStrokeWeight);
        canvas.drawRect(mFrameRectF, mFramePaint);
    }
5、畫指導(dǎo)線

這里同理,只需要拿到裁剪框邊界,計算索要的點的位置的點的坐標(biāo)即可繪制自己想要的線條

    /**
     * 畫指導(dǎo)線
     *
     * @param canvas
     */
    private void drawGuidelines(Canvas canvas) {
        mFramePaint.setColor(mGuideColor);
        mFramePaint.setStrokeWidth(mGuideStrokeWeight);
        // 從左往右第一個豎線的橫坐標(biāo)
        float x1 = mFrameRectF.left + (mFrameRectF.right - mFrameRectF.left) / 3.0f;
        float x2 = mFrameRectF.right - (mFrameRectF.right - mFrameRectF.left) / 3.0f;
        // 從上往下第一個橫的y坐標(biāo)
        float y1 = mFrameRectF.top + (mFrameRectF.bottom - mFrameRectF.top) / 3.0f;
        float y2 = mFrameRectF.bottom - (mFrameRectF.bottom - mFrameRectF.top) / 3.0f;
        // 畫豎線
        canvas.drawLine(x1, mFrameRectF.top, x1, mFrameRectF.bottom, mFramePaint);
        canvas.drawLine(x2, mFrameRectF.top, x2, mFrameRectF.bottom, mFramePaint);
        // 畫橫線
        canvas.drawLine(mFrameRectF.left, y1, mFrameRectF.right, y1, mFramePaint);
        canvas.drawLine(mFrameRectF.left, y2, mFrameRectF.right, y2, mFramePaint);
    }
6、畫定制四邊角的圖案

這里我是在四個角畫了線條,由于自由模式有點特殊,自由模式下四條邊也可以拉動,所以需要給四個邊中間點也繪制圖案

變量如下

    // 四角線的長度值
    private int mHandleSize;
    // 四角的線寬度
    private int mHandleWidth;

主要就是坐標(biāo)的計算

/**
     * 畫四角的線
     *
     * @param canvas
     */
    private void drawHandleLines(Canvas canvas) {
        mFramePaint.setColor(mHandleColor);
        mFramePaint.setStyle(Paint.Style.FILL);
        // 指導(dǎo)線最邊界(最左/最右/最下/最上)的x和y
        float handleLineLeftX = mFrameRectF.left - mHandleWidth;
        float handleLineRightX = mFrameRectF.right + mHandleWidth;
        float handleLineTopY = mFrameRectF.top - mHandleWidth;
        float handleLineBottomY = mFrameRectF.bottom + mHandleWidth;
        // 左上豎向
        RectF ltRectFVertical =
            new RectF(handleLineLeftX, handleLineTopY, mFrameRectF.left, handleLineTopY + mHandleSize);
        // 左上橫向
        RectF ltRectFHorizontal =
            new RectF(handleLineLeftX, handleLineTopY, handleLineLeftX + mHandleSize, mFrameRectF.top);

        RectF rtRectFHorizontal =
            new RectF(handleLineRightX - mHandleSize, handleLineTopY, handleLineRightX, mFrameRectF.top);
        RectF rtRectFVertical =
            new RectF(mFrameRectF.right, handleLineTopY, handleLineRightX, handleLineTopY + mHandleSize);

        RectF lbRectFVertical =
            new RectF(handleLineLeftX, handleLineBottomY - mHandleSize, mFrameRectF.left, mFrameRectF.bottom);
        RectF lbRectFHorizontal =
            new RectF(handleLineLeftX, mFrameRectF.bottom, handleLineLeftX + mHandleSize, handleLineBottomY);

        RectF rbRectFVertical =
            new RectF(mFrameRectF.right, handleLineBottomY - mHandleSize, handleLineRightX, handleLineBottomY);
        RectF rbRectFHorizontal =
            new RectF(handleLineRightX - mHandleSize, mFrameRectF.bottom, handleLineRightX, handleLineBottomY);

        canvas.drawRect(ltRectFVertical, mFramePaint);
        canvas.drawRect(ltRectFHorizontal, mFramePaint);

        canvas.drawRect(rtRectFVertical, mFramePaint);
        canvas.drawRect(rtRectFHorizontal, mFramePaint);

        canvas.drawRect(lbRectFVertical, mFramePaint);
        canvas.drawRect(lbRectFHorizontal, mFramePaint);

        canvas.drawRect(rbRectFVertical, mFramePaint);
        canvas.drawRect(rbRectFHorizontal, mFramePaint);

        if (mCropMode == CropModeEnum.FREE) {
            // 如果當(dāng)前是自由模式
            mFramePaint.setStrokeCap(Paint.Cap.ROUND);
            mFramePaint.setStrokeWidth(mHandleWidth + SizeUtils.dp2px(2));

            float centerX = mFrameRectF.left + mFrameRectF.width() / 2;
            float centerY = mFrameRectF.top + mFrameRectF.height() / 2;

            canvas.drawLine(centerX - mHandleSize, mFrameRectF.top, (centerX + mHandleSize), mFrameRectF.top,
                mFramePaint);
            canvas.drawLine(centerX - mHandleSize, mFrameRectF.bottom, centerX + mHandleSize, mFrameRectF.bottom,
                mFramePaint);
            canvas.drawLine(mFrameRectF.left, (centerY - mHandleSize), mFrameRectF.left, centerY + mHandleSize,
                mFramePaint);
            canvas.drawLine(mFrameRectF.right, centerY - mHandleSize, mFrameRectF.right, centerY + mHandleSize,
                mFramePaint);
        }
    }

四、添加MotionEvent控制

手指觸摸區(qū)域會有10種情況,除了四個邊角和邊界之外,還有裁剪框中間和裁剪框外部的情況,這里定義一個枚舉類來標(biāo)記這10種情況

    /**
     * 手指點擊的區(qū)域枚舉類
     *
     * @time Created by 2018/8/22 19:00
     */
    public enum TouchAreaEnum {
        CENTER, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM, OUT_OF_BOUNDS, CENTER_LEFT, CENTER_TOP, CENTER_RIGHT, CENTER_BOTTOM
    }

定義默認(rèn)的觸摸模式

    // 觸摸的情況
    private TouchAreaEnum mTouchArea = TouchAreaEnum.OUT_OF_BOUNDS;

接下來我們需要在Down事件里記錄處理的邊界,Move事件里動態(tài)判斷邊界是否需要移動,其他事件里恢復(fù)初始的狀態(tài)

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                onActionDown(event);
                return true;
            }
            case MotionEvent.ACTION_MOVE: {
                onActionMove(event);
            
                if (mTouchArea != TouchAreaEnum.OUT_OF_BOUNDS) {
                    // 阻止父view攔截事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                return true;
            }
            case MotionEvent.ACTION_CANCEL: {
                getParent().requestDisallowInterceptTouchEvent(true);
                onActionCancel();
                return true;
            }
            case MotionEvent.ACTION_UP: {
                getParent().requestDisallowInterceptTouchEvent(true);
                onActionUp();
                return true;
            }
            default: {
                break;
            }
        }
        return false;
    }
1、按下時計算點擊的區(qū)域位置
private void onActionDown(MotionEvent event) {
        invalidate();
        mLastX = event.getX();
        mLastY = event.getY();
        handleTouchArea(mLastX, mLastY);
    }
    
    /**
     * <Strong>控制指導(dǎo)線或者邊框線的顯示</Strong>
     * <p></p>
     * 處理觸摸的邊界來控制指導(dǎo)線或者邊框線的顯示,共5種,四個角和里面的中心部分
     *
     * @param x
     * @param y
     */
    private void handleTouchArea(float x, float y) {
        if (isInLeftTopCorner(x, y)) {
            mTouchArea = TouchAreaEnum.LEFT_TOP;
            handleGuideAndHandleMode();
            return;
        }
        if (isInRightTopCorner(x, y)) {
            mTouchArea = TouchAreaEnum.RIGHT_TOP;
            handleGuideAndHandleMode();
            return;
        }
        if (isInLeftBottomCorner(x, y)) {
            mTouchArea = TouchAreaEnum.LEFT_BOTTOM;
            handleGuideAndHandleMode();
            return;
        }
        if (isInRightBottomCorner(x, y)) {
            mTouchArea = TouchAreaEnum.RIGHT_BOTTOM;
            handleGuideAndHandleMode();
            return;
        }

        if (isInCenterLeftCorner(x, y)) {
            mTouchArea = TouchAreaEnum.CENTER_LEFT;
            handleGuideAndHandleMode();
            return;
        }
        if (isInCenterTopCorner(x, y)) {
            mTouchArea = TouchAreaEnum.CENTER_TOP;
            handleGuideAndHandleMode();
            return;
        }
        if (isInCenterRightCorner(x, y)) {
            mTouchArea = TouchAreaEnum.CENTER_RIGHT;
            handleGuideAndHandleMode();
            return;
        }
        if (isInCenterBottomCorner(x, y)) {
            mTouchArea = TouchAreaEnum.CENTER_BOTTOM;
            handleGuideAndHandleMode();
            return;
        }

        if (isInFrameCenter(x, y)) {
            if (mGuideShowMode == ShowModeEnum.SHOW_ON_TOUCH)
                mShowGuide = true;
            mTouchArea = TouchAreaEnum.CENTER;
            return;
        }
        // 默認(rèn)情況
        mTouchArea = TouchAreaEnum.OUT_OF_BOUNDS;
    }

這里的關(guān)鍵在于handleTouchArea()方法中,情況比較多,但是原理相同,這里只分析一種情況

    if (isInLeftTopCorner(x, y)) {
            mTouchArea = TouchAreaEnum.LEFT_TOP;
     
            return;
        }
        
        //判斷是否在左上角的邊界內(nèi)
     private boolean isInLeftTopCorner(float x, float y) {
        float dx = x - mFrameRectF.left;
        float dy = y - mFrameRectF.top;
        return isInsideBound(dx, dy);
    }
    
    private boolean isInsideBound(float dx, float dy) {
        float d = (float) (Math.pow(dx, 2) + Math.pow(dy, 2));
        return (Math.pow(mHandleSize + mTouchPadding, 2)) >= d;
    }

這里處理的其實就是判斷觸摸的點是否在邊界的有效范圍內(nèi),比如現(xiàn)在的點是左上角,那么可觸摸區(qū)域應(yīng)該是圍繞左上角頂點的四周圓形的一塊區(qū)域。如下圖,應(yīng)該是黃色的一塊圓形區(qū)域。isInsideBound()做的就是對邊界內(nèi)外的判斷。

2、根據(jù)第1步點擊的區(qū)域去判斷需不需要對邊界進(jìn)行Move處理

邊界移動也存在很多種情況??傮w上可以分為裁剪框整體移動和裁剪框邊界移動。

private void onActionMove(MotionEvent event) {
        float diffX = event.getX() - mLastX;
        float diffY = event.getY() - mLastY;
        // 區(qū)分點擊的區(qū)域進(jìn)行移動
        switch (mTouchArea) {
            case CENTER: {
                moveFrame(diffX, diffY);
                break;
            }
            case LEFT_TOP: {
                moveHandleLeftTop(diffX, diffY);
                break;
            }
            case RIGHT_TOP: {
                moveHandleRightTop(diffX, diffY);
                break;
            }
            case LEFT_BOTTOM: {
                moveHandleLeftBottom(diffX, diffY);
                break;
            }
            case RIGHT_BOTTOM: {
                moveHandleRightBottom(diffX, diffY);
                break;
            }

            case CENTER_LEFT: {
                moveHandleCenterLeft(diffX);
                break;
            }
            case CENTER_TOP: {
                moveHandleCenterTop(diffY);
                break;
            }
            case CENTER_RIGHT: {
                moveHandleCenterRight(diffX);
                break;
            }
            case CENTER_BOTTOM: {
                moveHandleCenterBottom(diffY);
                break;
            }
            case OUT_OF_BOUNDS: {
                break;
            }
        }
        invalidate();
        mLastX = event.getX();
        mLastY = event.getY();
    }
  • A.針對裁剪框移動進(jìn)行分析,move事件得到的X,Y只需要交給裁剪邊框的RectF處理就大吉大利了,handleMoveBounds()是平移的相反的操作,這種策略可避免產(chǎn)生由于Move數(shù)值精度不準(zhǔn)確產(chǎn)生的邊界問題)

private void moveFrame(float x, float y) {
        // 1.先平移(這里采取先平移如果條件不滿足再后退的策略,避免產(chǎn)生由于Move數(shù)值精度不準(zhǔn)確產(chǎn)生的邊界問題)
        mFrameRectF.left += x;
        mFrameRectF.right += x;
        mFrameRectF.top += y;
        mFrameRectF.bottom += y;
        // 2.判斷有沒有超出界外,如果超出則后退
        handleMoveBounds();
    }
  • B、針對邊框的邊界進(jìn)行處理,處理邊框會有2種情況,一種是自由模式下,長寬都可以分別調(diào)整,另外一種是確定比例的模式下,長寬需要一起調(diào)整的情況
private void moveHandleLeftTop(float diffX, float diffY) {
        if (mCropMode == CropModeEnum.FREE) {
            mFrameRectF.left += diffX;
            mFrameRectF.top += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mFrameMinSize - mFrameRectF.width();
                mFrameRectF.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mFrameMinSize - mFrameRectF.height();
                mFrameRectF.top -= offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRectF.left += dx;
            mFrameRectF.top += dy;
            // 控制縮放邊界
            if (isWidthTooSmall()) {
                float offsetX = mFrameMinSize - mFrameRectF.width();
                mFrameRectF.left -= offsetX;
                // todo 裁剪框比例控制
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRectF.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mFrameMinSize - mFrameRectF.height();
                mFrameRectF.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRectF.left -= offsetX;
            }

            float ox, oy;
            if (!isInsideX(mFrameRectF.left)) {
                ox = mImageRectF.left - mFrameRectF.left;
                mFrameRectF.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRectF.top += oy;
            }
            if (!isInsideY(mFrameRectF.top)) {
                oy = mImageRectF.top - mFrameRectF.top;
                mFrameRectF.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRectF.left += ox;
            }
        }
    }
  • A、對自由模式下的邊界拖動進(jìn)行處理
        if (mCropMode == CropModeEnum.FREE) {
            //不管發(fā)生什么事,先把過來的數(shù)值先加上
            mFrameRectF.left += diffX;
            mFrameRectF.top += diffY;
            //判斷變化后的邊界有沒有比限定的最小邊界小,如果小,那么就還原
            if (isWidthTooSmall()) {
                float offsetX = mFrameMinSize - mFrameRectF.width();
                mFrameRectF.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mFrameMinSize - mFrameRectF.height();
                mFrameRectF.top -= offsetY;
            }
            //處理有沒有超出最大邊界
            checkScaleBounds();
        }
  • B、對非自由模式下的邊界的縮放就行處理
            //這里只需要監(jiān)測move事件的x或者y的變化,然后通過設(shè)定的比例計算出另外的x或者y的坐標(biāo),從而限定裁剪框的比例
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            
            mFrameRectF.left += dx;
            mFrameRectF.top += dy;
            // 控制縮放邊界
            if (isWidthTooSmall()) {
                //同上自由模式的控制,這里也是要講超界的值恢復(fù),只是這里x,y要分別恢復(fù)
                float offsetX = mFrameMinSize - mFrameRectF.width();
                mFrameRectF.left -= offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRectF.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mFrameMinSize - mFrameRectF.height();
                mFrameRectF.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRectF.left -= offsetX;
            }
            //限制不能讓裁剪框超出圖片邊界
            float ox, oy;
            if (!isInsideX(mFrameRectF.left)) {
                ox = mImageRectF.left - mFrameRectF.left;
                mFrameRectF.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRectF.top += oy;
            }
            if (!isInsideY(mFrameRectF.top)) {
                oy = mImageRectF.top - mFrameRectF.top;
                mFrameRectF.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRectF.left += ox;
            }
3、抬起時恢復(fù)默認(rèn)的處理狀態(tài)

其它點擊事件,我們只要恢復(fù)默認(rèn)狀態(tài)就可

    private void onActionCancel() {
        mTouchArea = TouchAreaEnum.OUT_OF_BOUNDS;
        invalidate();
    }

旋轉(zhuǎn)翻轉(zhuǎn)處理

1、左右翻轉(zhuǎn)

左右翻轉(zhuǎn)和上下翻轉(zhuǎn)只需要用View默認(rèn)的ScaleX和ScaleY來控制就可以,比如左右翻轉(zhuǎn)

    /**
     * 左右翻轉(zhuǎn)
     */
    public void reverseY() {
        if (!mIsReverseY) {
            mIsReverseY = true;
        } else {
            mIsReverseY = false;
        }
        super.setScaleX(getScaleX() * -1f);
    }
2、旋轉(zhuǎn)處理

旋轉(zhuǎn)多少度是個問題,這就跟晚飯吃什么是個問題如此。

這里定義了關(guān)于旋轉(zhuǎn)角度的枚舉類:

/**
     * 旋轉(zhuǎn)角度的枚舉類
     *
     * @time Created by 2018/8/22 19:07
     */
    public enum RotateDegreesEnum {
        ROTATE_90D(90), ROTATE_180D(180), ROTATE_270D(270), ROTATE_M90D(-90), ROTATE_M180D(-180), ROTATE_M270D(-270), ROTATE_0D(
            0);

        private final int VALUE;

        RotateDegreesEnum(final int value) {
            this.VALUE = value;
        }

        public int getValue() {
            return VALUE;
        }
    }

還記得一開始使用的Metrix矩陣嗎,我們可以通過它來實現(xiàn)對當(dāng)前Img的旋轉(zhuǎn),先計算旋轉(zhuǎn)后的比例和角度變化,然后只要將變化值和起始值終點值放入屬性動畫中進(jìn)行驅(qū)動就會有旋轉(zhuǎn)時候的動畫效果。當(dāng)然,如果不需要動畫(比如下面代碼的最后),你可以直接設(shè)置終值,然后重新layout

    /**
     * 旋轉(zhuǎn)圖片
     *
     * @param degrees
     * @param durationMillis 需要調(diào)整的動畫時間
     */
    private void rotateImage(RotateDegreesEnum degrees, int durationMillis) {
        if (mIsRotating) {
            mValueAnimator.cancel();
        }
        //先計算旋轉(zhuǎn)后的比例和角度變化
        final float currentAngle = mImgAngle;
        //新的角度
        final float newAngle = (mImgAngle + degrees.getValue());
        final float angleDiff = newAngle - currentAngle;
        final float currentScale = mCropScale;
        //旋轉(zhuǎn)后的比例
        final float newScale = calcScale(mViewWidth, mViewHeight, newAngle);

        if (mIsAnimationEnabled) {
            final float scaleDiff = newScale - currentScale;

            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mImgAngle = newAngle % 360;
                    mCropScale = newScale;
                    doLayout(mViewWidth, mViewHeight);
                    mIsRotating = false;
                }

                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                    mIsRotating = true;
                }
            });
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    //動畫關(guān)鍵參數(shù)變化
                    float scale = (float) animation.getAnimatedValue();
                    mImgAngle = currentAngle + angleDiff * scale;
                    mCropScale = currentScale + scaleDiff * scale;
                    //核心旋轉(zhuǎn)縮放處理
                    setMatrix();
                    invalidate();
                }
            });
            mValueAnimator.setDuration(durationMillis);
            mValueAnimator.start();
        } else {
            //無動畫的情況
            mImgAngle = newAngle % 360;
            mCropScale = newScale;
            doLayout(mViewWidth, mViewHeight);
        }
    }

六、裁剪

裁剪功能其實最關(guān)鍵的是獲取裁剪后的Bitmap,注意要在工作者線程處理

    /**
     * 獲取裁剪后的圖片(<Strong>Notice:不包含翻轉(zhuǎn)變化</Strong>)
     *
     * @return
     */
    public Bitmap getCroppedBitmap() {
        Bitmap source = getBitmap();
        if (source == null)
            return null;
        Bitmap rotated = getRotatedBitmap(source);
        Rect cropRect = calcCropRect(source.getWidth(), source.getHeight());
        Bitmap cropped =
            Bitmap.createBitmap(rotated, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), null, false);
        if (rotated != cropped && rotated != source) {
            rotated.recycle();
        }
        if (mIsReverseY) {
            // 如果翻轉(zhuǎn)了照片,那么對照片進(jìn)行翻轉(zhuǎn)處理
            cropped = ImgUtils.reverseImg(cropped, -1, 1);
        }
        return cropped;
    }

1、對獲取的bitmap進(jìn)行旋轉(zhuǎn)處理

其實就是根據(jù)現(xiàn)在的旋轉(zhuǎn)角度對Bitmap就行Matrix的處理

    /**
     * 得到旋轉(zhuǎn)后的Bitmap
     *
     * @param bitmap
     * @return
     */
    private Bitmap getRotatedBitmap(Bitmap bitmap) {
        Matrix rotateMatrix = new Matrix();
        rotateMatrix.postRotate(mImgAngle, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, true);
    }

2、計算裁剪框相對原圖的區(qū)域

計算原圖進(jìn)行旋轉(zhuǎn)處理后相對原圖的寬高比例,然后通過變化得到裁剪框的旋轉(zhuǎn)后的比例


    private Rect calcCropRect(int originalImageWidth, int originalImageHeight) {
        //旋轉(zhuǎn)之后的寬
        float rotatedWidth = getRotatedWidth(mImgAngle, originalImageWidth, originalImageHeight);
        //旋轉(zhuǎn)之后的高
        float rotatedHeight = getRotatedHeight(mImgAngle, originalImageWidth, originalImageHeight);
        //旋轉(zhuǎn)后的相對旋轉(zhuǎn)前的比例
        float scaleForOriginal = rotatedWidth / mImageRectF.width();
        float offsetX = mImageRectF.left * scaleForOriginal;
        float offsetY = mImageRectF.top * scaleForOriginal;

        //對旋轉(zhuǎn)后的裁剪框的處理,
        int left = Math.round(mFrameRectF.left * scaleForOriginal - offsetX);
        int top = Math.round(mFrameRectF.top * scaleForOriginal - offsetY);
        int right = Math.round(mFrameRectF.right * scaleForOriginal - offsetX);
        int bottom = Math.round(mFrameRectF.bottom * scaleForOriginal - offsetY);

        int imageW = Math.round(rotatedWidth);
        int imageH = Math.round(rotatedHeight);
        return new Rect(Math.max(left, 0), Math.max(top, 0), Math.min(right, imageW), Math.min(bottom, imageH));
    }
3、對圖片進(jìn)行翻轉(zhuǎn)處理
if (mIsReverseY) {
            // 如果翻轉(zhuǎn)了照片,那么對照片進(jìn)行翻轉(zhuǎn)處理
            cropped = ImgUtils.reverseImg(cropped, -1, 1);
        }
4、拿到裁剪后的圖
 Bitmap cropped =
            Bitmap.createBitmap(rotated, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), null, false);

七、開放屬性Style

為了能在xml中自定義我們想要的屬性,針對開頭所說的額外考慮的部分和實際coding時所遇到的一些屬性進(jìn)行整理

得到屬性

<declare-styleable name="CropView">
        <attr name="img_src" format="reference" />
        <attr name="crop_mode">
            <enum name="fit_image" value="0" />
            <enum name="ratio_2_3" value="1" />
            <enum name="ratio_3_2" value="2" />
            <enum name="ratio_4_3" value="3" />
            <enum name="ratio_3_4" value="4" />
            <enum name="square" value="5" />
            <enum name="ratio_16_9" value="6" />
            <enum name="ratio_9_16" value="7" />
            <enum name="free" value="8" />

        </attr>
        <attr name="background_color" format="reference|color" />
        <attr name="overlay_color" format="reference|color" />
        <attr name="frame_color" format="reference|color" />
        <attr name="handle_color" format="reference|color" />
        <attr name="handle_width" format="dimension" />
        <attr name="handle_size" format="dimension" />
        <attr name="guide_color" format="reference|color" />
        <attr name="guide_show_mode">
            <enum name="show_always" value="1" />
            <enum name="show_on_touch" value="2" />
            <enum name="not_show" value="3" />
        </attr>
        <attr name="handle_show_mode">
            <enum name="show_always" value="1" />
            <enum name="show_on_touch" value="2" />
            <enum name="not_show" value="3" />
        </attr>
        <attr name="touch_padding" format="dimension" />
        <attr name="min_frame_size" format="dimension" />
        <attr name="frame_stroke_weight" format="dimension" />
        <attr name="guide_stroke_weight" format="dimension" />
        <attr name="crop_enabled" format="boolean" />
        <attr name="initial_frame_scale" format="float" />
    </declare-styleable>

加載屬性

 /**
     * 加載Style自定義屬性數(shù)據(jù)
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    private void loadStyleable(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropView, defStyleAttr, 0);
        Drawable drawable;
        mCropMode = CropModeEnum.SQUARE;
        try {
            drawable = ta.getDrawable(R.styleable.CropView_img_src);
            if (drawable != null)
                setImageDrawable(drawable);
            for (CropModeEnum mode : CropModeEnum.values()) {
                if (ta.getInt(R.styleable.CropView_crop_mode, 3) == mode.getID()) {
                    mCropMode = mode;
                    break;
                }
            }
            mBackgroundColor = ta.getColor(R.styleable.CropView_background_color, TRANSPARENT);
            mOverlayColor = ta.getColor(R.styleable.CropView_overlay_color, TRANSLUCENT_BLACK);
            mFrameColor = ta.getColor(R.styleable.CropView_frame_color, WHITE);
            mHandleColor = ta.getColor(R.styleable.CropView_handle_color, WHITE);
            mGuideColor = ta.getColor(R.styleable.CropView_guide_color, TRANSLUCENT_WHITE);
            for (ShowModeEnum mode : ShowModeEnum.values()) {
                if (ta.getInt(R.styleable.CropView_guide_show_mode, 1) == mode.getId()) {
                    mGuideShowMode = mode;
                    break;
                }
            }

            for (ShowModeEnum mode : ShowModeEnum.values()) {
                if (ta.getInt(R.styleable.CropView_handle_show_mode, 1) == mode.getId()) {
                    mHandleShowMode = mode;
                    break;
                }
            }
            setGuideShowMode(mGuideShowMode);
            setHandleShowMode(mHandleShowMode);
            mHandleSize = ta.getDimensionPixelSize(R.styleable.CropView_handle_size, SizeUtils.dp2px(HANDLE_SIZE));
            mHandleWidth = ta.getDimensionPixelSize(R.styleable.CropView_handle_width, SizeUtils.dp2px(HANDLE_WIDTH));
            mTouchPadding = ta.getDimensionPixelSize(R.styleable.CropView_touch_padding, 0);
            mFrameMinSize =
                    ta.getDimensionPixelSize(R.styleable.CropView_min_frame_size, SizeUtils.dp2px(FRAME_MIN_SIZE));
            mFrameStrokeWeight =
                    ta.getDimensionPixelSize(R.styleable.CropView_frame_stroke_weight, SizeUtils.dp2px(FRAME_STROKE_WEIGHT));
            mGuideStrokeWeight =
                    ta.getDimensionPixelSize(R.styleable.CropView_guide_stroke_weight, SizeUtils.dp2px(GUIDE_STROKE_WEIGHT));
            mIsCropEnabled = ta.getBoolean(R.styleable.CropView_crop_enabled, true);
            mInitialFrameScale =
                    constrain(ta.getFloat(R.styleable.CropView_initial_frame_scale, DEFAULT_INITIAL_SCALE), 0.01f, 1.0f,
                            DEFAULT_INITIAL_SCALE);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ta.recycle();
        }
    }

八、使用

使用第七節(jié)的屬性像ImageView引入即可

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

    <com.minminaya.crop.CropView
        android:id="@+id/crop_img"
        android:layout_width="match_parent"
        android:layout_height="583dp"
        android:layout_above="@+id/rv"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:padding="10dp"
        app:crop_enabled="true"
        app:crop_mode="free"
        app:background_color="#66FFFFFF"
        app:frame_color="@android:color/white"
        app:frame_stroke_weight="2dp"
        app:guide_color="#66FFFFFF"
        app:guide_show_mode="show_always"
        app:guide_stroke_weight="2dp"
        app:handle_color="@android:color/white"
        app:handle_show_mode="show_always"
        app:handle_size="24dp"
        app:handle_width="3dp"
        app:initial_frame_scale="1"
        app:min_frame_size="100dp"
        app:overlay_color="#AA1C1C1C"
        app:touch_padding="8dp" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="108dp"
        android:layout_alignParentBottom="true"
        android:background="@android:color/white" />

</RelativeLayout>

然后Activity中引入

public class MainActivity extends AppCompatActivity {

    private CropView mCropView;
    private RecyclerView mRecyclerView;
    private CropRecyclerViewAdapter mRecyclerViewAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = findViewById(R.id.rv);
        mCropView = findViewById(R.id.crop_img);
        mRecyclerViewAdapter = new CropRecyclerViewAdapter();

        mCropView.setImageResource(R.mipmap.test_pic);

        if (mRecyclerView != null) {
            mRecyclerViewAdapter = new CropRecyclerViewAdapter();
            // 數(shù)據(jù)來源
            mRecyclerViewAdapter.setCommonAdapterBean(handleRvAdapterData());
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
            mRecyclerView.setAdapter(mRecyclerViewAdapter);
            mRecyclerViewAdapter.setOnItemClickedListener(new CropRecyclerViewAdapter.OnItemClickedListener() {
                @Override
                public void onClicked(View view, int position) {
                    handleRvItemClicked(view, position);
                }
            });
        }
    }

    private CommonAdapterBean handleRvAdapterData() {
        CommonAdapterBean CommonAdapterBean = new CommonAdapterBean();
        List<Integer> funcPics = CommonAdapterBean.getFuncPics();
        List<String> funcNames = CommonAdapterBean.getFuncNames();
        String[] funcNameArrays =
                new String[] {Constant.CropBean.STR_ROTATION, Constant.CropBean.STR_REVERSION,
                        Constant.CropBean.STR_RATIO_FREE, Constant.CropBean.STR_RATIO_SQUARE,
                        Constant.CropBean.STR_RATIO_2_3, Constant.CropBean.STR_RATIO_3_2,
                        Constant.CropBean.STR_RATIO_3_4, Constant.CropBean.STR_RATIO_4_3,
                        Constant.CropBean.STR_RATIO_9_16, Constant.CropBean.STR_RATIO_16_9};
        for (String funcName : funcNameArrays) {
            funcPics.add(R.mipmap.ic_launcher);
            funcNames.add(funcName);
        }
        return CommonAdapterBean;
    }

    protected void handleRvItemClicked(View view, int position) {
        switch (position) {
            case Constant.CropBean.INDEX_ROTATION: {
                mCropView.rotateImage(CropView.RotateDegreesEnum.ROTATE_M90D);
                break;
            }
            case Constant.CropBean.INDEX_REVERSION: {
                mCropView.reverseY();
                break;
            }
            case Constant.CropBean.INDEX_RATIO_FREE: {
                mCropView.setCropMode(CropView.CropModeEnum.FREE);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_SQUARE: {
                mCropView.setCropMode(CropView.CropModeEnum.SQUARE);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_2_3: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_2_3);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_3_2: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_3_2);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_3_4: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_3_4);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_4_3: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_4_3);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_9_16: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_9_16);
                break;
            }
            case Constant.CropBean.INDEX_RATIO_16_9: {
                mCropView.setCropMode(CropView.CropModeEnum.RATIO_16_9);
                break;
            }
        }
    }
}

效果如下



源代碼

地址:https://github.com/minminaya/CropViewDemo


參考

Android自定義 view之圖片裁剪從設(shè)計到實現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容