1. 程式人生 > >圖片裁剪開源框架cropper原始碼解析

圖片裁剪開源框架cropper原始碼解析

這段時間工作內容不是很多,偶然間看到了圖片裁剪框架cropper,便對其產生了興趣,經過幾天的分析,由最初的丈二和尚到現在的深入瞭解也算是付出有所收穫吧,故在此進行學習記錄,不喜勿噴哈。

一、cropper說明文件部分翻譯

Class Overview
The Cropper is an image cropping tool. It provides a way to set an image in XML or programmatically, and displays a resizable crop window on top of the image. Calling the method getCroppedImage() will then return the Bitmap marked by the crop window.

譯1:cropper框架是一個圖片裁剪工具,它提供了一種在xml檔案或程式中對image圖片進行設定,同時在image表層顯示一個尺寸可動態變化的裁剪框。我們可以通過呼叫getCroppedImage()來獲取被裁剪框所標誌的Bitmap。

Developers can customize the following attributes (both via XML and programmatically):
1、appearance of guidelines in the crop window
2、whether the aspect ratio is fixed or not
3、aspect ratio (if the aspect ratio is fixed)
4、image resource

譯2:開發者可以自定義以下屬性(通過xml和程式碼均可)
1、控制裁剪框參考線的動態顯示
2、設定是否鎖定縱橫比
3、設定指定縱橫比(鎖定縱橫比的情況下)
4、設定image資原始檔

二、cropper框架使用

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    android:id="@+id/scrollview"
    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=".MainActivity" xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/content_padding"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/title" android:textSize="24sp" android:textStyle="bold"/> <com.edmodo.cropper.CropImageView android:id="@+id/CropImageView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/content_padding" android:adjustViewBounds="true" android:scaleType="centerInside" android:src="@drawable/butterfly"/> ... </ScrollView>

這個佈局xml檔案內容有點長,但內容非常簡單,我們只需要關注com.edmodo.cropper.CropImageView(這就是自定義的可供裁剪的View,繼承自ImageView)標籤即可,該標籤中指定了CropImageView的適配方式為centerInside(即將圖片的內容完整居中顯示),src圖片為@drawable/butterfly。
接下來再來分析MainActivity.java程式碼:


package com.example.croppersample;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.ToggleButton;

import com.edmodo.cropper.CropImageView;

public class MainActivity extends Activity {

    // Private Constants
    /**
    * 指定初始裁剪框的參考線的顯示模式:
    * 0:Off模式,參考線始終不顯示
    * 1:On Touch模式,裁剪框被觸控時顯示參考線,預設方式
    * 2:On模式,參考線一直顯示
    */
    private static final int GUIDELINES_ON_TOUCH = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        //ToggleButton,用於設定裁剪框是否鎖定縱橫比
        final ToggleButton fixedAspectRatioToggleButton = (ToggleButton) findViewById(R.id.fixedAspectRatioToggle);
        final TextView aspectRatioXTextView = (TextView) findViewById(R.id.aspectRatioX);
        //滑動條設定X軸的比例引數
        final SeekBar aspectRatioXSeekBar = (SeekBar) findViewById(R.id.aspectRatioXSeek);
        final TextView aspectRatioYTextView = (TextView) findViewById(R.id.aspectRatioY);
        //滑動條設定Y軸的比例引數
        final SeekBar aspectRatioYSeekBar = (SeekBar) findViewById(R.id.aspectRatioYSeek);
        final Spinner guidelinesSpinner = (Spinner) findViewById(R.id.showGuidelinesSpin);
        final CropImageView cropImageView = (CropImageView) findViewById(R.id.CropImageView);
        //獲取自定義裁剪View物件
        final ImageView croppedImageView = (ImageView) findViewById(R.id.croppedImageView);
        //圖片裁剪按鈕
        final Button cropButton = (Button) findViewById(R.id.Button_crop);
        fixedAspectRatioToggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                cropImageView.setFixedAspectRatio(isChecked);
                cropImageView.setAspectRatio(aspectRatioXSeekBar.getProgress(), aspectRatioYSeekBar.getProgress());
                aspectRatioXSeekBar.setEnabled(isChecked);
                aspectRatioYSeekBar.setEnabled(isChecked);
            }
        });

        // 初始設定X/Y軸的進度條均不可用
        aspectRatioXSeekBar.setEnabled(false);
        aspectRatioYSeekBar.setEnabled(false);

        aspectRatioXTextView.setText(String.valueOf(aspectRatioXSeekBar.getProgress()));
        aspectRatioYTextView.setText(String.valueOf(aspectRatioXSeekBar.getProgress()));


aspectRatioXSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar aspectRatioXSeekBar, int progress, boolean fromUser) {
                if (progress < 1) {
                    aspectRatioXSeekBar.setProgress(1);
                }

//設定裁剪框的X/Y軸的比例大小    
     cropImageView.setAspectRatio(aspectRatioXSeekBar.getProgress(), aspectRatioYSeekBar.getProgress());
                aspectRatioXTextView.setText(String.valueOf(aspectRatioXSeekBar.getProgress()));
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }
        });

        // Initialize aspect ratio Y SeekBar.
        aspectRatioYSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar aspectRatioYSeekBar, int progress, boolean fromUser) {
                if (progress < 1) {
                    aspectRatioYSeekBar.setProgress(1);
                }
                cropImageView.setAspectRatio(aspectRatioXSeekBar.getProgress(), aspectRatioYSeekBar.getProgress());
                aspectRatioYTextView.setText(String.valueOf(aspectRatioYSeekBar.getProgress()));
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }
        });

        // Set up the Guidelines Spinner.
        guidelinesSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                cropImageView.setGuidelines(i);
            }

            public void onNothingSelected(AdapterView<?> adapterView) {
                // Do nothing.
            }
        });
        //設定初始的參考線顯示模式
        guidelinesSpinner.setSelection(GUIDELINES_ON_TOUCH);

        // Initialize the Crop button.
        cropButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            //通過裁剪按鈕獲得裁剪到的bitmap,並進行顯示
                final Bitmap croppedImage = cropImageView.getCroppedImage();
                croppedImageView.setImageBitmap(croppedImage);
            }
        });
    }
}

通過以上程式碼我們可知,croppedImageView的裁剪分為兩種方式:第一種為鎖定縱橫比方式,分別通過指定的在X/Y軸的比例大小設定縱橫比,後續對裁剪框的大小調整便會參照此縱橫比;第二種為自由方式,即使用者可自由地進行裁剪框大小的設定,然後進行裁剪。
croppedImageView的主要方法如下:

//設定裁剪框是否保持縱橫比
public void setFixedAspectRatio(boolean fixAspectRatio) 

//設定指定的X/Y軸比例大小,縱橫比=X/Y,此時需要fixAspectRatio==true
public void setAspectRatio(int aspectRatioX, int aspectRatioY)

//設定參考線的顯示模式,模式說明參照前面介紹
public void setGuidelines(int guidelinesMode)

//獲得裁剪得到的Bitmap物件
public Bitmap getCroppedImage()

可以看到,CroppedImageView具有良好的封裝性,基本上我們只需通過以上幾個方法,便可實現對圖片的裁剪功能,是不是非常簡單和方便呢?總結為三步:
1、xml中引入CropImageView標籤;
2、獲得cropImageView物件,完成初始設定(鎖定縱橫比,參考線等);
3、呼叫getCroppedImage()獲取裁剪的bitmap物件;

三、cropper框架結構

這裡寫圖片描述
通過檢視原始碼,發現了一個設計很巧妙的地方——列舉(enum),對,列舉的使用,也在此膜拜一下作者大神,原始碼中在兩個地方用到了列舉:
1、edge包中的Edge,字面上可以猜測它和邊界有關,是的,該列舉中對裁剪框的四個邊界進行了總結,如下:

package com.edmodo.cropper.cropwindow.edge;

import android.graphics.RectF;
import android.support.annotation.NonNull;

import com.edmodo.cropper.util.AspectRatioUtil;

/**
 * Enum representing an edge in the crop window.
 */
public enum Edge {

    LEFT,  //裁剪框左邊界
    TOP,   //裁剪框上邊界
    RIGHT, //裁剪框右邊界
    BOTTOM;//裁剪框下邊界

    private float mCoordinate;  //邊界座標
    ...

可以看到每個Edge物件中都維持了一個mCoordinate區域性變數,這個變數非常重要,而且規定了當為Edge.LEFT或Edge.RIGHT時mCoordinate代表X方向橫座標,當為Edge.TOP或Edge.BOTTOM時代表Y方向縱座標,可以思考一下為什麼要這樣規定呢?之所以要定義出四個邊界的列舉,是為了確定出裁剪框的大小和座標位置,通過上面的的規定,即知道了左右邊界的X座標和上下邊界Y座標,似乎是能夠確定出裁剪框的大小和座標位置的。答案是肯定的,因為四個邊框的交接點的座標確定了下來,故我們只要知道了左上(left-top)座標和右下(right-bottom)座標便能確定出裁剪框的尺寸大小和位置座標了。

另外再看一下edge包中的另一個類EdgePair,其程式碼很短,也很簡單:

package com.edmodo.cropper.cropwindow.edge;

/**
 * Simple class to hold a pair of Edges.
 */
public class EdgePair {

    public Edge primary;     //X軸邊界
    public Edge secondary;   //Y軸邊界

    // Constructor 
    public EdgePair(Edge edge1, Edge edge2) {
        primary = edge1;
        secondary = edge2;
    }
}

可以看到,EdgePair就是包含兩個Edge的簡單集合,關於它的作用,將在後面進行說明。

2、handle包中的Handle,也可以從字面上猜測它和處理有關,這個列舉中定義了對裁剪框的所有有效觸控型別,如觸控內部、觸控四個邊界、觸控四個邊角共9中方式,先來看看它的原始碼:

package com.edmodo.cropper.cropwindow.handle;

import android.graphics.RectF;
import android.support.annotation.NonNull;
import com.edmodo.cropper.cropwindow.edge.Edge;

public enum Handle {

    //觸控左上角
    TOP_LEFT(new CornerHandleHelper(Edge.TOP, Edge.LEFT)),
    //觸控右上角
    TOP_RIGHT(new CornerHandleHelper(Edge.TOP, Edge.RIGHT)),
    //觸控左下角
    BOTTOM_LEFT(new CornerHandleHelper(Edge.BOTTOM, Edge.LEFT)),
    //觸控右下角
    BOTTOM_RIGHT(new CornerHandleHelper(Edge.BOTTOM, Edge.RIGHT)),
    //觸控左邊界
    LEFT(new VerticalHandleHelper(Edge.LEFT)),
    //觸控上邊界
    TOP(new HorizontalHandleHelper(Edge.TOP)),
    //觸控右邊界
    RIGHT(new VerticalHandleHelper(Edge.RIGHT)),
    //觸控下邊界
    BOTTOM(new HorizontalHandleHelper(Edge.BOTTOM)),
    //觸控裁剪框內部
    CENTER(new CenterHandleHelper());

    //HandleHelper為抽象類,定義了對不同觸控方式的處理
    private HandleHelper mHelper;

    //建構函式必須傳入一個觸控方式的處理類HandleHelper
    Handle(HandleHelper helper) {
        mHelper = helper;
    }

    //非鎖定縱橫比下,對觸控方式的響應,重新整理裁剪框顯示
    public void updateCropWindow(float x,
                                 float y,
                                 @NonNull RectF imageRect,
                                 float snapRadius) {

        mHelper.updateCropWindow(x, y, imageRect, snapRadius);
    }

    //鎖定縱橫比下,對觸控方式的響應,重新整理裁剪框顯示
    public void updateCropWindow(float x,
                                 float y,
                                 float targetAspectRatio,
                                 @NonNull RectF imageRect,
                                 float snapRadius) {

        mHelper.updateCropWindow(x, y, targetAspectRatio, imageRect, snapRadius);
    }
}

前面把handle類理解為和處理有關其實是不太準確的,在這裡更正一下,通過原始碼我們可以發現handle應該是理解為 待處理的觸控型別 物件,共有9種,真正的觸控處理是由HandleHelper物件完成的,該類為抽象類,這樣是為了保證不同 待處理的觸控方式 有不同的觸控處理動作。舉個栗子:當我們在觸控裁剪框內部時,觸控處理動作是裁剪框隨著手指的移動而移動,裁剪框本身大小不會變化;當我們觸控裁剪框左邊界時,觸控處理動作是裁剪框的左邊界隨著手指的移動而移動,裁剪框的大小會發生變化;當我們觸控裁剪框左上角時,觸控處理動作是裁剪框的左邊界和上邊界隨著手指的移動而移動,裁剪框的大小也會發生變化。
所以,我們可以看到抽象類HandleHelper有四個繼承子類,分別是CenterHandleHelper、CornerHandleHelper、HorizontalHandleHelper、VerticalHandleHelper,對應著不同的觸控處理動作。
這裡寫圖片描述

接下來就是util包了,主要包含四個工具類:AspectRadioUtil/HandleUtil/MathUtil/PaintUtil,通過字面上就能夠知道它們的作用了吧,下面在分析CropImageView原始碼時會逐一使用到。

四、cropper原始碼分析

cropper原始碼的主要體現為CropImageView類,它是整個框架的核心類,該類繼承自ImageView,在ImageView的基礎上增加了裁剪框的顯示、拖拽和裁剪功能,下面就結合其原始碼進行分析:
首先看看Constructor

public CropImageView(Context context) {
        super(context);
        init(context, null);
    }

    public CropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public CropImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

三個構造方法都呼叫了init(context, attrs)方法,主要完成一些初始化的設定

private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
        //獲取自定義屬性
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
        //分割線顯示模式
        mGuidelinesMode = typedArray.getInteger(R.styleable.CropImageView_guidelines, 1);
        //是否鎖定縱橫比
        mFixAspectRatio = typedArray.getBoolean(R.styleable.CropImageView_fixAspectRatio, false);
        //縱橫比X軸比例大小
        mAspectRatioX = typedArray.getInteger(R.styleable.CropImageView_aspectRatioX, 1);
        //縱橫比Y軸比例大小
        mAspectRatioY = typedArray.getInteger(R.styleable.CropImageView_aspectRatioY, 1);
        typedArray.recycle();

        final Resources resources = context.getResources();
        //描繪邊界的畫筆
        mBorderPaint = PaintUtil.newBorderPaint(resources);
        //描繪參考線的畫筆
        mGuidelinePaint = PaintUtil.newGuidelinePaint(resources);
        //描繪半透明蒙版(CropImageView之內裁剪框之外)的畫筆
        mSurroundingAreaOverlayPaint = PaintUtil.newSurroundingAreaOverlayPaint(resources);
        //描繪倒角的畫筆
        mCornerPaint = PaintUtil.newCornerPaint(resources);
        //手指觸點距離裁剪框範圍偏差
        mHandleRadius = resources.getDimension(R.dimen.target_radius);   
        //手指觸點距離CropImageView邊界偏差
        mSnapRadius = resources.getDimension(R.dimen.snap_radius);

        //描邊寬度
        mBorderThickness = resources.getDimension(R.dimen.border_thickness);

        //倒角寬度
        mCornerThickness = resources.getDimension(R.dimen.corner_thickness);
        //倒角長度
        mCornerLength = resources.getDimension(R.dimen.corner_length);
    }

相關注釋在原始碼中都已標註,其中比較難以理解的是mHandleRadius和mSnapRadius,這兩個變數代表偏差的意思,我們知道在用手指觸控手機螢幕時,由於手指和螢幕是大面積接觸,在計算接觸點的時候是存在一定誤差的,所以在處理時需要一定的方法來抵消掉這種誤差。mHandleRadius就是用於消除手指觸控裁剪框(包括邊界、倒角和內部)時的誤差,mSnapRadius是用於當手指拖拽裁剪框到接近(未到達)CropImageView邊界時,使得裁剪框的邊界和CropImageView的邊界重合。

接下來就是onLayout()方法了,CropImageView對該方法進行了複寫:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        super.onLayout(changed, left, top, right, bottom);
        //獲取CropImageView的座標資訊(left,top,right,bottom),保存於mBitmapRect中
        mBitmapRect = getBitmapRect();
        //初始化裁剪框
        initCropWindow(mBitmapRect);
    }

再來看看initCropWindow(mBitmapRect)方法:

private void initCropWindow(@NonNull RectF bitmapRect) {
            //鎖定縱橫比
            if (mFixAspectRatio) {
                initCropWindowWithFixedAspectRatio(bitmapRect);

            } else {  //未鎖定縱橫比
                final float horizontalPadding = 0.1f * bitmapRect.width();
                final float verticalPadding = 0.1f * bitmapRect.height();

                Edge.LEFT.setCoordinate(bitmapRect.left + horizontalPadding);
                Edge.TOP.setCoordinate(bitmapRect.top + verticalPadding);
                Edge.RIGHT.setCoordinate(bitmapRect.right - horizontalPadding);
                Edge.BOTTOM.setCoordinate(bitmapRect.bottom - verticalPadding);
            }
        }

可以看到裁剪框的初始化分為兩種情況,主要功能是完成對裁剪框的四邊界(Edge)座標進行賦值,例如未鎖定縱橫比的情況下,設定的邊界尺寸是CropImageView的對應邊界值減去預設的內邊距(padding,左右padding為寬度的1/10,上下padding為高度的1/10),後續的對於裁剪框的繪製使用的都是邊界(Edge)座標。

再接下來就是重要的onDraw()方法了

@Override
    protected void onDraw(Canvas canvas) {
        //呼叫父類繪製方法
        super.onDraw(canvas);

        /**
        * 下面四步完成裁剪框的繪製;
        * 1、繪製半透明蒙版效果
        * 2、繪製參考線
        * 3、繪製邊界
        * 4、繪製倒角
        */
        drawDarkenedSurroundingArea(canvas);
        drawGuidelines(canvas);
        drawBorder(canvas);
        drawCorners(canvas);
    }

關於每一步的繪製過程不再過多分析,只是強調一點,每一步用到的座標資料都來自於onLayout()中儲存至Edge的座標值。~~太嘮叨了…

最後分析一下最最重要的onTouchEvent(MotionEvent event)方法

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (!isEnabled()) {
            return false;
        }

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                onActionDown(event.getX(), event.getY());
                return true;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                onActionUp();
                return true;

            case MotionEvent.ACTION_MOVE:
                onActionMove(event.getX(), event.getY());
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;

            default:
                return false;
        }
    }

首先在MotionEvent.ACTION_DOWN中呼叫了onActionDown(event.getX(), event.getY())方法,看看裡面做了哪些處理

private void onActionDown(float x, float y) {
        //獲取裁剪框的左上角和右下角座標
        final float left = Edge.LEFT.getCoordinate();
        final float top = Edge.TOP.getCoordinate();
        final float right = Edge.RIGHT.getCoordinate();
        final float bottom = Edge.BOTTOM.getCoordinate();

        //根據手指觸點座標和裁剪框座標以及可允許誤差mHandleRadius判斷是哪種觸控種類(前面總結的9種中的一種)並返回
        mPressedHandle = HandleUtil.getPressedHandle(x, y, left, top, right, bottom, mHandleRadius);

        //如果獲取的觸控種類不為空,獲取其偏移量(x,y方向)
        if (mPressedHandle != null) {
            HandleUtil.getOffset(mPressedHandle, x, y, left, top, right, bottom, mTouchOffset);
            invalidate();
        }
    }

這裡解釋一下為什麼要獲取一個偏移量mTouchOffset(PointF型別),這個偏移量用於後續對裁剪框進行拖拽或大小改變時的座標補償,因為在獲取觸控種類時使用了mHandleRadius作為允許的偏差,所以在這個偏差範圍內的觸點誤差是需要補償回來的,不然會導致拖拽裁剪框的時候會有“一跳”的現象,影響介面友好性。到這裡你也許會問為什麼要引入mHandleRadius這樣一個偏差引數,如果不引入的話就不需要進行誤差補償了,對的,理論上就應該是這樣的。但是這樣的話,對使用者的要求就非常高了,如果沒有可允許偏差mHandleRadius,只有使用者非常精確地按下裁剪框的特殊位置(如邊界和邊角處),程式才會返回特定的觸控型別,這樣使用者在使用的時候是會被逼瘋的…

接下來就是MotionEvent.ACTION_MOVE中的onActionMove(event.getX(), event.getY())方法了

private void onActionMove(float x, float y) {

        if (mPressedHandle == null) {
            return;
        }

        //x,y座標分別通過mTouchOffset進行誤差補償
        x += mTouchOffset.x;
        y += mTouchOffset.y;

        //鎖定縱橫比
        if (mFixAspectRatio) {
            mPressedHandle.updateCropWindow(x, y, getTargetAspectRatio(), mBitmapRect, mSnapRadius);
        } else {  //非鎖定縱橫比
            mPressedHandle.updateCropWindow(x, y, mBitmapRect, mSnapRadius);
        }
        invalidate();
    }

再次以非鎖定縱橫比的情況進行分析,原始碼中可以看到在完成座標補償後,便對特定觸控型別進行了更新座標的操作,這裡以相對複雜的觸控左上倒角為例(mPressedHandle==Handle.TOP_LEFT),分析其中的處理過程:
mPressedHandle.updateCropWindow(x, y, mBitmapRect, mSnapRadius)會呼叫至mHelper.updateCropWindow(x, y, imageRect, snapRadius)方法,翻看其原始碼

void updateCropWindow(float x,
                          float y,
                          @NonNull RectF imageRect,
                          float snapRadius) {
        //獲取EdgePair(邊界對)物件
        final EdgePair activeEdges = getActiveEdges();
        //返回第一個邊界
        final Edge primaryEdge = activeEdges.primary;
        //返回第二個邊界
        final Edge secondaryEdge = activeEdges.secondary;

        if (primaryEdge != null)
            primaryEdge.adjustCoordinate(x, y, imageRect, snapRadius, UNFIXED_ASPECT_RATIO_CONSTANT);

        if (secondaryEdge != null)
            secondaryEdge.adjustCoordinate(x, y, imageRect, snapRadius, UNFIXED_ASPECT_RATIO_CONSTANT);
    }

可以看到前面提到的EdgePair在這裡使用到了,其作用就是儲存了兩條邊界,即在構造TOP_LEFT(new CornerHandleHelper(Edge.TOP, Edge.LEFT))時傳入的兩條邊界,當用戶拖拽倒角的時候分別呼叫兩條邊的adjustCoordinate(…)方法進行座標更新。

待座標更新完成後再進行重繪。requestDisallowInterceptTouchEvent(true)方法是保證父類的touch事件能夠傳遞下來。

然後就是MotionEvent.ACTION_UP和MotionEvent.ACTION_CANCEL的onActionUp()方法了

private void onActionUp() {
        if (mPressedHandle != null) {
            mPressedHandle = null;
            invalidate();
        }
    }

很簡單吧,就是進行觸控完成後的清理工作。

最後便是裁剪操作了,再分析一下其原始碼

public Bitmap getCroppedImage() {

        final Drawable drawable = getDrawable();
        if (drawable == null || !(drawable instanceof BitmapDrawable)) {
            return null;
        }

        final float[] matrixValues = new float[9];
        getImageMatrix().getValues(matrixValues);

        final float scaleX = matrixValues[Matrix.MSCALE_X];
        final float scaleY = matrixValues[Matrix.MSCALE_Y];
        final float transX = matrixValues[Matrix.MTRANS_X];
        final float transY = matrixValues[Matrix.MTRANS_Y];

        final float bitmapLeft = (transX < 0) ? Math.abs(transX) : 0;
        final float bitmapTop = (transY < 0) ? Math.abs(transY) : 0;

        //獲取原始的bitmap
        final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
        //獲取X軸裁剪的起始座標
        final float cropX = (bitmapLeft + Edge.LEFT.getCoordinate()) / scaleX;
        //獲取Y軸裁剪的起始座標
        final float cropY = (bitmapTop + Edge.TOP.getCoordinate()) / scaleY;

        //獲取裁剪寬度
        final float cropWidth = Math.min(Edge.getWidth() / scaleX, originalBitmap.getWidth() - cropX);
        //獲取裁剪高度
        final float cropHeight = Math.min(Edge.getHeight() / scaleY, originalBitmap.getHeight() - cropY);
        //返回裁剪後的bitmap
        return Bitmap.createBitmap(originalBitmap,
                                   (int) cropX,
                                   (int) cropY,
                                   (int) cropWidth,
                                   (int) cropHeight);
    }

由於裁剪的物件是原始的bitmap(即未經縮放處理),而裁剪邊界是經過縮放處理後的值,所以需要對裁剪邊界的座標或寬高進行等比例的放大,最後形成的起始座標和寬高才是對原始bitmap進行裁剪操作。

至此,關於cropper框架的分析過程基本就完成了。歡迎踴躍拍磚。。。