这段时间工作内容不是很多,偶然间看到了图片裁剪框架cropper,便对其产生了兴趣,经过几天的分析,由最初的丈二和尚到现在的深入了解也算是付出有所收获吧,故在此进行学习记录,不喜勿喷哈。
github地址:https://github.com/edmodo/cropper/wiki
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资源文件
参考对源码工程(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对象;
通过查看源码,发现了一个设计很巧妙的地方——枚举(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源码的主要体现为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框架的分析过程基本就完成了。欢迎踊跃拍砖。。。