package com.yalantis.ucrop.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
import com.yalantis.ucrop.callback.BitmapLoadCallback;
import com.yalantis.ucrop.model.ExifInfo;
import com.yalantis.ucrop.util.BitmapLoadUtils;
import com.yalantis.ucrop.util.FastBitmapDrawable;
import com.yalantis.ucrop.util.RectUtils;
/**
* Created by Oleksii Shliama (https://github.com/shliama).
* <p/>
* This class provides base logic to setup the image, transform it with matrix (move, scale, rotate),
* and methods to get current matrix state.
*/
public class TransformImageView extends ImageView {
private static final String TAG = "TransformImageView";
private static final int RECT_CORNER_POINTS_COORDS = 8;
private static final int RECT_CENTER_POINT_COORDS = 2;
private static final int MATRIX_VALUES_COUNT = 9;
protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS];
protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS];
private final float[] mMatrixValues = new float[MATRIX_VALUES_COUNT];
protected Matrix mCurrentImageMatrix = new Matrix();
protected int mThisWidth, mThisHeight;
protected TransformImageListener mTransformImageListener;
private float[] mInitialImageCorners;
private float[] mInitialImageCenter;
protected boolean mBitmapDecoded = false;
protected boolean mBitmapLaidOut = false;
private int mMaxBitmapSize = 0;
private String mImageInputPath, mImageOutputPath;
private ExifInfo mExifInfo;
/**
* Interface for rotation and scale change notifying.
*/
public interface TransformImageListener {
void onLoadComplete();
void onLoadFailure(@NonNull Exception e);
void onRotate(float currentAngle);
void onScale(float currentScale);
}
public TransformImageView(Context context) {
this(context, null);
}
public TransformImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TransformImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public void setTransformImageListener(TransformImageListener transformImageListener) {
mTransformImageListener = transformImageListener;
}
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType == ScaleType.MATRIX) {
super.setScaleType(scaleType);
} else {
Log.w(TAG, "Invalid ScaleType. Only ScaleType.MATRIX can be used");
}
}
/**
* Setter for {@link #mMaxBitmapSize} value.
* Be sure to call it before {@link #setImageURI(Uri)} or other image setters.
*
* @param maxBitmapSize - max size for both width and height of bitmap that will be used in the view.
*/
public void setMaxBitmapSize(int maxBitmapSize) {
mMaxBitmapSize = maxBitmapSize;
}
public int getMaxBitmapSize() {
if (mMaxBitmapSize <= 0) {
mMaxBitmapSize = BitmapLoadUtils.calculateMaxBitmapSize(getContext());
}
return mMaxBitmapSize;
}
@Override
public void setImageBitmap(final Bitmap bitmap) {
setImageDrawable(new FastBitmapDrawable(bitmap));
}
public String getImageInputPath() {
return mImageInputPath;
}
public String getImageOutputPath() {
return mImageOutputPath;
}
public ExifInfo getExifInfo() {
return mExifInfo;
}
/**
* This method takes an Uri as a parameter, then calls method to decode it into Bitmap with specified size.
*
* @param imageUri - image Uri
* @throws Exception - can throw exception if having problems with decoding Uri or OOM.
*/
public void setImageUri(@NonNull Uri imageUri, @Nullable Uri outputUri) throws Exception {
int maxBitmapSize = getMaxBitmapSize();
BitmapLoadUtils.decodeBitmapInBackground(getContext(), imageUri, outputUri, maxBitmapSize, maxBitmapSize,
new BitmapLoadCallback() {
@Override
public void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull String imageInputPath, @Nullable String imageOutputPath) {
mImageInputPath = imageInputPath;
mImageOutputPath = imageOutputPath;
mExifInfo = exifInfo;
mBitmapDecoded = true;
setImageBitmap(bitmap);
}
@Override
public void onFailure(@NonNull Exception bitmapWorkerException) {
Log.e(TAG, "onFailure: setImageUri", bitmapWorkerException);
if (mTransformImageListener != null) {
mTransformImageListener.onLoadFailure(bitmapWorkerException);
}
}
});
}
/**
* @return - current image scale value.
* [1.0f - for original image, 2.0f - for 200% scaled image, etc.]
*/
public float getCurrentScale() {
return getMatrixScale(mCurrentImageMatrix);
}
/**
* This method calculates scale value for given Matrix object.
*/
public float getMatrixScale(@NonNull Matrix matrix) {
return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2)
+ Math.pow(getMatrixValue(matrix, Matrix.MSKEW_Y), 2));
}
/**
* @return - current image rotation angle.
*/
public float getCurrentAngle() {
return getMatrixAngle(mCurrentImageMatrix);
}
/**
* This method calculates rotation angle for given Matrix object.
*/
public float getMatrixAngle(@NonNull Matrix matrix) {
return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X),
getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI));
}
@Override
public void setImageMatrix(Matrix matrix) {
super.setImageMatrix(matrix);
mCurrentImageMatrix.set(matrix);
updateCurrentImagePoints();
}
@Nullable
public Bitmap getViewBitmap() {
if (getDrawable() == null || !(getDrawable() instanceof FastBitmapDrawable)) {
return null;
} else {
return ((FastBitmapDrawable) getDrawable()).getBitmap();
}
}
/**
* This method translates current image.
*
* @param deltaX - horizontal shift
* @param deltaY - vertical shift
*/
public void postTranslate(float deltaX, float deltaY) {
if (deltaX != 0 || deltaY != 0) {
mCurrentImageMatrix.postTranslate(deltaX, deltaY);
setImageMatrix(mCurrentImageMatrix);
}
}
/**
* This method scales current image.
*
* @param deltaScale - scale value
* @param px - scale center X
* @param py - scale center Y
*/
public void postScale(float deltaScale, float px, float py) {
if (deltaScale != 0) {
mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);
setImageMatrix(mCurrentImageMatrix);
if (mTransformImageListener != null) {
mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));
}
}
}
/**
* This method rotates current image.
*
* @param deltaAngle - rotation angle
* @param px - rotation center X
* @param py - rotation center Y
*/
public void postRotate(float deltaAngle, float px, float py) {
if (deltaAngle != 0) {
mCurrentImageMatrix.postRotate(deltaAngle, px, py);
setImageMatrix(mCurrentImageMatrix);
if (mTransformImageListener != null) {
mTransformImageListener.onRotate(getMatrixAngle(mCurrentImageMatrix));
}
}
}
protected void init() {
setScaleType(ScaleType.MATRIX);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed || (mBitmapDecoded && !mBitmapLaidOut)) {
left = getPaddingLeft();
top = getPaddingTop();
right = getWidth() - getPaddingRight();
bottom = getHeight() - getPaddingBottom();
mThisWidth = right - left;
mThisHeight = bottom - top;
onImageLaidOut();
}
}
/**
* When image is laid out {@link #mInitialImageCenter} and {@link #mInitialImageCenter}
* must be set.
*/
protected void onImageLaidOut() {
final Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
float w = drawable.getIntrinsicWidth();
float h = drawable.getIntrinsicHeight();
Log.d(TAG, String.format("Image size: [%d:%d]", (int) w, (int) h));
RectF initialImageRect = new RectF(0, 0, w, h);
mInitialImageCorners = RectUtils.getCornersFromRect(initialImageRect);
mInitialImageCenter = RectUtils.getCenterFromRect(initialImageRect);
mBitmapLaidOut = true;
if (mTransformImageListener != null) {
mTransformImageListener.onLoadComplete();
}
}
/**
* This method returns Matrix value for given index.
*
* @param matrix - valid Matrix object
* @param valueIndex - index of needed value. See {@link Matrix#MSCALE_X} and others.
* @return - matrix value for index
*/
protected float getMatrixValue(@NonNull Matrix matrix, @IntRange(from = 0, to = MATRIX_VALUES_COUNT) int valueIndex) {
matrix.getValues(mMatrixValues);
return mMatrixValues[valueIndex];
}
/**
* This method logs given matrix X, Y, scale, and angle values.
* Can be used for debug.
*/
@SuppressWarnings("unused")
protected void printMatrix(@NonNull String logPrefix, @NonNull Matrix matrix) {
float x = getMatrixValue(matrix, Matrix.MTRANS_X);
float y = getMatrixValue(matrix, Matrix.MTRANS_Y);
float rScale = getMatrixScale(matrix);
float rAngle = getMatrixAngle(matrix);
Log.d(TAG, logPrefix + ": matrix: { x: " + x + ", y: " + y + ", scale: " + rScale + ", angle: " + rAngle + " }");
}
/**
* This method updates current image corners and center points that are stored in
* {@link #mCurrentImageCorners} and {@link #mCurrentImageCenter} arrays.
* Those are used for several calculations.
*/
private void updateCurrentImagePoints() {
mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);
mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);
}
}