神刀安全网

android-crop源码解析

android-crop源码解析

整个界面分为上面的两个 Button,显示的 CropImageView,裁剪显示区域的 HighlightView。通过拖拉已经放大缩小 HighlightView 来确定裁剪区域。同时当 HighlightView 缩小的一定值的时候, CropImageView 也会缩小。

使用

引入包:

compile 'com.soundcloud.android:android-crop:1.0.1@aar'

AndroidManifest.xml 中申明 CropImageActivity :

<activity android:name="com.soundcloud.android.crop.CropImageActivity" />

选择想要裁剪的图片(选择图片的这个过程也可以自己完成):

Crop.pickImage(activity)

之后会返回到 onActivityResult 中:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == Crop.REQUEST_PICK && resultCode == RESULT_OK) {
beginCrop(result.getData());
}
}

执行裁剪:

private void beginCrop(Uri source) {
Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped"));//temp url
Crop.of(source, destination).asSquare().start(activity);
}

裁剪完成:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == Crop.REQUEST_CROPK) {
handleCrop(resultCode, result);
}
}

源码解析

选择图片

先从选择图片开始看,这个地方也就是通过 Intent 的 type 为 image/* 去做操作:

public class Crop {
/**
* Pick image from an Activity
* 通过Activity的方式启动
*
* @param activity Activity to receive result
*/

public static void pickImage(Activity activity) {
pickImage(activity, REQUEST_PICK);
}

/**
* Pick image from an Activity with a custom request code
*
* @param activity Activity to receive result
* @param requestCode requestCode for result
*/

public static void pickImage(Activity activity, int requestCode) {
try {
activity.startActivityForResult(getImagePicker(), requestCode);
} catch (ActivityNotFoundException e) {
showImagePickerError(activity);
}
}

private static Intent getImagePicker() {
return new Intent(Intent.ACTION_GET_CONTENT).setType("image/*");
}

private static void showImagePickerError(Context context) {
Toast.makeText(context, R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
}

}

Crop 中不仅仅提供了 Activity 的方式,同时还提供了 android.app.Fragmentandroid.support.v4.app.Fragment 的方式。

裁图前的准备

先从入口开始看:

Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped"));
Crop.of(source, destination).asSquare().start(this);

传入的 destination 是裁剪完之后保存的地址。

public class Crop {
interface Extra {
String ASPECT_X = "aspect_x";
String ASPECT_Y = "aspect_y";
String MAX_X = "max_x";
String MAX_Y = "max_y";
String ERROR = "error";
}

private Intent cropIntent;

/**
* Create a crop Intent builder with source and destination image Uris
*
* @param source Uri for image to crop
* @param destination Uri for saving the cropped image
*/

public static Crop of(Uri source, Uri destination) {
return new Crop(source, destination);
}

private Crop(Uri source, Uri destination) {
cropIntent = new Intent();
cropIntent.setData(source);
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination);
}

/**
* Crop area with fixed 1:1 aspect ratio
* X:Y 的比例为 1:1
*/

public Crop asSquare() {
cropIntent.putExtra(Extra.ASPECT_X, 1);
cropIntent.putExtra(Extra.ASPECT_Y, 1);
return this;
}

/**
* Send the crop Intent from an Activity
*
* @param activity Activity to receive result
*/

public void start(Activity activity) {
start(activity, REQUEST_CROP);
}

/**
* Send the crop Intent from an Activity with a custom request code
*
* @param activity Activity to receive result
* @param requestCode requestCode for result
*/

public void start(Activity activity, int requestCode) {
activity.startActivityForResult(getIntent(activity), requestCode);
}

/**
* Get Intent to start crop Activity
*
* @param context Context
* @return Intent for CropImageActivity
*/

public Intent getIntent(Context context) {
cropIntent.setClass(context, CropImageActivity.class);
return cropIntent;
}
}

通过调用 Crop.of(source, destination).asSquare().start(this) 进入到 CropImageActivity 这个界面中。其中还经历了许多的配置,比如初始化的时候裁图显示的X/Y比例是多少等。

那么进入到 CropImageActivity 中看看:

public class CropImageActivity extends MonitoredActivity {
private static final int SIZE_DEFAULT = 2048;
private static final int SIZE_LIMIT = 4096;

private int aspectX;
private int aspectY;

// Output image
private int maxX;
private int maxY;
private int exifRotation;

private Uri sourceUri;//要裁图的图片的uri
private Uri saveUri;//裁完图的图片要保存的地址

private final Handler handler = new Handler();

private CropImageView imageView;//显示bitmap的View
private HighlightView cropView;//显示裁剪的View

@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setupWindowFlags();//初始化界面
setupViews();//初始化界面里的控件

loadInput();//加载传入的数据
if (rotateBitmap == null) {
finish();
return;
}
startCrop();//开始裁剪
}

/**
* 加载数据
*
* @return
*/

private void loadInput() {
Intent intent = getIntent();
Bundle extras = intent.getExtras();

if (extras != null) {
aspectX = extras.getInt(Crop.Extra.ASPECT_X);
aspectY = extras.getInt(Crop.Extra.ASPECT_Y);
maxX = extras.getInt(Crop.Extra.MAX_X);
maxY = extras.getInt(Crop.Extra.MAX_Y);
saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT);
}

sourceUri = intent.getData();
if (sourceUri != null) {
exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri));//通过图片的Exif信息得到要选择多少度

InputStream is = null;
try {
sampleSize = calculateBitmapSampleSize(sourceUri);//计算图片在decode的时候的sampleSize
is = getContentResolver().openInputStream(sourceUri);
BitmapFactory.Options option = new BitmapFactory.Options();
option.inSampleSize = sampleSize;
//decode出通过exif信息旋转之后,通过option的simpleSize采样缩放之后的bitmap,赋值给成员变量rotateBitmap
rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation);
} catch (IOException e) {
Log.e("Error reading image: " + e.getMessage(), e);
setResultException(e);
} catch (OutOfMemoryError e) {
Log.e("OOM reading image: " + e.getMessage(), e);
setResultException(e);
} finally {
CropUtil.closeSilently(is);
}
}
}

/**
* 计算simpleSize
*
* @return
*/

private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException {
InputStream is = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try {
is = getContentResolver().openInputStream(bitmapUri);
BitmapFactory.decodeStream(is, null, options); //bitmap的高宽都存到了options中
} finally {
CropUtil.closeSilently(is);//关流
}

int maxSize = getMaxImageSize();//得到最大的显示size
int sampleSize = 1;
//通过bitmap的高或者宽除以simpleSize得到的值与maxSize对比,如果还大的话,simpleSize向右移动一位,就是乘以2,如果都不大于maxSize的话,就选定当前这个值
while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) {
sampleSize = sampleSize << 1;
}
return sampleSize;
}

/**
* 得到最大的Size
*
* @return
*/

private int getMaxImageSize() {
int textureLimit = getMaxTextureSize();
if (textureLimit == 0) {
return SIZE_DEFAULT;//2048
} else {
return Math.min(textureLimit, SIZE_LIMIT);//4096
}
}

/**
* OpenGL的texture的大小是ImageView最大绘制的大小
*
* @return
*/

private int getMaxTextureSize() {
// The OpenGL texture size is the maximum size that can be drawn in an ImageView
int[] maxSize = new int[1];
GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0);
return maxSize[0];
}

private void startCrop() {
if (isFinishing()) {
return;
}
//设置bitmap
imageView.setImageRotateBitmapResetBase(rotateBitmap, true);
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait),
new Runnable() {
public void run() {
//线程计数器
final CountDownLatch latch = new CountDownLatch(1);
//主线程进行UI操作
handler.post(new Runnable() {
public void run() {
if (imageView.getScale() == 1F) {
imageView.center();
}
latch.countDown();
}
});
//子线程阻塞住,直到latch.countDown();执行
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//设置裁剪View
new Cropper().crop();
}
}, handler
);
}

private class Cropper {
public void crop() {
handler.post(new Runnable() {
public void run() {
makeDefault();//设置默认值
imageView.invalidate();//更新绘制
if (imageView.highlightViews.size() == 1) {
cropView = imageView.highlightViews.get(0);
cropView.setFocus(true);
}
}
});
}

private void makeDefault() {
if (rotateBitmap == null) {
return;
}

HighlightView hv = new HighlightView(imageView);//这个imageview就是CropImageView
final int width = rotateBitmap.getWidth();
final int height = rotateBitmap.getHeight();

Rect imageRect = new Rect(0, 0, width, height);

// Make the default size about 4/5 of the width or height
int cropWidth = Math.min(width, height) * 4 / 5;//要显示裁剪的view的宽度是bitmap的宽度或高度中的最小值的4/5
@SuppressWarnings("SuspiciousNameCombination")
int cropHeight = cropWidth;

if (aspectX != 0 && aspectY != 0) {
if (aspectX > aspectY) {//根据传入的X:Y的比例进行设置
cropHeight = cropWidth * aspectY / aspectX;
} else {
cropWidth = cropHeight * aspectX / aspectY;
}
}

int x = (width - cropWidth) / 2;//裁剪view的x坐标
int y = (height - cropHeight) / 2;//裁剪view的y坐标

RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0);//初始化HighlightView
imageView.add(hv);
}
}
}

startCrop 中做的 job 是以下操作,主要是通过 Activity 生命周期来显示或者隐藏 Dialog 和在子线程中执行 job 操作:

class CropUtil {
/**
* 该方法主要操作就是进行耗时操作前后显示dialog,同时监听Activity的生命去显示和隐藏dialog
*
* @param activity
* @param title
* @param message
* @param job
* @param handler
*/

public static void startBackgroundJob(MonitoredActivity activity, String title, String message, Runnable job, Handler handler) {
// Make the progress dialog uncancelable, so that we can guarantee
// the thread will be done before the activity getting destroyed
ProgressDialog dialog = ProgressDialog.show(
activity, title, message, true, false);
new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
}

//MonitoredActivity.LifeCycleAdapter是Activity的生命周期监听的一个Adapter
private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable {

private final MonitoredActivity activity;
private final ProgressDialog dialog;
private final Runnable job;
private final Handler handler;//主线程的handler
private final Runnable cleanupRunner = new Runnable() {
public void run() {
//从activity中remove掉,防止泄露
activity.removeLifeCycleListener(BackgroundJob.this);
if (dialog.getWindow() != null) dialog.dismiss();
}
};

public BackgroundJob(MonitoredActivity activity, Runnable job,
ProgressDialog dialog, Handler handler)
{

this.activity = activity;
this.dialog = dialog;
this.job = job;
this.activity.addLifeCycleListener(this);
this.handler = handler;
}

public void run() {
try {
//执行job
job.run();
} finally {
handler.post(cleanupRunner);
}
}

@Override
public void onActivityDestroyed(MonitoredActivity activity) {
// We get here only when the onDestroyed being called before
// the cleanupRunner. So, run it now and remove it from the queue
cleanupRunner.run();
handler.removeCallbacks(cleanupRunner);
}

@Override
public void onActivityStopped(MonitoredActivity activity) {
//当Activity的状态为stop的时候,将dialog隐藏掉
dialog.hide();
}

@Override
public void onActivityStarted(MonitoredActivity activity) {
//当Activity的状态为started的时候,将dialog显示出来
dialog.show();
}
}
}

真正裁剪

当点击『完成』时候:

public class CropImageActivity extends MonitoredActivity {
private void onSaveClicked() {
if (cropView == null || isSaving) {
return;
}
isSaving = true;

Bitmap croppedImage;
//得到HighlightView的Rect大小
Rect r = cropView.getScaledCropRect(sampleSize);
int width = r.width();
int height = r.height();

int outWidth = width;
int outHeight = height;
//如果intent传参传的是maxX和maxY的话,这里就需要进行判断
//如果裁剪出的width大于maxX或者裁剪出的height大于maxY,则进行比例缩放
if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) {
float ratio = (float) width / (float) height;
if ((float) maxX / (float) maxY > ratio) {
outHeight = maxY;
outWidth = (int) ((float) maxY * ratio + .5f);
} else {
outWidth = maxX;
outHeight = (int) ((float) maxX / ratio + .5f);
}
}

try {
//得到裁剪之后的Bitmap
croppedImage = decodeRegionCrop(r, outWidth, outHeight);
} catch (IllegalArgumentException e) {
setResultException(e);
finish();
return;
}

if (croppedImage != null) {
//在CropImageView中显示裁剪了的bitmap
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
imageView.center();
imageView.highlightViews.clear();
}
//保存裁剪之后的bitmap
saveImage(croppedImage);
}

private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) {
// Release memory now
//释放CropImageView中的bitmap
clearImageView();

InputStream is = null;
Bitmap croppedImage = null;
try {
is = getContentResolver().openInputStream(sourceUri);
//得到BitmapRegionDecoder
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
final int width = decoder.getWidth();
final int height = decoder.getHeight();
//如果exif信息中要旋转
if (exifRotation != 0) {
// Adjust crop area to account for image rotation
Matrix matrix = new Matrix();
matrix.setRotate(-exifRotation);
//adjusted为旋转后的rect
RectF adjusted = new RectF();
matrix.mapRect(adjusted, new RectF(rect));

// Adjust to account for origin at 0,0
adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0);
rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom);
}

try {
//裁剪rect区域的图像
croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options());
//裁剪的宽度或高度大于想要的宽度或高度的话,通过矩阵进行缩放,然后再创建出一个想要高宽的bitmap
if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) {
Matrix matrix = new Matrix();
matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height());
croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true);
}
} catch (IllegalArgumentException e) {
// Rethrow with some extra information
throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image ("
+ width + "," + height + "," + exifRotation + ")", e);
}

} catch (IOException e) {
Log.e("Error cropping image: " + e.getMessage(), e);
setResultException(e);
} catch (OutOfMemoryError e) {
Log.e("OOM cropping image: " + e.getMessage(), e);
setResultException(e);
} finally {
CropUtil.closeSilently(is);
}
return croppedImage;
}

/**
* 回收之前没有裁剪的bitmap
*/

private void clearImageView() {
imageView.clear();
if (rotateBitmap != null) {
rotateBitmap.recycle();
}
System.gc();
}

private void saveImage(Bitmap croppedImage) {
if (croppedImage != null) {
final Bitmap b = croppedImage;
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving),
new Runnable() {
public void run() {
saveOutput(b);
}
}, handler
);
} else {
finish();
}
}

/**
* 保存bitmap
*
* @param croppedImage
*/

private void saveOutput(Bitmap croppedImage) {
if (saveUri != null) {
OutputStream outputStream = null;
try {
outputStream = getContentResolver().openOutputStream(saveUri);
if (outputStream != null) {
croppedImage.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
}
} catch (IOException e) {
setResultException(e);
Log.e("Cannot open file: " + saveUri, e);
} finally {
CropUtil.closeSilently(outputStream);
}

CropUtil.copyExifRotation(
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri),
CropUtil.getFromMediaUri(this, getContentResolver(), saveUri)
);

setResultUri(saveUri);
}

final Bitmap b = croppedImage;
handler.post(new Runnable() {
public void run() {
imageView.clear();
b.recycle();
}
});

finish();
}
}

裁剪是通过 BitmapRegionDecoder 来实现的, BitmapRegionDecoder.newInstance(InputStream is, boolean isShareable) ,传入的参数是一个 InputStream ,这样做的好处是可以减少创建出原图大小的 Bitmap ,达到节省内存, 有效的防止 OOM

RotateBitmap

class RotateBitmap {

private Bitmap bitmap;
private int rotation;

public RotateBitmap(Bitmap bitmap, int rotation) {
this.bitmap = bitmap;
this.rotation = rotation % 360;
}

public void setRotation(int rotation) {
this.rotation = rotation;
}

public int getRotation() {
return rotation;
}

public Bitmap getBitmap() {
return bitmap;
}

public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}

//得到旋转的矩阵
public Matrix getRotateMatrix() {
// By default this is an identity matrix
Matrix matrix = new Matrix();
if (bitmap != null && rotation != 0) {
// We want to do the rotation at origin, but since the bounding
// rectangle will be changed after rotation, so the delta values
// are based on old & new width/height respectively.
int cx = bitmap.getWidth() / 2;
int cy = bitmap.getHeight() / 2;
matrix.preTranslate(-cx, -cy);
matrix.postRotate(rotation);
matrix.postTranslate(getWidth() / 2, getHeight() / 2);
}
return matrix;
}

public boolean isOrientationChanged() {
return (rotation / 90) % 2 != 0;
}

//得到高度,要判断exif中是否要旋转
public int getHeight() {
if (bitmap == null) return 0;
if (isOrientationChanged()) {
return bitmap.getWidth();
} else {
return bitmap.getHeight();
}
}

//得到宽度,要判断exif中是否要旋转
public int getWidth() {
if (bitmap == null) return 0;
if (isOrientationChanged()) {
return bitmap.getHeight();
} else {
return bitmap.getWidth();
}
}

//回收bitmap
public void recycle() {
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}
}
}

HighlightView

class HighlightView {

public static final int GROW_NONE = (1 << 0);//没有状态
public static final int GROW_LEFT_EDGE = (1 << 1);//触摸到左边边界状态
public static final int GROW_RIGHT_EDGE = (1 << 2);//触摸到右边边界状态
public static final int GROW_TOP_EDGE = (1 << 3);//触摸到上面边界状态
public static final int GROW_BOTTOM_EDGE = (1 << 4);//触摸到下边边界状态
public static final int MOVE = (1 << 5);//移动状态

private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5;//默认颜色
private static final float HANDLE_RADIUS_DP = 12f;//默认handle的半径
private static final float OUTLINE_DP = 2f;//线的宽度

enum ModifyMode {None, Move, Grow}//当前触摸状态

enum HandleMode {Changing, Always, Never}

RectF cropRect; // Image space
Rect drawRect; // Screen space
Matrix matrix;
private RectF imageRect; // Image space

private final Paint outsidePaint = new Paint();
private final Paint outlinePaint = new Paint();
private final Paint handlePaint = new Paint();

private View viewContext; // View displaying image
private boolean showThirds;//是否绘制裁剪View里面的四条线
private boolean showCircle;//是否绘制圆
private int highlightColor;//裁剪View绘制的颜色

private ModifyMode modifyMode = ModifyMode.None;
private HandleMode handleMode = HandleMode.Changing;
private boolean maintainAspectRatio;//是否希望变化的时候保存X/Y的比例
private float initialAspectRatio;//初始化的时候比例
private float handleRadius;
private float outlineWidth;
private boolean isFocused;

public HighlightView(View context) {
viewContext = context;
initStyles(context.getContext());
}

private void initStyles(Context context) {
//读取attr的值
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true);
TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView);
try {
showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false);
showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false);
highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor,
DEFAULT_HIGHLIGHT_COLOR);
handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)];
} finally {
attributes.recycle();
}
}

public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) {
//通过传进来的Matrix生成一个新的Matrix赋值给成员变量
matrix = new Matrix(m);
//裁剪的区域
this.cropRect = cropRect;
//整个图的区域
this.imageRect = new RectF(imageRect);
//希望保持比例
this.maintainAspectRatio = maintainAspectRatio;
//得到初始化比例
initialAspectRatio = this.cropRect.width() / this.cropRect.height();
//匹配裁剪区域对应到屏幕坐标系上
drawRect = computeLayout();
//初始化画笔
outsidePaint.setARGB(125, 50, 50, 50);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setAntiAlias(true);
outlineWidth = dpToPx(OUTLINE_DP);

handlePaint.setColor(highlightColor);
handlePaint.setStyle(Paint.Style.FILL);
handlePaint.setAntiAlias(true);
handleRadius = dpToPx(HANDLE_RADIUS_DP);
//当前ModifyMode为None
modifyMode = ModifyMode.None;
}

private float dpToPx(float dp) {
return dp * viewContext.getResources().getDisplayMetrics().density;
}

protected void draw(Canvas canvas) {
//保存
canvas.save();
Path path = new Path();
outlinePaint.setStrokeWidth(outlineWidth);
//如果没有获得焦点,那么绘制黑色
if (!hasFocus()) {
outlinePaint.setColor(Color.BLACK);
canvas.drawRect(drawRect, outlinePaint);
} else {//获得了焦点的情况下
//得到CropImageView的Rect区域
Rect viewDrawingRect = new Rect();
viewContext.getDrawingRect(viewDrawingRect);

path.addRect(new RectF(drawRect), Path.Direction.CW);
outlinePaint.setColor(highlightColor);
//是否支持硬件加速
if (isClipPathSupported(canvas)) {
//clipPath取不属于RectF(drawRect)和viewDrawingRect的集合
canvas.clipPath(path, Region.Op.DIFFERENCE);
canvas.drawRect(viewDrawingRect, outsidePaint);
} else {
drawOutsideFallback(canvas);
}
//回到保存之前的样子
canvas.restore();
//画path
canvas.drawPath(path, outlinePaint);
//画中间的四条线
if (showThirds) {
drawThirds(canvas);
}
//画圆
if (showCircle) {
drawCircle(canvas);
}

if (handleMode == HandleMode.Always || (handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) {
//画handle的圆
drawHandles(canvas);
}
}
}

/*
* Fall back to naive method for darkening outside crop area
*/

private void drawOutsideFallback(Canvas canvas) {
canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint);
canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint);
canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint);
canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint);
}

/*
* Clip path is broken, unreliable or not supported on:
* - JellyBean MR1
* - ICS & ICS MR1 with hardware acceleration turned on
*/

@SuppressLint("NewApi")
private boolean isClipPathSupported(Canvas canvas) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
return false;
} else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return true;
} else {
return !canvas.isHardwareAccelerated();
}
}

/**
* 画handle的圆
*
* @param canvas
*/

private void drawHandles(Canvas canvas) {
int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2);
int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2);

canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint);
canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint);
canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint);
canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint);
}

/**
* 画里面的四条线
*
* @param canvas
*/

private void drawThirds(Canvas canvas) {
outlinePaint.setStrokeWidth(1);
float xThird = (drawRect.right - drawRect.left) / 3;
float yThird = (drawRect.bottom - drawRect.top) / 3;

canvas.drawLine(drawRect.left + xThird, drawRect.top,
drawRect.left + xThird, drawRect.bottom, outlinePaint);
canvas.drawLine(drawRect.left + xThird * 2, drawRect.top,
drawRect.left + xThird * 2, drawRect.bottom, outlinePaint);
canvas.drawLine(drawRect.left, drawRect.top + yThird,
drawRect.right, drawRect.top + yThird, outlinePaint);
canvas.drawLine(drawRect.left, drawRect.top + yThird * 2,
drawRect.right, drawRect.top + yThird * 2, outlinePaint);
}

private void drawCircle(Canvas canvas) {
outlinePaint.setStrokeWidth(1);
canvas.drawOval(new RectF(drawRect), outlinePaint);
}

public void setMode(ModifyMode mode) {
if (mode != modifyMode) {
modifyMode = mode;
viewContext.invalidate();
}
}

// Determines which edges are hit by touching at (x, y)
//在CropImageView的OnTouchEvent中,通过x,y判断是否有碰触到HighlightView
public int getHit(float x, float y) {
Rect r = computeLayout();
final float hysteresis = 20F;//误差值,误差在这个范围内就算hit到了
int retval = GROW_NONE;

// verticalCheck makes sure the position is between the top and
// the bottom edge (with some tolerance). Similar for horizCheck.
boolean verticalCheck = (y >= r.top - hysteresis)
&& (y < r.bottom + hysteresis);
boolean horizCheck = (x >= r.left - hysteresis)
&& (x < r.right + hysteresis);

// Check whether the position is near some edge(s)
if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
retval |= GROW_LEFT_EDGE;
}
if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
retval |= GROW_RIGHT_EDGE;
}
if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
retval |= GROW_TOP_EDGE;
}
if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
retval |= GROW_BOTTOM_EDGE;
}

// Not near any edge but inside the rectangle: move
if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
retval = MOVE;
}
return retval;
}

// Handles motion (dx, dy) in screen space.
// The "edge" parameter specifies which edges the user is dragging.
//处理手势事件
void handleMotion(int edge, float dx, float dy) {
Rect r = computeLayout();
//如果触摸状态是MOVE的话,移动
if (edge == MOVE) {
// Convert to image space before sending to moveBy()
//将坐标转换成相对于CropImageView的坐标
moveBy(dx * (cropRect.width() / r.width()),
dy * (cropRect.height() / r.height()));
} else {
//不是触摸到左右边界,那么移动x方向为0
if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
dx = 0;
}
//不是触摸到上下边界,那么移动y方向为0
if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
dy = 0;
}

// Convert to image space before sending to growBy()
float xDelta = dx * (cropRect.width() / r.width());
float yDelta = dy * (cropRect.height() / r.height());
//放大缩小
growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
(((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
}
}

// Grows the cropping rectangle by (dx, dy) in image space
//cropRect移动dx,dy
void moveBy(float dx, float dy) {
Rect invalRect = new Rect(drawRect);

cropRect.offset(dx, dy);

// Put the cropping rectangle inside image rectangle
cropRect.offset(
Math.max(0, imageRect.left - cropRect.left),
Math.max(0, imageRect.top - cropRect.top));

cropRect.offset(
Math.min(0, imageRect.right - cropRect.right),
Math.min(0, imageRect.bottom - cropRect.bottom));

drawRect = computeLayout();
//取并集
invalRect.union(drawRect);
//invalRect再缩小-handleRadius那么大
invalRect.inset(-(int) handleRadius, -(int) handleRadius);
//绘制
viewContext.invalidate(invalRect);
}

// Grows the cropping rectangle by (dx, dy) in image space.
void growBy(float dx, float dy) {
//如果要保持比例的话,重新计算dx或dy
if (maintainAspectRatio) {
if (dx != 0) {
dy = dx / initialAspectRatio;
} else if (dy != 0) {
dx = dy * initialAspectRatio;
}
}

// Don't let the cropping rectangle grow too fast.
// Grow at most half of the difference between the image rectangle and
// the cropping rectangle.
RectF r = new RectF(cropRect);
if (dx > 0F && r.width() + 2 * dx > imageRect.width()) {
dx = (imageRect.width() - r.width()) / 2F;
if (maintainAspectRatio) {
dy = dx / initialAspectRatio;
}
}
if (dy > 0F && r.height() + 2 * dy > imageRect.height()) {
dy = (imageRect.height() - r.height()) / 2F;
if (maintainAspectRatio) {
dx = dy * initialAspectRatio;
}
}
//左右缩小-dx,上下缩小-dy
r.inset(-dx, -dy);

// Don't let the cropping rectangle shrink too fast
//不要收缩的太快
final float widthCap = 25F;
if (r.width() < widthCap) {
r.inset(-(widthCap - r.width()) / 2F, 0F);
}
float heightCap = maintainAspectRatio
? (widthCap / initialAspectRatio)
: widthCap;
if (r.height() < heightCap) {
r.inset(0F, -(heightCap - r.height()) / 2F);
}

// Put the cropping rectangle inside the image rectangle
if (r.left < imageRect.left) {
r.offset(imageRect.left - r.left, 0F);
} else if (r.right > imageRect.right) {
r.offset(-(r.right - imageRect.right), 0F);
}
if (r.top < imageRect.top) {
r.offset(0F, imageRect.top - r.top);
} else if (r.bottom > imageRect.bottom) {
r.offset(0F, -(r.bottom - imageRect.bottom));
}

cropRect.set(r);
drawRect = computeLayout();
viewContext.invalidate();
}

// Returns the cropping rectangle in image space with specified scale
public Rect getScaledCropRect(float scale) {
//得到传入参数放大后的的裁剪区域,scale一般传入的就是simpleSize
return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale),
(int) (cropRect.right * scale), (int) (cropRect.bottom * scale));
}

// Maps the cropping rectangle from image space to screen space
private Rect computeLayout() {
RectF r = new RectF(cropRect.left, cropRect.top,
cropRect.right, cropRect.bottom);
matrix.mapRect(r);
return new Rect(Math.round(r.left), Math.round(r.top),
Math.round(r.right), Math.round(r.bottom));
}

public void invalidate() {
drawRect = computeLayout();
}

public boolean hasFocus() {
return isFocused;
}

public void setFocus(boolean isFocused) {
this.isFocused = isFocused;
}

}

CropImageView

public class CropImageView extends ImageViewTouchBase {

ArrayList<HighlightView> highlightViews = new ArrayList<HighlightView>();
HighlightView motionHighlightView;
Context context;

private float lastX;
private float lastY;
private int motionEdge;
private int validPointerId;

public CropImageView(Context context) {
super(context);
}

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

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

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (bitmapDisplayed.getBitmap() != null) {
for (HighlightView hv : highlightViews) {
//设置矩阵
hv.matrix.set(getUnrotatedMatrix());
hv.invalidate();
if (hv.hasFocus()) {
//让HighlightView全部显示在屏幕上
centerBasedOnHighlightView(hv);
}
}
}
}

@Override
protected void zoomTo(float scale, float centerX, float centerY) {
//固定位置zoom
super.zoomTo(scale, centerX, centerY);
for (HighlightView hv : highlightViews) {
hv.matrix.set(getUnrotatedMatrix());
hv.invalidate();
}
}

@Override
protected void zoomIn() {
//放大
super.zoomIn();
for (HighlightView hv : highlightViews) {
hv.matrix.set(getUnrotatedMatrix());
hv.invalidate();
}
}

@Override
protected void zoomOut() {
//缩小
super.zoomOut();
for (HighlightView hv : highlightViews) {
hv.matrix.set(getUnrotatedMatrix());
hv.invalidate();
}
}

@Override
protected void postTranslate(float deltaX, float deltaY) {
//平移
super.postTranslate(deltaX, deltaY);
for (HighlightView hv : highlightViews) {
hv.matrix.postTranslate(deltaX, deltaY);
hv.invalidate();
}
}

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
CropImageActivity cropImageActivity = (CropImageActivity) context;
//如果正在保存,则跳过
if (cropImageActivity.isSaving()) {
return false;
}

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (HighlightView hv : highlightViews) {
//判断触摸到了什么
int edge = hv.getHit(event.getX(), event.getY());
if (edge != HighlightView.GROW_NONE) {
motionEdge = edge;
motionHighlightView = hv;
lastX = event.getX();
lastY = event.getY();
// Prevent multiple touches from interfering with crop area re-sizing
//设置PointerId防止多指操作
validPointerId = event.getPointerId(event.getActionIndex());
//设置触摸状态
motionHighlightView.setMode((edge == HighlightView.MOVE)
? HighlightView.ModifyMode.Move
: HighlightView.ModifyMode.Grow);
break;
}
}
break;
case MotionEvent.ACTION_UP:
if (motionHighlightView != null) {
//让HighlightView全部显示在屏幕上
centerBasedOnHighlightView(motionHighlightView);
motionHighlightView.setMode(HighlightView.ModifyMode.None);
}
motionHighlightView = null;
center();
break;
case MotionEvent.ACTION_MOVE:
//让HiglightView去处理手势
if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) {
motionHighlightView.handleMotion(motionEdge, event.getX()
- lastX, event.getY() - lastY);
lastX = event.getX();
lastY = event.getY();
}

// If we're not zoomed then there's no point in even allowing the user to move the image around.
// This call to center puts it back to the normalized location.
if (getScale() == 1F) {
center();
}
break;
}

return true;
}

// Pan the displayed image to make sure the cropping rectangle is visible.
private void ensureVisible(HighlightView hv) {
Rect r = hv.drawRect;

int panDeltaX1 = Math.max(0, getLeft() - r.left);
int panDeltaX2 = Math.min(0, getRight() - r.right);

int panDeltaY1 = Math.max(0, getTop() - r.top);
int panDeltaY2 = Math.min(0, getBottom() - r.bottom);

int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;

if (panDeltaX != 0 || panDeltaY != 0) {
panBy(panDeltaX, panDeltaY);
}
}

// If the cropping rectangle's size changed significantly, change the
// view's center and scale according to the cropping rectangle.
private void centerBasedOnHighlightView(HighlightView hv) {
Rect drawRect = hv.drawRect;

float width = drawRect.width();
float height = drawRect.height();

float thisWidth = getWidth();
float thisHeight = getHeight();

float z1 = thisWidth / width * .6F;
float z2 = thisHeight / height * .6F;

float zoom = Math.min(z1, z2);
zoom = zoom * this.getScale();
zoom = Math.max(1F, zoom);

if ((Math.abs(zoom - getScale()) / zoom) > .1) {
float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() };
getUnrotatedMatrix().mapPoints(coordinates);
zoomTo(zoom, coordinates[0], coordinates[1], 300F);
}

ensureVisible(hv);
}

@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
for (HighlightView highlightView : highlightViews) {
highlightView.draw(canvas);
}
}

public void add(HighlightView hv) {
highlightViews.add(hv);
invalidate();
}
}

ImageViewTouchBase

abstract class ImageViewTouchBase extends ImageView {

private static final float SCALE_RATE = 1.25F;

// This is the base transformation which is used to show the image
// initially. The current computation for this shows the image in
// it's entirety, letterboxing as needed. One could choose to
// show the image as cropped instead.
//
// This matrix is recomputed when we go from the thumbnail image to
// the full size image.
protected Matrix baseMatrix = new Matrix();

// This is the supplementary transformation which reflects what
// the user has done in terms of zooming and panning.
//
// This matrix remains the same when we go from the thumbnail image
// to the full size image.
protected Matrix suppMatrix = new Matrix();

// This is the final matrix which is computed as the concatentation
// of the base matrix and the supplementary matrix.
private final Matrix displayMatrix = new Matrix();

// Temporary buffer used for getting the values out of a matrix.
private final float[] matrixValues = new float[9];

// The current bitmap being displayed.
protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0);

int thisWidth = -1;
int thisHeight = -1;

float maxZoom;

private Runnable onLayoutRunnable;

protected Handler handler = new Handler();

// ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
// its use of that Bitmap
public interface Recycler {
public void recycle(Bitmap b);
}

private Recycler recycler;

public ImageViewTouchBase(Context context) {
super(context);
init();
}

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

public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

public void setRecycler(Recycler recycler) {
this.recycler = recycler;
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
thisWidth = right - left;
thisHeight = bottom - top;
Runnable r = onLayoutRunnable;
if (r != null) {
onLayoutRunnable = null;
r.run();
}
if (bitmapDisplayed.getBitmap() != null) {
getProperBaseMatrix(bitmapDisplayed, baseMatrix, true);
setImageMatrix(getImageViewMatrix());
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
event.startTracking();
return true;
}
return super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
if (getScale() > 1.0f) {
// If we're zoomed in, pressing Back jumps out to show the
// entire image, otherwise Back returns the user to the gallery
zoomTo(1.0f);
return true;
}
}
return super.onKeyUp(keyCode, event);
}

@Override
public void setImageBitmap(Bitmap bitmap) {
setImageBitmap(bitmap, 0);
}

//设置旋转的Bitmap
private void setImageBitmap(Bitmap bitmap, int rotation) {
super.setImageBitmap(bitmap);
Drawable d = getDrawable();
if (d != null) {
d.setDither(true);
}

Bitmap old = bitmapDisplayed.getBitmap();
bitmapDisplayed.setBitmap(bitmap);
bitmapDisplayed.setRotation(rotation);

if (old != null && old != bitmap && recycler != null) {
recycler.recycle(old);
}
}

//释放
public void clear() {
setImageBitmapResetBase(null, true);
}


// This function changes bitmap, reset base matrix according to the size
// of the bitmap, and optionally reset the supplementary matrix
public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp);
}

public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) {
final int viewWidth = getWidth();

if (viewWidth <= 0) {
onLayoutRunnable = new Runnable() {
public void run() {
//反复调用自己,知道得到view的宽度为止
setImageRotateBitmapResetBase(bitmap, resetSupp);
}
};
return;
}

if (bitmap.getBitmap() != null) {
getProperBaseMatrix(bitmap, baseMatrix, true);
setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
} else {
baseMatrix.reset();
setImageBitmap(null);
}

if (resetSupp) {
suppMatrix.reset();
}
setImageMatrix(getImageViewMatrix());
maxZoom = calculateMaxZoom();
}

// Center as much as possible in one or both axis. Centering is defined as follows:
// * If the image is scaled down below the view's dimensions then center it.
// * If the image is scaled larger than the view and is translated out of view then translate it back into view.
protected void center() {
final Bitmap bitmap = bitmapDisplayed.getBitmap();
if (bitmap == null) {
return;
}
Matrix m = getImageViewMatrix();

RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
m.mapRect(rect);

float height = rect.height();
float width = rect.width();

float deltaX = 0, deltaY = 0;

deltaY = centerVertical(rect, height, deltaY);
deltaX = centerHorizontal(rect, width, deltaX);

postTranslate(deltaX, deltaY);
setImageMatrix(getImageViewMatrix());
}

private float centerVertical(RectF rect, float height, float deltaY) {
int viewHeight = getHeight();
if (height < viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = getHeight() - rect.bottom;
}
return deltaY;
}

private float centerHorizontal(RectF rect, float width, float deltaX) {
int viewWidth = getWidth();
if (width < viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
return deltaX;
}

//初始化,设置ImageView的ScaleType
private void init() {
setScaleType(ImageView.ScaleType.MATRIX);
}

protected float getValue(Matrix matrix, int whichValue) {
matrix.getValues(matrixValues);
return matrixValues[whichValue];
}

// Get the scale factor out of the matrix.
protected float getScale(Matrix matrix) {
return getValue(matrix, Matrix.MSCALE_X);
}

protected float getScale() {
return getScale(suppMatrix);
}

// Setup the base matrix so that the image is centered and scaled properly.
private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) {
float viewWidth = getWidth();
float viewHeight = getHeight();

float w = bitmap.getWidth();
float h = bitmap.getHeight();
matrix.reset();

// We limit up-scaling to 3x otherwise the result may look bad if it's a small icon
float widthScale = Math.min(viewWidth / w, 3.0f);
float heightScale = Math.min(viewHeight / h, 3.0f);
float scale = Math.min(widthScale, heightScale);

if (includeRotation) {
matrix.postConcat(bitmap.getRotateMatrix());
}
matrix.postScale(scale, scale);
matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
}

// Combine the base matrix and the supp matrix to make the final matrix
protected Matrix getImageViewMatrix() {
// The final matrix is computed as the concatentation of the base matrix
// and the supplementary matrix
displayMatrix.set(baseMatrix);
displayMatrix.postConcat(suppMatrix);
return displayMatrix;
}

public Matrix getUnrotatedMatrix(){
Matrix unrotated = new Matrix();
getProperBaseMatrix(bitmapDisplayed, unrotated, false);
unrotated.postConcat(suppMatrix);
return unrotated;
}

protected float calculateMaxZoom() {
if (bitmapDisplayed.getBitmap() == null) {
return 1F;
}

float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth;
float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight;
return Math.max(fw, fh) * 4; // 400%
}

protected void zoomTo(float scale, float centerX, float centerY) {
if (scale > maxZoom) {
scale = maxZoom;
}

float oldScale = getScale();
float deltaScale = scale / oldScale;

suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
setImageMatrix(getImageViewMatrix());
center();
}

protected void zoomTo(final float scale, final float centerX,
final float centerY, final float durationMs)
{

final float incrementPerMs = (scale - getScale()) / durationMs;
final float oldScale = getScale();
final long startTime = System.currentTimeMillis();

handler.post(new Runnable() {
public void run() {
long now = System.currentTimeMillis();
float currentMs = Math.min(durationMs, now - startTime);
float target = oldScale + (incrementPerMs * currentMs);
zoomTo(target, centerX, centerY);

if (currentMs < durationMs) {
handler.post(this);
}
}
});
}

protected void zoomTo(float scale) {
float cx = getWidth() / 2F;
float cy = getHeight() / 2F;
zoomTo(scale, cx, cy);
}

protected void zoomIn() {
zoomIn(SCALE_RATE);
}

protected void zoomOut() {
zoomOut(SCALE_RATE);
}

protected void zoomIn(float rate) {
if (getScale() >= maxZoom) {
return; // Don't let the user zoom into the molecular level
}
if (bitmapDisplayed.getBitmap() == null) {
return;
}

float cx = getWidth() / 2F;
float cy = getHeight() / 2F;

suppMatrix.postScale(rate, rate, cx, cy);
setImageMatrix(getImageViewMatrix());
}

protected void zoomOut(float rate) {
if (bitmapDisplayed.getBitmap() == null) {
return;
}

float cx = getWidth() / 2F;
float cy = getHeight() / 2F;

// Zoom out to at most 1x
Matrix tmp = new Matrix(suppMatrix);
tmp.postScale(1F / rate, 1F / rate, cx, cy);

if (getScale(tmp) < 1F) {
suppMatrix.setScale(1F, 1F, cx, cy);
} else {
suppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
}
setImageMatrix(getImageViewMatrix());
center();
}

protected void postTranslate(float dx, float dy) {
suppMatrix.postTranslate(dx, dy);
}

protected void panBy(float dx, float dy) {
postTranslate(dx, dy);
setImageMatrix(getImageViewMatrix());
}
}

总结

项目中裁剪的核心是 BitmapRegionDecoder ,还要许多可取之处,比如计算 simpleSize 的方法、得到 View 的宽度的方法等。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » android-crop源码解析

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址