图片裁剪开源框架cropper源码解析

这段时间工作内容不是很多,偶然间看到了图片裁剪框架cropper,便对其产生了兴趣,经过几天的分析,由最初的丈二和尚到现在的深入了解也算是付出有所收获吧,故在此进行学习记录,不喜勿喷哈。

一、cropper说明文档部分翻译

github地址:https://github.com/edmodo/cropper/wiki
图片裁剪开源框架cropper源码解析_第1张图片

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框架使用

参考对源码工程(https://github.com/edmodo/cropper)中CropperSimple示例代码的分析,介绍该工具的使用:


<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框架结构

图片裁剪开源框架cropper源码解析_第2张图片
通过查看源码,发现了一个设计很巧妙的地方——枚举(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,对应着不同的触摸处理动作。
图片裁剪开源框架cropper源码解析_第3张图片

接下来就是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框架的分析过程基本就完成了。欢迎踊跃拍砖。。。

你可能感兴趣的:(android)