diff --git a/.travis.yml b/.travis.yml
index 591a41ef..dee552ac 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,13 @@
language: android
+jdk: oraclejdk8
sudo: false
android:
components:
- - build-tools-24.0.1
- - android-24
+ - tools
+ - platform-tools
+ - build-tools-28.0.3
+ - android-28
- extra-android-m2repository
script:
diff --git a/README.md b/README.md
index 8d35c8cc..2ccf64e9 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,19 @@
Android Image Cropper
=======
-[](https://android-arsenal.com/details/1/3487)
-[  ](https://bintray.com/arthurhub/maven/Android-Image-Cropper/_latestVersion)
+# :triangular_flag_on_post: The Project is NOT currently maintained :triangular_flag_on_post:
+
+## Please use **[CanHub's fork](https://github.com/CanHub/Android-Image-Cropper)!**
+
+### Thank everybody for using the library. It was very fun to create and a privilage to help you build awesome apps.
+### The same way I took an unmaintained initial implementation from [edmodo](https://github.com/edmodo/cropper), I'm happy to see [CanHub](https://github.com/CanHub/Android-Image-Cropper) taking it now.
+### Good luck and happy coding :octocat:
+
+
+----
+----
+[](https://android-arsenal.com/details/1/3487)
+[](https://travis-ci.org/ArthurHub/Android-Image-Cropper)
**Powerful** (Zoom, Rotation, Multi-Source), **customizable** (Shape, Limits, Style), **optimized** (Async, Sampling, Matrix) and **simple** image cropping library for Android.
@@ -13,43 +24,61 @@ Android Image Cropper
[See GitHub Wiki for more info.](https://github.com/ArthurHub/Android-Image-Cropper/wiki)
-Include the library
+1. Include the library
```
- compile 'com.theartofdev.edmodo:android-image-cropper:2.3.+'
+ dependencies {
+ api 'com.theartofdev.edmodo:android-image-cropper:2.8.+'
+ }
```
+Add permissions to manifest
+
+ ```
+
+
+ ```
+Add this line to your Proguard config file
+
+```
+-keep class androidx.appcompat.widget.** { *; }
+```
### Using Activity
2. Add `CropImageActivity` into your AndroidManifest.xml
```xml
+ android:theme="@style/Base.Theme.AppCompat"/>
```
3. Start `CropImageActivity` using builder pattern from your activity
```java
+ // start picker to get image for cropping and then use the image in cropping activity
+ CropImage.activity()
+ .setGuidelines(CropImageView.Guidelines.ON)
+ .start(this);
+
+ // start cropping activity for pre-acquired image saved on the device
CropImage.activity(imageUri)
- .setGuidelines(CropImageView.Guidelines.ON)
- .start(this);
+ .start(this);
// for fragment (DO NOT use `getActivity()`)
- CropImage.activity(imageUri)
- .start(getContext(), this);
+ CropImage.activity()
+ .start(getContext(), this);
```
4. Override `onActivityResult` method in your activity to get crop result
```java
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
- CropImage.ActivityResult result = CropImage.getActivityResult(data);
- if (resultCode == RESULT_OK) {
- Uri resultUri = result.getUri();
- } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
- Exception error = result.getError();
- }
+ if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+ CropImage.ActivityResult result = CropImage.getActivityResult(data);
+ if (resultCode == RESULT_OK) {
+ Uri resultUri = result.getUri();
+ } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
+ Exception error = result.getError();
}
+ }
}
```
@@ -58,38 +87,39 @@ Include the library
```xml
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/cropImageView"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
```
3. Set image to crop
```java
- cropImageView.setImageBitmap(bitmap);
- // or
cropImageView.setImageUriAsync(uri);
+ // or (prefer using uri for performance and better user experience)
+ cropImageView.setImageBitmap(bitmap);
```
4. Get cropped image
```java
- Bitmap cropped = cropImageView.getCroppedImage();
- // or (must subscribe to async event using cropImageView.setOnCropImageCompleteListener(listener))
+ // subscribe to async event using cropImageView.setOnCropImageCompleteListener(listener)
cropImageView.getCroppedImageAsync();
+ // or
+ Bitmap cropped = cropImageView.getCroppedImage();
```
## Features
- Built-in `CropImageActivity`.
- Set cropping image as Bitmap, Resource or Android URI (Gallery, Camera, Dropbox, etc.).
-- Image rotation during cropping.
+- Image rotation/flipping during cropping.
- Auto zoom-in/out to relevant cropping area.
- Auto rotate bitmap by image Exif data.
- Set result image min/max limits in pixels.
- Set initial crop window size/location.
- Request cropped image resize to specific size.
- Bitmap memory optimization, OOM handling (should never occur)!
-- API Level 10.
+- API Level 14.
- More..
## Customizations
@@ -107,25 +137,17 @@ For more information, see the [GitHub Wiki](https://github.com/ArthurHub/Android
- [Adding auto-zoom feature to Android-Image-Cropper](https://theartofdev.com/2016/04/25/adding-auto-zoom-feature-to-android-image-cropper/)
## Change log
-*2.3.0*
-
-- Change required width/height behavior to support resizing (inside/fit/exact) see wiki for details.
-- Add sampling fallback to lower cropped image resolution on OOM error (if image loaded from URI).
-- Setting aspect ratio will also set it to fixed, to help with confusion, add clear aspect ratio method.
-- Add support for setting min/max crop result size in code on CropImageView.
-- Fix cropping failing bug when skia fails region cropping.
-- Add Fallback to Intent.ACTION_PICK if no intent found for Intent.ACTION_GET_CONTENT (thx geolyth)
-- Multi touch support for cropping window (experimental, thx bbwharris)
-- **BREAKING CHANGES**:
- - If you previously used requested width/height the default behavior now is to resize inside the cropped image, to preserve the previous behavior you need to pass the SAMPLING option.
- - `OnGetCroppedImageCompleteListener` and `OnSaveCroppedImageCompleteListener` is deprecated, use `OnCropImageCompleteListener` that combines the two and provides the result object as crop activity.
- - Set aspect ratio also sets fixed aspect ratio to true, if this is not the desired behavior set the fix aspect ratio flag manually or call the method after calling set aspect ratio.
-
-*2.2.5*
-
-- Fix to webp file extension (thx Nathan)
-- Fix wrong initial crop window when image contains exif data.
-- Added corners to circular crop window, can be removed by setting `BorderCornerThickness` to 0.
+*2.8.0*
+- Fix crash on Android O (thx @juliooa)
+- Update to support library to AndroidX (thx @mradzinski)
+- Handle failure when selecting non image file (thx @uncledoc)
+- More translations (thx @jkwiecien, @david-serrano)
+
+*2.7.0*
+- Update gradle wrapper to 4.4
+- Update support library to 27.1.1 and set is statically! (thx @androideveloper)
+- Fix NPE in activity creation by tools (thx @unverbraucht)
+- More translations (thx @gwharvey, @dlackty, @JairoGeek, @shaymargolis)
See [full change log](https://github.com/ArthurHub/Android-Image-Cropper/wiki/Change-Log).
diff --git a/art/zoom sample small 2.gif b/art/zoom sample small 2.gif
new file mode 100644
index 00000000..a4aa7a2b
Binary files /dev/null and b/art/zoom sample small 2.gif differ
diff --git a/art/zoom sample small 3.gif b/art/zoom sample small 3.gif
new file mode 100644
index 00000000..05246472
Binary files /dev/null and b/art/zoom sample small 3.gif differ
diff --git a/build.gradle b/build.gradle
index 857aec95..24d6cbf9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,29 @@
buildscript {
repositories {
jcenter()
+ google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.1.3'
+ classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
- mavenLocal()
jcenter()
- mavenCentral()
+ maven {
+ url "https://maven.google.com"
+ }
}
-}
\ No newline at end of file
+}
+
+ext {
+ compileSdkVersion = 28
+ buildToolsVersion = '28.0.3'
+ androidXLibraryVersion = '1.0.0'
+
+ PUBLISH_GROUP_ID = 'com.theartofdev.edmodo'
+ PUBLISH_ARTIFACT_ID = 'android-image-cropper'
+ PUBLISH_VERSION = '2.8.0'
+ // gradlew clean build generateRelease
+}
diff --git a/cropper/build.gradle b/cropper/build.gradle
index ca64b0cf..7bea026c 100644
--- a/cropper/build.gradle
+++ b/cropper/build.gradle
@@ -3,20 +3,13 @@ apply plugin: 'com.android.library'
// http://www.flexlabs.org/2013/06/using-local-aar-android-library-packages-in-gradle-builds
apply plugin: 'maven-publish'
-ext {
- PUBLISH_GROUP_ID = 'com.theartofdev.edmodo'
- PUBLISH_ARTIFACT_ID = 'android-image-cropper'
- PUBLISH_VERSION = '2.3.0'
- // gradlew clean build generateRelease
-}
-
android {
- compileSdkVersion 24
- buildToolsVersion '24.0.1'
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
defaultConfig {
- minSdkVersion 10
- targetSdkVersion 24
+ minSdkVersion 14
+ targetSdkVersion rootProject.compileSdkVersion
versionCode 1
versionName PUBLISH_VERSION
}
@@ -46,9 +39,10 @@ publishing {
}
}
-apply from: 'https://raw.githubusercontent.com/ArthurHub/release-android-library/master/android-release-aar.gradle'
+apply from: 'https://raw.githubusercontent.com/blundell/release-android-library/master/android-release-aar.gradle'
dependencies {
- compile 'com.android.support:appcompat-v7:24.1.1'
+ api "androidx.appcompat:appcompat:$androidXLibraryVersion"
+ implementation "androidx.exifinterface:exifinterface:$androidXLibraryVersion"
}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java
index eedb9234..6c6723df 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java
@@ -19,274 +19,282 @@
import java.lang.ref.WeakReference;
-/**
- * Task to crop bitmap asynchronously from the UI thread.
- */
-final class BitmapCroppingWorkerTask extends AsyncTask {
-
- //region: Fields and Consts
-
- /**
- * Use a WeakReference to ensure the ImageView can be garbage collected
- */
- private final WeakReference mCropImageViewReference;
-
- /**
- * the bitmap to crop
- */
- private final Bitmap mBitmap;
-
- /**
- * The Android URI of the image to load
- */
- private final Uri mUri;
-
- /**
- * The context of the crop image view widget used for loading of bitmap by Android URI
- */
- private final Context mContext;
-
- /**
- * Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3)
- */
- private final float[] mCropPoints;
-
- /**
- * Degrees the image was rotated after loading
- */
- private final int mDegreesRotated;
-
- /**
- * the original width of the image to be cropped (for image loaded from URI)
- */
- private final int mOrgWidth;
-
- /**
- * the original height of the image to be cropped (for image loaded from URI)
- */
- private final int mOrgHeight;
-
- /**
- * is there is fixed aspect ratio for the crop rectangle
- */
- private final boolean mFixAspectRatio;
-
- /**
- * the X aspect ration of the crop rectangle
- */
- private final int mAspectRatioX;
-
- /**
- * the Y aspect ration of the crop rectangle
- */
- private final int mAspectRatioY;
-
- /**
- * required width of the cropping image
- */
- private final int mReqWidth;
-
- /**
- * required height of the cropping image
- */
- private final int mReqHeight;
-
- /**
- * The option to handle requested width/height
- */
- private final CropImageView.RequestSizeOptions mReqSizeOptions;
-
- /**
- * the Android Uri to save the cropped image to
- */
- private final Uri mSaveUri;
-
- /**
- * the compression format to use when writing the image
- */
- private final Bitmap.CompressFormat mSaveCompressFormat;
-
- /**
- * the quality (if applicable) to use when writing the image (0 - 100)
- */
- private final int mSaveCompressQuality;
- //endregion
-
- public BitmapCroppingWorkerTask(CropImageView cropImageView, Bitmap bitmap, float[] cropPoints,
- int degreesRotated, boolean fixAspectRatio, int aspectRatioX, int aspectRatioY,
- int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options,
- Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
-
- mCropImageViewReference = new WeakReference<>(cropImageView);
- mContext = cropImageView.getContext();
- mBitmap = bitmap;
- mCropPoints = cropPoints;
- mUri = null;
- mDegreesRotated = degreesRotated;
- mFixAspectRatio = fixAspectRatio;
- mAspectRatioX = aspectRatioX;
- mAspectRatioY = aspectRatioY;
- mReqWidth = reqWidth;
- mReqHeight = reqHeight;
- mReqSizeOptions = options;
- mSaveUri = saveUri;
- mSaveCompressFormat = saveCompressFormat;
- mSaveCompressQuality = saveCompressQuality;
- mOrgWidth = 0;
- mOrgHeight = 0;
- }
-
- public BitmapCroppingWorkerTask(CropImageView cropImageView, Uri uri, float[] cropPoints,
- int degreesRotated, int orgWidth, int orgHeight,
- boolean fixAspectRatio, int aspectRatioX, int aspectRatioY,
- int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options,
- Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
-
- mCropImageViewReference = new WeakReference<>(cropImageView);
- mContext = cropImageView.getContext();
- mUri = uri;
- mCropPoints = cropPoints;
- mDegreesRotated = degreesRotated;
- mFixAspectRatio = fixAspectRatio;
- mAspectRatioX = aspectRatioX;
- mAspectRatioY = aspectRatioY;
- mOrgWidth = orgWidth;
- mOrgHeight = orgHeight;
- mReqWidth = reqWidth;
- mReqHeight = reqHeight;
- mReqSizeOptions = options;
- mSaveUri = saveUri;
- mSaveCompressFormat = saveCompressFormat;
- mSaveCompressQuality = saveCompressQuality;
- mBitmap = null;
- }
-
- /**
- * The Android URI that this task is currently loading.
- */
- public Uri getUri() {
- return mUri;
- }
+/** Task to crop bitmap asynchronously from the UI thread. */
+final class BitmapCroppingWorkerTask
+ extends AsyncTask {
+
+ // region: Fields and Consts
+
+ /** Use a WeakReference to ensure the ImageView can be garbage collected */
+ private final WeakReference mCropImageViewReference;
+
+ /** the bitmap to crop */
+ private final Bitmap mBitmap;
+
+ /** The Android URI of the image to load */
+ private final Uri mUri;
+
+ /** The context of the crop image view widget used for loading of bitmap by Android URI */
+ private final Context mContext;
+
+ /** Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3) */
+ private final float[] mCropPoints;
+
+ /** Degrees the image was rotated after loading */
+ private final int mDegreesRotated;
+
+ /** the original width of the image to be cropped (for image loaded from URI) */
+ private final int mOrgWidth;
+
+ /** the original height of the image to be cropped (for image loaded from URI) */
+ private final int mOrgHeight;
+
+ /** is there is fixed aspect ratio for the crop rectangle */
+ private final boolean mFixAspectRatio;
+
+ /** the X aspect ration of the crop rectangle */
+ private final int mAspectRatioX;
+
+ /** the Y aspect ration of the crop rectangle */
+ private final int mAspectRatioY;
+
+ /** required width of the cropping image */
+ private final int mReqWidth;
+
+ /** required height of the cropping image */
+ private final int mReqHeight;
+
+ /** is the image flipped horizontally */
+ private final boolean mFlipHorizontally;
+
+ /** is the image flipped vertically */
+ private final boolean mFlipVertically;
+
+ /** The option to handle requested width/height */
+ private final CropImageView.RequestSizeOptions mReqSizeOptions;
+
+ /** the Android Uri to save the cropped image to */
+ private final Uri mSaveUri;
+
+ /** the compression format to use when writing the image */
+ private final Bitmap.CompressFormat mSaveCompressFormat;
+
+ /** the quality (if applicable) to use when writing the image (0 - 100) */
+ private final int mSaveCompressQuality;
+ // endregion
+
+ BitmapCroppingWorkerTask(
+ CropImageView cropImageView,
+ Bitmap bitmap,
+ float[] cropPoints,
+ int degreesRotated,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ int reqWidth,
+ int reqHeight,
+ boolean flipHorizontally,
+ boolean flipVertically,
+ CropImageView.RequestSizeOptions options,
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality) {
+
+ mCropImageViewReference = new WeakReference<>(cropImageView);
+ mContext = cropImageView.getContext();
+ mBitmap = bitmap;
+ mCropPoints = cropPoints;
+ mUri = null;
+ mDegreesRotated = degreesRotated;
+ mFixAspectRatio = fixAspectRatio;
+ mAspectRatioX = aspectRatioX;
+ mAspectRatioY = aspectRatioY;
+ mReqWidth = reqWidth;
+ mReqHeight = reqHeight;
+ mFlipHorizontally = flipHorizontally;
+ mFlipVertically = flipVertically;
+ mReqSizeOptions = options;
+ mSaveUri = saveUri;
+ mSaveCompressFormat = saveCompressFormat;
+ mSaveCompressQuality = saveCompressQuality;
+ mOrgWidth = 0;
+ mOrgHeight = 0;
+ }
+
+ BitmapCroppingWorkerTask(
+ CropImageView cropImageView,
+ Uri uri,
+ float[] cropPoints,
+ int degreesRotated,
+ int orgWidth,
+ int orgHeight,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ int reqWidth,
+ int reqHeight,
+ boolean flipHorizontally,
+ boolean flipVertically,
+ CropImageView.RequestSizeOptions options,
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality) {
+
+ mCropImageViewReference = new WeakReference<>(cropImageView);
+ mContext = cropImageView.getContext();
+ mUri = uri;
+ mCropPoints = cropPoints;
+ mDegreesRotated = degreesRotated;
+ mFixAspectRatio = fixAspectRatio;
+ mAspectRatioX = aspectRatioX;
+ mAspectRatioY = aspectRatioY;
+ mOrgWidth = orgWidth;
+ mOrgHeight = orgHeight;
+ mReqWidth = reqWidth;
+ mReqHeight = reqHeight;
+ mFlipHorizontally = flipHorizontally;
+ mFlipVertically = flipVertically;
+ mReqSizeOptions = options;
+ mSaveUri = saveUri;
+ mSaveCompressFormat = saveCompressFormat;
+ mSaveCompressQuality = saveCompressQuality;
+ mBitmap = null;
+ }
+
+ /** The Android URI that this task is currently loading. */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Crop image in background.
+ *
+ * @param params ignored
+ * @return the decoded bitmap data
+ */
+ @Override
+ protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
+ try {
+ if (!isCancelled()) {
+
+ BitmapUtils.BitmapSampled bitmapSampled;
+ if (mUri != null) {
+ bitmapSampled =
+ BitmapUtils.cropBitmap(
+ mContext,
+ mUri,
+ mCropPoints,
+ mDegreesRotated,
+ mOrgWidth,
+ mOrgHeight,
+ mFixAspectRatio,
+ mAspectRatioX,
+ mAspectRatioY,
+ mReqWidth,
+ mReqHeight,
+ mFlipHorizontally,
+ mFlipVertically);
+ } else if (mBitmap != null) {
+ bitmapSampled =
+ BitmapUtils.cropBitmapObjectHandleOOM(
+ mBitmap,
+ mCropPoints,
+ mDegreesRotated,
+ mFixAspectRatio,
+ mAspectRatioX,
+ mAspectRatioY,
+ mFlipHorizontally,
+ mFlipVertically);
+ } else {
+ return new Result((Bitmap) null, 1);
+ }
- /**
- * Crop image in background.
- *
- * @param params ignored
- * @return the decoded bitmap data
- */
- @Override
- protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
- try {
- if (!isCancelled()) {
-
- Bitmap bitmap = null;
- int sampleSize = 1;
- if (mUri != null) {
- BitmapUtils.BitmapSampled bitmapSampled =
- BitmapUtils.cropBitmap(mContext, mUri, mCropPoints, mDegreesRotated, mOrgWidth, mOrgHeight,
- mFixAspectRatio, mAspectRatioX, mAspectRatioY, mReqWidth, mReqHeight);
- bitmap = bitmapSampled.bitmap;
- sampleSize = bitmapSampled.sampleSize;
- } else if (mBitmap != null) {
- bitmap = BitmapUtils.cropBitmap(mBitmap, mCropPoints, mDegreesRotated, mFixAspectRatio, mAspectRatioX, mAspectRatioY);
- }
-
- bitmap = BitmapUtils.resizeBitmap(bitmap, mReqWidth, mReqHeight, mReqSizeOptions);
-
- if (mSaveUri == null) {
- return new Result(bitmap, sampleSize);
- } else {
- BitmapUtils.writeBitmapToUri(mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
- if (bitmap != null) {
- bitmap.recycle();
- }
- return new Result(mSaveUri, sampleSize);
- }
- }
- return null;
- } catch (Exception e) {
- return new Result(e, mSaveUri != null);
+ Bitmap bitmap =
+ BitmapUtils.resizeBitmap(bitmapSampled.bitmap, mReqWidth, mReqHeight, mReqSizeOptions);
+
+ if (mSaveUri == null) {
+ return new Result(bitmap, bitmapSampled.sampleSize);
+ } else {
+ BitmapUtils.writeBitmapToUri(
+ mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
+ if (bitmap != null) {
+ bitmap.recycle();
+ }
+ return new Result(mSaveUri, bitmapSampled.sampleSize);
}
+ }
+ return null;
+ } catch (Exception e) {
+ return new Result(e, mSaveUri != null);
}
-
- /**
- * Once complete, see if ImageView is still around and set bitmap.
- *
- * @param result the result of bitmap cropping
- */
- @Override
- protected void onPostExecute(Result result) {
- if (result != null) {
- boolean completeCalled = false;
- if (!isCancelled()) {
- CropImageView cropImageView = mCropImageViewReference.get();
- if (cropImageView != null) {
- completeCalled = true;
- cropImageView.onImageCroppingAsyncComplete(result);
- }
- }
- if (!completeCalled && result.bitmap != null) {
- // fast release of unused bitmap
- result.bitmap.recycle();
- }
+ }
+
+ /**
+ * Once complete, see if ImageView is still around and set bitmap.
+ *
+ * @param result the result of bitmap cropping
+ */
+ @Override
+ protected void onPostExecute(Result result) {
+ if (result != null) {
+ boolean completeCalled = false;
+ if (!isCancelled()) {
+ CropImageView cropImageView = mCropImageViewReference.get();
+ if (cropImageView != null) {
+ completeCalled = true;
+ cropImageView.onImageCroppingAsyncComplete(result);
}
+ }
+ if (!completeCalled && result.bitmap != null) {
+ // fast release of unused bitmap
+ result.bitmap.recycle();
+ }
}
+ }
- //region: Inner class: Result
-
- /**
- * The result of BitmapCroppingWorkerTask async loading.
- */
- public static final class Result {
-
- /**
- * The cropped bitmap
- */
- public final Bitmap bitmap;
-
- /**
- * The saved cropped bitmap uri
- */
- public final Uri uri;
-
- /**
- * The error that occurred during async bitmap cropping.
- */
- public final Exception error;
-
- /**
- * is the cropping request was to get a bitmap or to save it to uri
- */
- public final boolean isSave;
-
- /**
- * sample size used creating the crop bitmap to lower its size
- */
- public final int sampleSize;
-
- Result(Bitmap bitmap, int sampleSize) {
- this.bitmap = bitmap;
- this.uri = null;
- this.error = null;
- this.isSave = false;
- this.sampleSize = sampleSize;
- }
+ // region: Inner class: Result
- Result(Uri uri, int sampleSize) {
- this.bitmap = null;
- this.uri = uri;
- this.error = null;
- this.isSave = true;
- this.sampleSize = sampleSize;
- }
+ /** The result of BitmapCroppingWorkerTask async loading. */
+ static final class Result {
- Result(Exception error, boolean isSave) {
- this.bitmap = null;
- this.uri = null;
- this.error = error;
- this.isSave = isSave;
- this.sampleSize = 1;
- }
+ /** The cropped bitmap */
+ public final Bitmap bitmap;
+
+ /** The saved cropped bitmap uri */
+ public final Uri uri;
+
+ /** The error that occurred during async bitmap cropping. */
+ final Exception error;
+
+ /** is the cropping request was to get a bitmap or to save it to uri */
+ final boolean isSave;
+
+ /** sample size used creating the crop bitmap to lower its size */
+ final int sampleSize;
+
+ Result(Bitmap bitmap, int sampleSize) {
+ this.bitmap = bitmap;
+ this.uri = null;
+ this.error = null;
+ this.isSave = false;
+ this.sampleSize = sampleSize;
+ }
+
+ Result(Uri uri, int sampleSize) {
+ this.bitmap = null;
+ this.uri = uri;
+ this.error = null;
+ this.isSave = true;
+ this.sampleSize = sampleSize;
+ }
+
+ Result(Exception error, boolean isSave) {
+ this.bitmap = null;
+ this.uri = null;
+ this.error = error;
+ this.isSave = isSave;
+ this.sampleSize = 1;
}
- //endregion
+ }
+ // endregion
}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java
index 591c76ae..683d2ae2 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java
@@ -20,156 +20,131 @@
import java.lang.ref.WeakReference;
-/**
- * Task to load bitmap asynchronously from the UI thread.
- */
+/** Task to load bitmap asynchronously from the UI thread. */
final class BitmapLoadingWorkerTask extends AsyncTask {
- //region: Fields and Consts
+ // region: Fields and Consts
- /**
- * Use a WeakReference to ensure the ImageView can be garbage collected
- */
- private final WeakReference mCropImageViewReference;
+ /** Use a WeakReference to ensure the ImageView can be garbage collected */
+ private final WeakReference mCropImageViewReference;
- /**
- * The Android URI of the image to load
- */
- private final Uri mUri;
+ /** The Android URI of the image to load */
+ private final Uri mUri;
- /**
- * The context of the crop image view widget used for loading of bitmap by Android URI
- */
- private final Context mContext;
+ /** The context of the crop image view widget used for loading of bitmap by Android URI */
+ private final Context mContext;
- /**
- * required width of the cropping image after density adjustment
- */
- private final int mWidth;
+ /** required width of the cropping image after density adjustment */
+ private final int mWidth;
- /**
- * required height of the cropping image after density adjustment
- */
- private final int mHeight;
- //endregion
+ /** required height of the cropping image after density adjustment */
+ private final int mHeight;
+ // endregion
- public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri) {
- mUri = uri;
- mCropImageViewReference = new WeakReference<>(cropImageView);
+ public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri) {
+ mUri = uri;
+ mCropImageViewReference = new WeakReference<>(cropImageView);
- mContext = cropImageView.getContext();
+ mContext = cropImageView.getContext();
- DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics();
- double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1;
- mWidth = (int) (metrics.widthPixels * densityAdj);
- mHeight = (int) (metrics.heightPixels * densityAdj);
- }
+ DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics();
+ double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1;
+ mWidth = (int) (metrics.widthPixels * densityAdj);
+ mHeight = (int) (metrics.heightPixels * densityAdj);
+ }
- /**
- * The Android URI that this task is currently loading.
- */
- public Uri getUri() {
- return mUri;
- }
+ /** The Android URI that this task is currently loading. */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Decode image in background.
+ *
+ * @param params ignored
+ * @return the decoded bitmap data
+ */
+ @Override
+ protected Result doInBackground(Void... params) {
+ try {
+ if (!isCancelled()) {
+
+ BitmapUtils.BitmapSampled decodeResult =
+ BitmapUtils.decodeSampledBitmap(mContext, mUri, mWidth, mHeight);
+
+ if (!isCancelled()) {
- /**
- * Decode image in background.
- *
- * @param params ignored
- * @return the decoded bitmap data
- */
- @Override
- protected Result doInBackground(Void... params) {
- try {
- if (!isCancelled()) {
-
- BitmapUtils.BitmapSampled decodeResult =
- BitmapUtils.decodeSampledBitmap(mContext, mUri, mWidth, mHeight);
-
- if (!isCancelled()) {
-
- BitmapUtils.RotateBitmapResult rotateResult =
- BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, mContext, mUri);
-
- return new Result(mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees);
- }
- }
- return null;
- } catch (Exception e) {
- return new Result(mUri, e);
+ BitmapUtils.RotateBitmapResult rotateResult =
+ BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, mContext, mUri);
+
+ return new Result(
+ mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees);
}
+ }
+ return null;
+ } catch (Exception e) {
+ return new Result(mUri, e);
}
-
- /**
- * Once complete, see if ImageView is still around and set bitmap.
- *
- * @param result the result of bitmap loading
- */
- @Override
- protected void onPostExecute(Result result) {
- if (result != null) {
- boolean completeCalled = false;
- if (!isCancelled()) {
- CropImageView cropImageView = mCropImageViewReference.get();
- if (cropImageView != null) {
- completeCalled = true;
- cropImageView.onSetImageUriAsyncComplete(result);
- }
- }
- if (!completeCalled && result.bitmap != null) {
- // fast release of unused bitmap
- result.bitmap.recycle();
- }
+ }
+
+ /**
+ * Once complete, see if ImageView is still around and set bitmap.
+ *
+ * @param result the result of bitmap loading
+ */
+ @Override
+ protected void onPostExecute(Result result) {
+ if (result != null) {
+ boolean completeCalled = false;
+ if (!isCancelled()) {
+ CropImageView cropImageView = mCropImageViewReference.get();
+ if (cropImageView != null) {
+ completeCalled = true;
+ cropImageView.onSetImageUriAsyncComplete(result);
}
+ }
+ if (!completeCalled && result.bitmap != null) {
+ // fast release of unused bitmap
+ result.bitmap.recycle();
+ }
}
+ }
- //region: Inner class: Result
-
- /**
- * The result of BitmapLoadingWorkerTask async loading.
- */
- public static final class Result {
-
- /**
- * The Android URI of the image to load
- */
- public final Uri uri;
-
- /**
- * The loaded bitmap
- */
- public final Bitmap bitmap;
-
- /**
- * The sample size used to load the given bitmap
- */
- public final int loadSampleSize;
-
- /**
- * The degrees the image was rotated
- */
- public final int degreesRotated;
-
- /**
- * The error that occurred during async bitmap loading.
- */
- public final Exception error;
-
- Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) {
- this.uri = uri;
- this.bitmap = bitmap;
- this.loadSampleSize = loadSampleSize;
- this.degreesRotated = degreesRotated;
- this.error = null;
- }
+ // region: Inner class: Result
- Result(Uri uri, Exception error) {
- this.uri = uri;
- this.bitmap = null;
- this.loadSampleSize = 0;
- this.degreesRotated = 0;
- this.error = error;
- }
+ /** The result of BitmapLoadingWorkerTask async loading. */
+ public static final class Result {
+
+ /** The Android URI of the image to load */
+ public final Uri uri;
+
+ /** The loaded bitmap */
+ public final Bitmap bitmap;
+
+ /** The sample size used to load the given bitmap */
+ public final int loadSampleSize;
+
+ /** The degrees the image was rotated */
+ public final int degreesRotated;
+
+ /** The error that occurred during async bitmap loading. */
+ public final Exception error;
+
+ Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) {
+ this.uri = uri;
+ this.bitmap = bitmap;
+ this.loadSampleSize = loadSampleSize;
+ this.degreesRotated = degreesRotated;
+ this.error = null;
+ }
+
+ Result(Uri uri, Exception error) {
+ this.uri = uri;
+ this.bitmap = null;
+ this.loadSampleSize = 0;
+ this.degreesRotated = 0;
+ this.error = error;
}
- //endregion
+ }
+ // endregion
}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java
index c1d16d3b..023043a3 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java
@@ -14,16 +14,13 @@
import android.content.ContentResolver;
import android.content.Context;
-import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
-import android.media.ExifInterface;
import android.net.Uri;
-import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
@@ -40,686 +37,841 @@
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
-/**
- * Utility class that deals with operations with an ImageView.
- */
+import androidx.exifinterface.media.ExifInterface;
+
+/** Utility class that deals with operations with an ImageView. */
final class BitmapUtils {
- static final Rect EMPTY_RECT = new Rect();
-
- static final RectF EMPTY_RECT_F = new RectF();
-
- /**
- * Reusable rectangle for general internal usage
- */
- static final RectF RECT = new RectF();
-
- /**
- * Reusable point for general internal usage
- */
- static final float[] POINTS = new float[6];
-
- /**
- * Reusable point for general internal usage
- */
- static final float[] POINTS2 = new float[6];
-
- /**
- * Used to know the max texture size allowed to be rendered
- */
- static int mMaxTextureSize;
-
- /**
- * used to save bitmaps during state save and restore so not to reload them.
- */
- static Pair> mStateBitmap;
-
- /**
- * Rotate the given image by reading the Exif value of the image (uri).
- * If no rotation is required the image will not be rotated.
- * New bitmap is created and the old one is recycled.
- */
- public static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
- try {
- File file = getFileFromUri(context, uri);
- if (file.exists()) {
- ExifInterface ei = new ExifInterface(file.getAbsolutePath());
- return rotateBitmapByExif(bitmap, ei);
- }
- } catch (Exception ignored) {
- }
- return new RotateBitmapResult(bitmap, 0);
- }
+ static final Rect EMPTY_RECT = new Rect();
- /**
- * Rotate the given image by given Exif value.
- * If no rotation is required the image will not be rotated.
- * New bitmap is created and the old one is recycled.
- */
- public static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
- int degrees;
- int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
- switch (orientation) {
- case ExifInterface.ORIENTATION_ROTATE_90:
- degrees = 90;
- break;
- case ExifInterface.ORIENTATION_ROTATE_180:
- degrees = 180;
- break;
- case ExifInterface.ORIENTATION_ROTATE_270:
- degrees = 270;
- break;
- default:
- degrees = 0;
- break;
- }
- return new RotateBitmapResult(bitmap, degrees);
- }
+ static final RectF EMPTY_RECT_F = new RectF();
- /**
- * Decode bitmap from stream using sampling to get bitmap with the requested limit.
- */
- public static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
+ /** Reusable rectangle for general internal usage */
+ static final RectF RECT = new RectF();
- try {
- ContentResolver resolver = context.getContentResolver();
-
- // First decode with inJustDecodeBounds=true to check dimensions
- BitmapFactory.Options options = decodeImageForOption(resolver, uri);
+ /** Reusable point for general internal usage */
+ static final float[] POINTS = new float[6];
- // Calculate inSampleSize
- options.inSampleSize = Math.max(
- calculateInSampleSizeByReqestedSize(options.outWidth, options.outHeight, reqWidth, reqHeight),
- calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));
+ /** Reusable point for general internal usage */
+ static final float[] POINTS2 = new float[6];
- // Decode bitmap with inSampleSize set
- Bitmap bitmap = decodeImage(resolver, uri, options);
+ /** Used to know the max texture size allowed to be rendered */
+ private static int mMaxTextureSize;
- return new BitmapSampled(bitmap, options.inSampleSize);
+ /** used to save bitmaps during state save and restore so not to reload them. */
+ static Pair> mStateBitmap;
- } catch (Exception e) {
- throw new RuntimeException("Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
- }
+ /**
+ * Rotate the given image by reading the Exif value of the image (uri).
+ * If no rotation is required the image will not be rotated.
+ * New bitmap is created and the old one is recycled.
+ */
+ static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
+ ExifInterface ei = null;
+ try {
+ InputStream is = context.getContentResolver().openInputStream(uri);
+ if (is != null) {
+ ei = new ExifInterface(is);
+ is.close();
+ }
+ } catch (Exception ignored) {
}
+ return ei != null ? rotateBitmapByExif(bitmap, ei) : new RotateBitmapResult(bitmap, 0);
+ }
+
+ /**
+ * Rotate the given image by given Exif value.
+ * If no rotation is required the image will not be rotated.
+ * New bitmap is created and the old one is recycled.
+ */
+ static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
+ int degrees;
+ int orientation =
+ exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ degrees = 90;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ degrees = 180;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ degrees = 270;
+ break;
+ default:
+ degrees = 0;
+ break;
+ }
+ return new RotateBitmapResult(bitmap, degrees);
+ }
- /**
- * Crop image bitmap from given bitmap using the given points in the original bitmap and the given rotation.
- * if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the image that
- * contains the requires rectangle, rotate and then crop again a sub rectangle.
- */
- public static Bitmap cropBitmap(Bitmap bitmap, float[] points, int degreesRotated,
- boolean fixAspectRatio, int aspectRatioX, int aspectRatioY) {
-
- // get the rectangle in original image that contains the required cropped area (larger for non rectangular crop)
- Rect rect = getRectFromPoints(points, bitmap.getWidth(), bitmap.getHeight(), fixAspectRatio, aspectRatioX, aspectRatioY);
-
- // crop and rotate the cropped image in one operation
- Matrix matrix = new Matrix();
- matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
- Bitmap result = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);
-
- if (result == bitmap) {
- // corner case when all bitmap is selected, no worth optimizing for it
- result = bitmap.copy(bitmap.getConfig(), false);
- }
-
- // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
- if (degreesRotated % 90 != 0) {
+ /** Decode bitmap from stream using sampling to get bitmap with the requested limit. */
+ static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
- // extra crop because non rectangular crop cannot be done directly on the image without rotating first
- result = cropForRotatedImage(result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
- }
+ try {
+ ContentResolver resolver = context.getContentResolver();
- return result;
- }
+ // First decode with inJustDecodeBounds=true to check dimensions
+ BitmapFactory.Options options = decodeImageForOption(resolver, uri);
- /**
- * Crop image bitmap from URI by decoding it with specific width and height to down-sample if required.
- * Additionally if OOM is thrown try to increase the sampling (2,4,8).
- */
- public static BitmapSampled cropBitmap(Context context, Uri loadedImageUri, float[] points,
- int degreesRotated, int orgWidth, int orgHeight, boolean fixAspectRatio,
- int aspectRatioX, int aspectRatioY, int reqWidth, int reqHeight) {
- int sampleMulti = 1;
- while (true) {
- try {
- // if successful, just return the resulting bitmap
- return cropBitmap(context, loadedImageUri, points,
- degreesRotated, orgWidth, orgHeight, fixAspectRatio,
- aspectRatioX, aspectRatioY, reqWidth, reqHeight,
- sampleMulti);
- } catch (OutOfMemoryError e) {
- // if OOM try to increase the sampling to lower the memory usage
- sampleMulti *= 2;
- if (sampleMulti > 16) {
- throw new RuntimeException("Failed to handle OOM by sampling (" + sampleMulti + "): " + loadedImageUri + "\r\n" + e.getMessage(), e);
- }
- }
- }
- }
+ if(options.outWidth == -1 && options.outHeight == -1)
+ throw new RuntimeException("File is not a picture");
- /**
- * Get left value of the bounding rectangle of the given points.
- */
- public static float getRectLeft(float[] points) {
- return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
- }
+ // Calculate inSampleSize
+ options.inSampleSize =
+ Math.max(
+ calculateInSampleSizeByReqestedSize(
+ options.outWidth, options.outHeight, reqWidth, reqHeight),
+ calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));
- /**
- * Get top value of the bounding rectangle of the given points.
- */
- public static float getRectTop(float[] points) {
- return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
- }
+ // Decode bitmap with inSampleSize set
+ Bitmap bitmap = decodeImage(resolver, uri, options);
- /**
- * Get right value of the bounding rectangle of the given points.
- */
- public static float getRectRight(float[] points) {
- return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
- }
+ return new BitmapSampled(bitmap, options.inSampleSize);
- /**
- * Get bottom value of the bounding rectangle of the given points.
- */
- public static float getRectBottom(float[] points) {
- return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
}
-
- /**
- * Get width of the bounding rectangle of the given points.
- */
- public static float getRectWidth(float[] points) {
- return getRectRight(points) - getRectLeft(points);
+ }
+
+ /**
+ * Crop image bitmap from given bitmap using the given points in the original bitmap and the given
+ * rotation.
+ * if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
+ * image that contains the requires rectangle, rotate and then crop again a sub rectangle.
+ * If crop fails due to OOM we scale the cropping image by 0.5 every time it fails until it is
+ * small enough.
+ */
+ static BitmapSampled cropBitmapObjectHandleOOM(
+ Bitmap bitmap,
+ float[] points,
+ int degreesRotated,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ boolean flipHorizontally,
+ boolean flipVertically) {
+ int scale = 1;
+ while (true) {
+ try {
+ Bitmap cropBitmap =
+ cropBitmapObjectWithScale(
+ bitmap,
+ points,
+ degreesRotated,
+ fixAspectRatio,
+ aspectRatioX,
+ aspectRatioY,
+ 1 / (float) scale,
+ flipHorizontally,
+ flipVertically);
+ return new BitmapSampled(cropBitmap, scale);
+ } catch (OutOfMemoryError e) {
+ scale *= 2;
+ if (scale > 8) {
+ throw e;
+ }
+ }
}
-
- /**
- * Get heightof the bounding rectangle of the given points.
- */
- public static float getRectHeight(float[] points) {
- return getRectBottom(points) - getRectTop(points);
+ }
+
+ /**
+ * Crop image bitmap from given bitmap using the given points in the original bitmap and the given
+ * rotation.
+ * if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
+ * image that contains the requires rectangle, rotate and then crop again a sub rectangle.
+ *
+ * @param scale how much to scale the cropped image part, use 0.5 to lower the image by half (OOM
+ * handling)
+ */
+ private static Bitmap cropBitmapObjectWithScale(
+ Bitmap bitmap,
+ float[] points,
+ int degreesRotated,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ float scale,
+ boolean flipHorizontally,
+ boolean flipVertically) {
+
+ // get the rectangle in original image that contains the required cropped area (larger for non
+ // rectangular crop)
+ Rect rect =
+ getRectFromPoints(
+ points,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ fixAspectRatio,
+ aspectRatioX,
+ aspectRatioY);
+
+ // crop and rotate the cropped image in one operation
+ Matrix matrix = new Matrix();
+ matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
+ matrix.postScale(flipHorizontally ? -scale : scale, flipVertically ? -scale : scale);
+ Bitmap result =
+ Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);
+
+ if (result == bitmap) {
+ // corner case when all bitmap is selected, no worth optimizing for it
+ result = bitmap.copy(bitmap.getConfig(), false);
}
- /**
- * Get horizontal center value of the bounding rectangle of the given points.
- */
- public static float getRectCenterX(float[] points) {
- return (getRectRight(points) + getRectLeft(points)) / 2f;
- }
+ // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
+ if (degreesRotated % 90 != 0) {
- /**
- * Get verical center value of the bounding rectangle of the given points.
- */
- public static float getRectCenterY(float[] points) {
- return (getRectBottom(points) + getRectTop(points)) / 2f;
+ // extra crop because non rectangular crop cannot be done directly on the image without
+ // rotating first
+ result =
+ cropForRotatedImage(
+ result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
}
- /**
- * Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2 points that
- * contains the given 4 points and is a stright rectangle.
- */
- public static Rect getRectFromPoints(float[] points, int imageWidth, int imageHeight, boolean fixAspectRatio, int aspectRatioX, int aspectRatioY) {
- int left = Math.round(Math.max(0, getRectLeft(points)));
- int top = Math.round(Math.max(0, getRectTop(points)));
- int right = Math.round(Math.min(imageWidth, getRectRight(points)));
- int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));
-
- Rect rect = new Rect(left, top, right, bottom);
- if (fixAspectRatio) {
- fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
+ return result;
+ }
+
+ /**
+ * Crop image bitmap from URI by decoding it with specific width and height to down-sample if
+ * required.
+ * Additionally if OOM is thrown try to increase the sampling (2,4,8).
+ */
+ static BitmapSampled cropBitmap(
+ Context context,
+ Uri loadedImageUri,
+ float[] points,
+ int degreesRotated,
+ int orgWidth,
+ int orgHeight,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ int reqWidth,
+ int reqHeight,
+ boolean flipHorizontally,
+ boolean flipVertically) {
+ int sampleMulti = 1;
+ while (true) {
+ try {
+ // if successful, just return the resulting bitmap
+ return cropBitmap(
+ context,
+ loadedImageUri,
+ points,
+ degreesRotated,
+ orgWidth,
+ orgHeight,
+ fixAspectRatio,
+ aspectRatioX,
+ aspectRatioY,
+ reqWidth,
+ reqHeight,
+ flipHorizontally,
+ flipVertically,
+ sampleMulti);
+ } catch (OutOfMemoryError e) {
+ // if OOM try to increase the sampling to lower the memory usage
+ sampleMulti *= 2;
+ if (sampleMulti > 16) {
+ throw new RuntimeException(
+ "Failed to handle OOM by sampling ("
+ + sampleMulti
+ + "): "
+ + loadedImageUri
+ + "\r\n"
+ + e.getMessage(),
+ e);
}
-
- return rect;
+ }
}
-
- /**
- * Fix the given rectangle if it doesn't confirm to aspect ration rule.
- * Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
- */
- public static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
- if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
- if (rect.height() > rect.width()) {
- rect.bottom -= rect.height() - rect.width();
- } else {
- rect.right -= rect.width() - rect.height();
- }
- }
+ }
+
+ /** Get left value of the bounding rectangle of the given points. */
+ static float getRectLeft(float[] points) {
+ return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
+ }
+
+ /** Get top value of the bounding rectangle of the given points. */
+ static float getRectTop(float[] points) {
+ return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
+ }
+
+ /** Get right value of the bounding rectangle of the given points. */
+ static float getRectRight(float[] points) {
+ return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
+ }
+
+ /** Get bottom value of the bounding rectangle of the given points. */
+ static float getRectBottom(float[] points) {
+ return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
+ }
+
+ /** Get width of the bounding rectangle of the given points. */
+ static float getRectWidth(float[] points) {
+ return getRectRight(points) - getRectLeft(points);
+ }
+
+ /** Get height of the bounding rectangle of the given points. */
+ static float getRectHeight(float[] points) {
+ return getRectBottom(points) - getRectTop(points);
+ }
+
+ /** Get horizontal center value of the bounding rectangle of the given points. */
+ static float getRectCenterX(float[] points) {
+ return (getRectRight(points) + getRectLeft(points)) / 2f;
+ }
+
+ /** Get vertical center value of the bounding rectangle of the given points. */
+ static float getRectCenterY(float[] points) {
+ return (getRectBottom(points) + getRectTop(points)) / 2f;
+ }
+
+ /**
+ * Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2
+ * points that contains the given 4 points and is a straight rectangle.
+ */
+ static Rect getRectFromPoints(
+ float[] points,
+ int imageWidth,
+ int imageHeight,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY) {
+ int left = Math.round(Math.max(0, getRectLeft(points)));
+ int top = Math.round(Math.max(0, getRectTop(points)));
+ int right = Math.round(Math.min(imageWidth, getRectRight(points)));
+ int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));
+
+ Rect rect = new Rect(left, top, right, bottom);
+ if (fixAspectRatio) {
+ fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
}
- /**
- * Write the given bitmap to the given uri using the given compression.
- */
- public static void writeBitmapToUri(Context context, Bitmap bitmap, Uri uri, Bitmap.CompressFormat compressFormat, int compressQuality) throws FileNotFoundException {
- OutputStream outputStream = null;
- try {
- outputStream = context.getContentResolver().openOutputStream(uri);
- bitmap.compress(compressFormat, compressQuality, outputStream);
- } finally {
- closeSafe(outputStream);
- }
+ return rect;
+ }
+
+ /**
+ * Fix the given rectangle if it doesn't confirm to aspect ration rule.
+ * Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
+ */
+ private static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
+ if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
+ if (rect.height() > rect.width()) {
+ rect.bottom -= rect.height() - rect.width();
+ } else {
+ rect.right -= rect.width() - rect.height();
+ }
}
-
- /**
- * Resize the given bitmap to the given width/height by the given option.
- */
- public static Bitmap resizeBitmap(Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
- try {
- if (reqWidth > 0 && reqHeight > 0 && (options == CropImageView.RequestSizeOptions.RESIZE_FIT ||
- options == CropImageView.RequestSizeOptions.RESIZE_INSIDE ||
- options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {
-
- Bitmap resized = null;
- if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
- resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
- } else {
- int width = bitmap.getWidth();
- int height = bitmap.getHeight();
- float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
- if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
- resized = Bitmap.createScaledBitmap(bitmap, (int) (width / scale), (int) (height / scale), false);
- }
- }
- if (resized != null) {
- if (resized != bitmap) {
- bitmap.recycle();
- }
- return resized;
- }
- }
- } catch (Exception e) {
- Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
+ }
+
+ /**
+ * Write given bitmap to a temp file. If file already exists no-op as we already saved the file in
+ * this session. Uses JPEG 95% compression.
+ *
+ * @param uri the uri to write the bitmap to, if null
+ * @return the uri where the image was saved in, either the given uri or new pointing to temp
+ * file.
+ */
+ static Uri writeTempStateStoreBitmap(Context context, Bitmap bitmap, Uri uri) {
+ try {
+ boolean needSave = true;
+ if (uri == null) {
+ uri =
+ Uri.fromFile(
+ File.createTempFile("aic_state_store_temp", ".jpg", context.getCacheDir()));
+ } else if (new File(uri.getPath()).exists()) {
+ needSave = false;
+ }
+ if (needSave) {
+ writeBitmapToUri(context, bitmap, uri, Bitmap.CompressFormat.JPEG, 95);
+ }
+ return uri;
+ } catch (Exception e) {
+ Log.w("AIC", "Failed to write bitmap to temp file for image-cropper save instance state", e);
+ return null;
+ }
+ }
+
+ /** Write the given bitmap to the given uri using the given compression. */
+ static void writeBitmapToUri(
+ Context context,
+ Bitmap bitmap,
+ Uri uri,
+ Bitmap.CompressFormat compressFormat,
+ int compressQuality)
+ throws FileNotFoundException {
+ OutputStream outputStream = null;
+ try {
+ outputStream = context.getContentResolver().openOutputStream(uri);
+ bitmap.compress(compressFormat, compressQuality, outputStream);
+ } finally {
+ closeSafe(outputStream);
+ }
+ }
+
+ /** Resize the given bitmap to the given width/height by the given option. */
+ static Bitmap resizeBitmap(
+ Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
+ try {
+ if (reqWidth > 0
+ && reqHeight > 0
+ && (options == CropImageView.RequestSizeOptions.RESIZE_FIT
+ || options == CropImageView.RequestSizeOptions.RESIZE_INSIDE
+ || options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {
+
+ Bitmap resized = null;
+ if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
+ resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
+ } else {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
+ if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
+ resized =
+ Bitmap.createScaledBitmap(
+ bitmap, (int) (width / scale), (int) (height / scale), false);
+ }
}
- return bitmap;
+ if (resized != null) {
+ if (resized != bitmap) {
+ bitmap.recycle();
+ }
+ return resized;
+ }
+ }
+ } catch (Exception e) {
+ Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
+ }
+ return bitmap;
+ }
+
+ // region: Private methods
+
+ /**
+ * Crop image bitmap from URI by decoding it with specific width and height to down-sample if
+ * required.
+ *
+ * @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
+ * @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
+ * @param sampleMulti used to increase the sampling of the image to handle memory issues.
+ */
+ private static BitmapSampled cropBitmap(
+ Context context,
+ Uri loadedImageUri,
+ float[] points,
+ int degreesRotated,
+ int orgWidth,
+ int orgHeight,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ int reqWidth,
+ int reqHeight,
+ boolean flipHorizontally,
+ boolean flipVertically,
+ int sampleMulti) {
+
+ // get the rectangle in original image that contains the required cropped area (larger for non
+ // rectangular crop)
+ Rect rect =
+ getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);
+
+ int width = reqWidth > 0 ? reqWidth : rect.width();
+ int height = reqHeight > 0 ? reqHeight : rect.height();
+
+ Bitmap result = null;
+ int sampleSize = 1;
+ try {
+ // decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is
+ // given.
+ BitmapSampled bitmapSampled =
+ decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
+ result = bitmapSampled.bitmap;
+ sampleSize = bitmapSampled.sampleSize;
+ } catch (Exception ignored) {
}
- //region: Private methods
-
- /**
- * Crop image bitmap from URI by decoding it with specific width and height to down-sample if required.
- *
- * @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
- * @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
- * @param sampleMulti used to increase the sampling of the image to handle memory issues.
- */
- private static BitmapSampled cropBitmap(Context context, Uri loadedImageUri, float[] points,
- int degreesRotated, int orgWidth, int orgHeight, boolean fixAspectRatio,
- int aspectRatioX, int aspectRatioY, int reqWidth, int reqHeight, int sampleMulti) {
-
- // get the rectangle in original image that contains the required cropped area (larger for non rectangular crop)
- Rect rect = getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);
+ if (result != null) {
+ try {
+ // rotate the decoded region by the required amount
+ result = rotateAndFlipBitmapInt(result, degreesRotated, flipHorizontally, flipVertically);
- int width = reqWidth > 0 ? reqWidth : rect.width();
- int height = reqHeight > 0 ? reqHeight : rect.height();
+ // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
+ if (degreesRotated % 90 != 0) {
- Bitmap result = null;
- int sampleSize = 1;
- try {
- // decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is given.
- BitmapSampled bitmapSampled = decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
- result = bitmapSampled.bitmap;
- sampleSize = bitmapSampled.sampleSize;
- } catch (Exception e) {
+ // extra crop because non rectangular crop cannot be done directly on the image without
+ // rotating first
+ result =
+ cropForRotatedImage(
+ result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
}
-
+ } catch (OutOfMemoryError e) {
if (result != null) {
- try {
- // rotate the decoded region by the required amount
- result = rotateBitmapInt(result, degreesRotated);
-
- // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
- if (degreesRotated % 90 != 0) {
-
- // extra crop because non rectangular crop cannot be done directly on the image without rotating first
- result = cropForRotatedImage(result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
- }
- } catch (OutOfMemoryError e) {
- if (result != null) {
- result.recycle();
- }
- throw e;
- }
- return new BitmapSampled(result, sampleSize);
- } else {
- // failed to decode region, may be skia issue, try full decode and then crop
- return cropBitmap(context, loadedImageUri, points, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY, sampleMulti, rect, width, height);
- }
- }
-
- /**
- * Crop bitmap by fully loading the original and then cropping it, fallback in case cropping region failed.
- */
- private static BitmapSampled cropBitmap(Context context, Uri loadedImageUri, float[] points,
- int degreesRotated, boolean fixAspectRatio, int aspectRatioX, int aspectRatioY,
- int sampleMulti, Rect rect, int width, int height) {
- Bitmap result = null;
- int sampleSize;
- try {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = sampleSize = sampleMulti * calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);
-
- Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
- if (fullBitmap != null) {
- try {
- // adjust crop points by the sampling because the image is smaller
- float[] points2 = new float[points.length];
- System.arraycopy(points, 0, points2, 0, points.length);
- for (int i = 0; i < points2.length; i++) {
- points2[i] = points2[i] / options.inSampleSize;
- }
-
- result = cropBitmap(fullBitmap, points2, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
- } finally {
- if (result != fullBitmap) {
- fullBitmap.recycle();
- }
- }
- }
- } catch (OutOfMemoryError e) {
- if (result != null) {
- result.recycle();
- }
- throw e;
- } catch (Exception e) {
- throw new RuntimeException("Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
+ result.recycle();
}
- return new BitmapSampled(result, sampleSize);
+ throw e;
+ }
+ return new BitmapSampled(result, sampleSize);
+ } else {
+ // failed to decode region, may be skia issue, try full decode and then crop
+ return cropBitmap(
+ context,
+ loadedImageUri,
+ points,
+ degreesRotated,
+ fixAspectRatio,
+ aspectRatioX,
+ aspectRatioY,
+ sampleMulti,
+ rect,
+ width,
+ height,
+ flipHorizontally,
+ flipVertically);
}
-
- /**
- * Decode image from uri using "inJustDecodeBounds" to get the image dimensions.
- */
- private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri) throws FileNotFoundException {
- InputStream stream = null;
+ }
+
+ /**
+ * Crop bitmap by fully loading the original and then cropping it, fallback in case cropping
+ * region failed.
+ */
+ private static BitmapSampled cropBitmap(
+ Context context,
+ Uri loadedImageUri,
+ float[] points,
+ int degreesRotated,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY,
+ int sampleMulti,
+ Rect rect,
+ int width,
+ int height,
+ boolean flipHorizontally,
+ boolean flipVertically) {
+ Bitmap result = null;
+ int sampleSize;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize =
+ sampleSize =
+ sampleMulti
+ * calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);
+
+ Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
+ if (fullBitmap != null) {
try {
- stream = resolver.openInputStream(uri);
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
- options.inJustDecodeBounds = false;
- return options;
+ // adjust crop points by the sampling because the image is smaller
+ float[] points2 = new float[points.length];
+ System.arraycopy(points, 0, points2, 0, points.length);
+ for (int i = 0; i < points2.length; i++) {
+ points2[i] = points2[i] / options.inSampleSize;
+ }
+
+ result =
+ cropBitmapObjectWithScale(
+ fullBitmap,
+ points2,
+ degreesRotated,
+ fixAspectRatio,
+ aspectRatioX,
+ aspectRatioY,
+ 1,
+ flipHorizontally,
+ flipVertically);
} finally {
- closeSafe(stream);
+ if (result != fullBitmap) {
+ fullBitmap.recycle();
+ }
}
+ }
+ } catch (OutOfMemoryError e) {
+ if (result != null) {
+ result.recycle();
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
}
-
- /**
- * Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
- * the inSampleSize until success.
- */
- private static Bitmap decodeImage(ContentResolver resolver, Uri uri, BitmapFactory.Options options) throws FileNotFoundException {
- do {
- InputStream stream = null;
- try {
- stream = resolver.openInputStream(uri);
- return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
- } catch (OutOfMemoryError e) {
- options.inSampleSize *= 2;
- } finally {
- closeSafe(stream);
- }
- } while (options.inSampleSize <= 512);
- throw new RuntimeException("Failed to decode image: " + uri);
+ return new BitmapSampled(result, sampleSize);
+ }
+
+ /** Decode image from uri using "inJustDecodeBounds" to get the image dimensions. */
+ private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri)
+ throws FileNotFoundException {
+ InputStream stream = null;
+ try {
+ stream = resolver.openInputStream(uri);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
+ options.inJustDecodeBounds = false;
+ return options;
+ } finally {
+ closeSafe(stream);
}
-
- /**
- * Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested limit.
- *
- * @param sampleMulti used to increase the sampling of the image to handle memory issues.
- */
- private static BitmapSampled decodeSampledBitmapRegion(Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
- InputStream stream = null;
- BitmapRegionDecoder decoder = null;
+ }
+
+ /**
+ * Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
+ * the inSampleSize until success.
+ */
+ private static Bitmap decodeImage(
+ ContentResolver resolver, Uri uri, BitmapFactory.Options options)
+ throws FileNotFoundException {
+ do {
+ InputStream stream = null;
+ try {
+ stream = resolver.openInputStream(uri);
+ return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
+ } catch (OutOfMemoryError e) {
+ options.inSampleSize *= 2;
+ } finally {
+ closeSafe(stream);
+ }
+ } while (options.inSampleSize <= 512);
+ throw new RuntimeException("Failed to decode image: " + uri);
+ }
+
+ /**
+ * Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested
+ * limit.
+ *
+ * @param sampleMulti used to increase the sampling of the image to handle memory issues.
+ */
+ private static BitmapSampled decodeSampledBitmapRegion(
+ Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
+ InputStream stream = null;
+ BitmapRegionDecoder decoder = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize =
+ sampleMulti
+ * calculateInSampleSizeByReqestedSize(
+ rect.width(), rect.height(), reqWidth, reqHeight);
+
+ stream = context.getContentResolver().openInputStream(uri);
+ decoder = BitmapRegionDecoder.newInstance(stream, false);
+ do {
try {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = sampleMulti * calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), reqWidth, reqHeight);
-
- stream = context.getContentResolver().openInputStream(uri);
- decoder = BitmapRegionDecoder.newInstance(stream, false);
- do {
- try {
- return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
- } catch (OutOfMemoryError e) {
- options.inSampleSize *= 2;
- }
- } while (options.inSampleSize <= 512);
- } catch (Exception e) {
- throw new RuntimeException("Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
- } finally {
- closeSafe(stream);
- if (decoder != null) {
- decoder.recycle();
- }
+ return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
+ } catch (OutOfMemoryError e) {
+ options.inSampleSize *= 2;
}
- return new BitmapSampled(null, 1);
+ } while (options.inSampleSize <= 512);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
+ } finally {
+ closeSafe(stream);
+ if (decoder != null) {
+ decoder.recycle();
+ }
}
-
- /**
- * Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap contains parts
- * beyond the required crop area, this method crops the already cropped and rotated bitmap to the final
- * rectangle.
- * Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
- */
- private static Bitmap cropForRotatedImage(Bitmap bitmap, float[] points, Rect rect, int degreesRotated,
- boolean fixAspectRatio, int aspectRatioX, int aspectRatioY) {
- if (degreesRotated % 90 != 0) {
-
- int adjLeft = 0, adjTop = 0, width = 0, height = 0;
- double rads = Math.toRadians(degreesRotated);
- int compareTo = degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270) ? rect.left : rect.right;
- for (int i = 0; i < points.length; i += 2) {
- if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
- adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
- adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
- width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
- height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
- break;
- }
- }
-
- rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
- if (fixAspectRatio) {
- fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
- }
-
- Bitmap bitmapTmp = bitmap;
- bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
- if (bitmapTmp != bitmap) {
- bitmapTmp.recycle();
- }
+ return new BitmapSampled(null, 1);
+ }
+
+ /**
+ * Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap
+ * contains parts beyond the required crop area, this method crops the already cropped and rotated
+ * bitmap to the final rectangle.
+ * Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
+ */
+ private static Bitmap cropForRotatedImage(
+ Bitmap bitmap,
+ float[] points,
+ Rect rect,
+ int degreesRotated,
+ boolean fixAspectRatio,
+ int aspectRatioX,
+ int aspectRatioY) {
+ if (degreesRotated % 90 != 0) {
+
+ int adjLeft = 0, adjTop = 0, width = 0, height = 0;
+ double rads = Math.toRadians(degreesRotated);
+ int compareTo =
+ degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270)
+ ? rect.left
+ : rect.right;
+ for (int i = 0; i < points.length; i += 2) {
+ if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
+ adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
+ adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
+ width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
+ height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
+ break;
}
- return bitmap;
+ }
+
+ rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
+ if (fixAspectRatio) {
+ fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
+ }
+
+ Bitmap bitmapTmp = bitmap;
+ bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
+ if (bitmapTmp != bitmap) {
+ bitmapTmp.recycle();
+ }
}
-
- /**
- * Calculate the largest inSampleSize value that is a power of 2 and keeps both
- * height and width larger than the requested height and width.
- */
- private static int calculateInSampleSizeByReqestedSize(int width, int height, int reqWidth, int reqHeight) {
- int inSampleSize = 1;
- if (height > reqHeight || width > reqWidth) {
- while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
- inSampleSize *= 2;
- }
- }
- return inSampleSize;
+ return bitmap;
+ }
+
+ /**
+ * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
+ * larger than the requested height and width.
+ */
+ private static int calculateInSampleSizeByReqestedSize(
+ int width, int height, int reqWidth, int reqHeight) {
+ int inSampleSize = 1;
+ if (height > reqHeight || width > reqWidth) {
+ while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
+ inSampleSize *= 2;
+ }
}
-
- /**
- * Calculate the largest inSampleSize value that is a power of 2 and keeps both
- * height and width smaller than max texture size allowed for the device.
- */
- private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
- int inSampleSize = 1;
- if (mMaxTextureSize == 0) {
- mMaxTextureSize = getMaxTextureSize();
- }
- if (mMaxTextureSize > 0) {
- while ((height / inSampleSize) > mMaxTextureSize || (width / inSampleSize) > mMaxTextureSize) {
- inSampleSize *= 2;
- }
- }
- return inSampleSize;
+ return inSampleSize;
+ }
+
+ /**
+ * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
+ * smaller than max texture size allowed for the device.
+ */
+ private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
+ int inSampleSize = 1;
+ if (mMaxTextureSize == 0) {
+ mMaxTextureSize = getMaxTextureSize();
}
-
- /**
- * Get {@link File} object for the given Android URI.
- * Use content resolver to get real path if direct path doesn't return valid file.
- */
- private static File getFileFromUri(Context context, Uri uri) {
-
- // first try by direct path
- File file = new File(uri.getPath());
- if (file.exists()) {
- return file;
- }
-
- // try reading real path from content resolver (gallery images)
- Cursor cursor = null;
- try {
- String[] proj = {MediaStore.Images.Media.DATA};
- cursor = context.getContentResolver().query(uri, proj, null, null, null);
- int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
- cursor.moveToFirst();
- String realPath = cursor.getString(column_index);
- file = new File(realPath);
- } catch (Exception ignored) {
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return file;
+ if (mMaxTextureSize > 0) {
+ while ((height / inSampleSize) > mMaxTextureSize
+ || (width / inSampleSize) > mMaxTextureSize) {
+ inSampleSize *= 2;
+ }
}
-
- /**
- * Rotate the given bitmap by the given degrees.
- * New bitmap is created and the old one is recycled.
- */
- private static Bitmap rotateBitmapInt(Bitmap bitmap, int degrees) {
- if (degrees > 0) {
- Matrix matrix = new Matrix();
- matrix.setRotate(degrees);
- Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
- if (newBitmap != bitmap) {
- bitmap.recycle();
- }
- return newBitmap;
- } else {
- return bitmap;
- }
+ return inSampleSize;
+ }
+
+ /**
+ * Rotate the given bitmap by the given degrees.
+ * New bitmap is created and the old one is recycled.
+ */
+ private static Bitmap rotateAndFlipBitmapInt(
+ Bitmap bitmap, int degrees, boolean flipHorizontally, boolean flipVertically) {
+ if (degrees > 0 || flipHorizontally || flipVertically) {
+ Matrix matrix = new Matrix();
+ matrix.setRotate(degrees);
+ matrix.postScale(flipHorizontally ? -1 : 1, flipVertically ? -1 : 1);
+ Bitmap newBitmap =
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
+ if (newBitmap != bitmap) {
+ bitmap.recycle();
+ }
+ return newBitmap;
+ } else {
+ return bitmap;
}
+ }
+
+ /**
+ * Get the max size of bitmap allowed to be rendered on the device.
+ * http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
+ */
+ private static int getMaxTextureSize() {
+ // Safe minimum default size
+ final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
+
+ try {
+ // Get EGL Display
+ EGL10 egl = (EGL10) EGLContext.getEGL();
+ EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ // Initialise
+ int[] version = new int[2];
+ egl.eglInitialize(display, version);
+
+ // Query total number of configurations
+ int[] totalConfigurations = new int[1];
+ egl.eglGetConfigs(display, null, 0, totalConfigurations);
+
+ // Query actual list configurations
+ EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
+ egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
+
+ int[] textureSize = new int[1];
+ int maximumTextureSize = 0;
+
+ // Iterate through all the configurations to located the maximum texture size
+ for (int i = 0; i < totalConfigurations[0]; i++) {
+ // Only need to check for width since opengl textures are always squared
+ egl.eglGetConfigAttrib(
+ display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
+
+ // Keep track of the maximum texture size
+ if (maximumTextureSize < textureSize[0]) {
+ maximumTextureSize = textureSize[0];
+ }
+ }
- /**
- * Get the max size of bitmap allowed to be rendered on the device.
- * http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
- */
- private static int getMaxTextureSize() {
- // Safe minimum default size
- final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
+ // Release
+ egl.eglTerminate(display);
- try {
- // Get EGL Display
- EGL10 egl = (EGL10) EGLContext.getEGL();
- EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
-
- // Initialise
- int[] version = new int[2];
- egl.eglInitialize(display, version);
-
- // Query total number of configurations
- int[] totalConfigurations = new int[1];
- egl.eglGetConfigs(display, null, 0, totalConfigurations);
-
- // Query actual list configurations
- EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
- egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
-
- int[] textureSize = new int[1];
- int maximumTextureSize = 0;
-
- // Iterate through all the configurations to located the maximum texture size
- for (int i = 0; i < totalConfigurations[0]; i++) {
- // Only need to check for width since opengl textures are always squared
- egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
-
- // Keep track of the maximum texture size
- if (maximumTextureSize < textureSize[0]) {
- maximumTextureSize = textureSize[0];
- }
- }
-
- // Release
- egl.eglTerminate(display);
-
- // Return largest texture size found, or default
- return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
- } catch (Exception e) {
- return IMAGE_MAX_BITMAP_DIMENSION;
- }
+ // Return largest texture size found, or default
+ return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
+ } catch (Exception e) {
+ return IMAGE_MAX_BITMAP_DIMENSION;
}
-
- /**
- * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
- * exception thrown.
- *
- * @param closeable the closable object to close
- */
- private static void closeSafe(Closeable closeable) {
- if (closeable != null) {
- try {
- closeable.close();
- } catch (IOException ignored) {
- }
- }
+ }
+
+ /**
+ * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
+ * exception thrown.
+ *
+ * @param closeable the closable object to close
+ */
+ private static void closeSafe(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException ignored) {
+ }
}
- //endregion
+ }
+ // endregion
- //region: Inner class: BitmapSampled
+ // region: Inner class: BitmapSampled
- /**
- * Holds bitmap instance and the sample size that the bitmap was loaded/cropped with.
- */
- public static final class BitmapSampled {
+ /** Holds bitmap instance and the sample size that the bitmap was loaded/cropped with. */
+ static final class BitmapSampled {
- /**
- * The bitmap instance
- */
- public final Bitmap bitmap;
+ /** The bitmap instance */
+ public final Bitmap bitmap;
- /**
- * The sample size used to lower the size of the bitmap (1,2,4,8,...)
- */
- public final int sampleSize;
+ /** The sample size used to lower the size of the bitmap (1,2,4,8,...) */
+ final int sampleSize;
- public BitmapSampled(Bitmap bitmap, int sampleSize) {
- this.bitmap = bitmap;
- this.sampleSize = sampleSize;
- }
+ BitmapSampled(Bitmap bitmap, int sampleSize) {
+ this.bitmap = bitmap;
+ this.sampleSize = sampleSize;
}
- //endregion
+ }
+ // endregion
- //region: Inner class: RotateBitmapResult
+ // region: Inner class: RotateBitmapResult
- /**
- * The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, android.media.ExifInterface)}.
- */
- public static final class RotateBitmapResult {
+ /** The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, ExifInterface)}. */
+ static final class RotateBitmapResult {
- /**
- * The loaded bitmap
- */
- public final Bitmap bitmap;
+ /** The loaded bitmap */
+ public final Bitmap bitmap;
- /**
- * The degrees the image was rotated
- */
- public final int degrees;
+ /** The degrees the image was rotated */
+ final int degrees;
- RotateBitmapResult(Bitmap bitmap, int degrees) {
- this.bitmap = bitmap;
- this.degrees = degrees;
- }
+ RotateBitmapResult(Bitmap bitmap, int degrees) {
+ this.bitmap = bitmap;
+ this.degrees = degrees;
}
- //endregion
-}
\ No newline at end of file
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java
index a7be473a..ba8b807b 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java
@@ -30,828 +30,989 @@
import android.graphics.RectF;
import android.net.Uri;
import android.os.Build;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.fragment.app.Fragment;
+
/**
- * Helper to simplify crop image work like starting pick-image acitvity and handling camera/gallery intents.
- * The goal of the helper is to simplify the starting and most-common usage of image cropping and not
- * all porpose all possible scenario one-to-rule-them-all code base. So feel free to use it as is and as
- * a wiki to make your own.
- * Added value you get out-of-the-box is some edge case handling that you may miss otherwise, like the
- * stupid-ass Android camera result URI that may differ from version to version and from device to device.
+ * Helper to simplify crop image work like starting pick-image acitvity and handling camera/gallery
+ * intents.
+ * The goal of the helper is to simplify the starting and most-common usage of image cropping and
+ * not all porpose all possible scenario one-to-rule-them-all code base. So feel free to use it as
+ * is and as a wiki to make your own.
+ * Added value you get out-of-the-box is some edge case handling that you may miss otherwise, like
+ * the stupid-ass Android camera result URI that may differ from version to version and from device
+ * to device.
*/
+@SuppressWarnings("WeakerAccess, unused")
public final class CropImage {
- //region: Fields and Consts
+ // region: Fields and Consts
+
+ /** The key used to pass crop image source URI to {@link CropImageActivity}. */
+ public static final String CROP_IMAGE_EXTRA_SOURCE = "CROP_IMAGE_EXTRA_SOURCE";
+
+ /** The key used to pass crop image options to {@link CropImageActivity}. */
+ public static final String CROP_IMAGE_EXTRA_OPTIONS = "CROP_IMAGE_EXTRA_OPTIONS";
+
+ /** The key used to pass crop image bundle data to {@link CropImageActivity}. */
+ public static final String CROP_IMAGE_EXTRA_BUNDLE = "CROP_IMAGE_EXTRA_BUNDLE";
+
+ /** The key used to pass crop image result data back from {@link CropImageActivity}. */
+ public static final String CROP_IMAGE_EXTRA_RESULT = "CROP_IMAGE_EXTRA_RESULT";
+
+ /**
+ * The request code used to start pick image activity to be used on result to identify the this
+ * specific request.
+ */
+ public static final int PICK_IMAGE_CHOOSER_REQUEST_CODE = 200;
+
+ /** The request code used to request permission to pick image from external storage. */
+ public static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 201;
+
+ /** The request code used to request permission to capture image from camera. */
+ public static final int CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE = 2011;
+
+ /**
+ * The request code used to start {@link CropImageActivity} to be used on result to identify the
+ * this specific request.
+ */
+ public static final int CROP_IMAGE_ACTIVITY_REQUEST_CODE = 203;
+
+ /** The result code used to return error from {@link CropImageActivity}. */
+ public static final int CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE = 204;
+ // endregion
+
+ private CropImage() {}
+
+ /**
+ * Create a new bitmap that has all pixels beyond the oval shape transparent. Old bitmap is
+ * recycled.
+ */
+ public static Bitmap toOvalBitmap(@NonNull Bitmap bitmap) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(output);
+
+ int color = 0xff424242;
+ Paint paint = new Paint();
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+
+ RectF rect = new RectF(0, 0, width, height);
+ canvas.drawOval(rect, paint);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+
+ bitmap.recycle();
+
+ return output;
+ }
+
+ /**
+ * Start an activity to get image for cropping using chooser intent that will have all the
+ * available applications for the device like camera (MyCamera), galery (Photos), store apps
+ * (Dropbox), etc.
+ * Use "pick_image_intent_chooser_title" string resource to override pick chooser title.
+ *
+ * @param activity the activity to be used to start activity from
+ */
+ public static void startPickImageActivity(@NonNull Activity activity) {
+ activity.startActivityForResult(
+ getPickImageChooserIntent(activity), PICK_IMAGE_CHOOSER_REQUEST_CODE);
+ }
+
+ /**
+ * Same as {@link #startPickImageActivity(Activity) startPickImageActivity} method but instead of
+ * being called and returning to an Activity, this method can be called and return to a Fragment.
+ *
+ * @param context The Fragments context. Use getContext()
+ * @param fragment The calling Fragment to start and return the image to
+ */
+ public static void startPickImageActivity(@NonNull Context context, @NonNull Fragment fragment) {
+ fragment.startActivityForResult(
+ getPickImageChooserIntent(context), PICK_IMAGE_CHOOSER_REQUEST_CODE);
+ }
+
+ /**
+ * Create a chooser intent to select the source to get image from.
+ * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
+ * All possible sources are added to the intent chooser.
+ * Use "pick_image_intent_chooser_title" string resource to override chooser title.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ */
+ public static Intent getPickImageChooserIntent(@NonNull Context context) {
+ return getPickImageChooserIntent(
+ context, context.getString(R.string.pick_image_intent_chooser_title), false, true);
+ }
+
+ /**
+ * Create a chooser intent to select the source to get image from.
+ * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
+ * All possible sources are added to the intent chooser.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param title the title to use for the chooser UI
+ * @param includeDocuments if to include KitKat documents activity containing all sources
+ * @param includeCamera if to include camera intents
+ */
+ public static Intent getPickImageChooserIntent(
+ @NonNull Context context,
+ CharSequence title,
+ boolean includeDocuments,
+ boolean includeCamera) {
+
+ List allIntents = new ArrayList<>();
+ PackageManager packageManager = context.getPackageManager();
+
+ // collect all camera intents if Camera permission is available
+ if (!isExplicitCameraPermissionRequired(context) && includeCamera) {
+ allIntents.addAll(getCameraIntents(context, packageManager));
+ }
+
+ List galleryIntents =
+ getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, includeDocuments);
+ if (galleryIntents.size() == 0) {
+ // if no intents found for get-content try pick intent action (Huawei P9).
+ galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_PICK, includeDocuments);
+ }
+ allIntents.addAll(galleryIntents);
+
+ Intent target;
+ if (allIntents.isEmpty()) {
+ target = new Intent();
+ } else {
+ target = allIntents.get(allIntents.size() - 1);
+ allIntents.remove(allIntents.size() - 1);
+ }
+
+ // Create a chooser from the main intent
+ Intent chooserIntent = Intent.createChooser(target, title);
+
+ // Add all other intents
+ chooserIntent.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));
+
+ return chooserIntent;
+ }
+
+ /**
+ * Get the main Camera intent for capturing image using device camera app. If the outputFileUri is
+ * null, a default Uri will be created with {@link #getCaptureImageOutputUri(Context)}, so then
+ * you will be able to get the pictureUri using {@link #getPickImageResultUri(Context, Intent)}.
+ * Otherwise, it is just you use the Uri passed to this method.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param outputFileUri the Uri where the picture will be placed.
+ */
+ public static Intent getCameraIntent(@NonNull Context context, Uri outputFileUri) {
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ if (outputFileUri == null) {
+ outputFileUri = getCaptureImageOutputUri(context);
+ }
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
+ return intent;
+ }
+
+ /** Get all Camera intents for capturing image using device camera apps. */
+ public static List getCameraIntents(
+ @NonNull Context context, @NonNull PackageManager packageManager) {
+
+ List allIntents = new ArrayList<>();
+
+ // Determine Uri of camera image to save.
+ Uri outputFileUri = getCaptureImageOutputUri(context);
+
+ Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ List listCam = packageManager.queryIntentActivities(captureIntent, 0);
+ for (ResolveInfo res : listCam) {
+ Intent intent = new Intent(captureIntent);
+ intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
+ intent.setPackage(res.activityInfo.packageName);
+ if (outputFileUri != null) {
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
+ }
+ allIntents.add(intent);
+ }
+
+ return allIntents;
+ }
+
+ /**
+ * Get all Gallery intents for getting image from one of the apps of the device that handle
+ * images.
+ */
+ public static List getGalleryIntents(
+ @NonNull PackageManager packageManager, String action, boolean includeDocuments) {
+ List intents = new ArrayList<>();
+ Intent galleryIntent =
+ action == Intent.ACTION_GET_CONTENT
+ ? new Intent(action)
+ : new Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ galleryIntent.setType("image/*");
+ List listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
+ for (ResolveInfo res : listGallery) {
+ Intent intent = new Intent(galleryIntent);
+ intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
+ intent.setPackage(res.activityInfo.packageName);
+ intents.add(intent);
+ }
+
+ // remove documents intent
+ if (!includeDocuments) {
+ for (Intent intent : intents) {
+ if (intent
+ .getComponent()
+ .getClassName()
+ .equals("com.android.documentsui.DocumentsActivity")) {
+ intents.remove(intent);
+ break;
+ }
+ }
+ }
+ return intents;
+ }
+
+ /**
+ * Check if explicetly requesting camera permission is required.
+ * It is required in Android Marshmellow and above if "CAMERA" permission is requested in the
+ * manifest.
+ * See StackOverflow
+ * question.
+ */
+ public static boolean isExplicitCameraPermissionRequired(@NonNull Context context) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && hasPermissionInManifest(context, "android.permission.CAMERA")
+ && context.checkSelfPermission(Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Check if the app requests a specific permission in the manifest.
+ *
+ * @param permissionName the permission to check
+ * @return true - the permission in requested in manifest, false - not.
+ */
+ public static boolean hasPermissionInManifest(
+ @NonNull Context context, @NonNull String permissionName) {
+ String packageName = context.getPackageName();
+ try {
+ PackageInfo packageInfo =
+ context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+ final String[] declaredPermisisons = packageInfo.requestedPermissions;
+ if (declaredPermisisons != null && declaredPermisisons.length > 0) {
+ for (String p : declaredPermisisons) {
+ if (p.equalsIgnoreCase(permissionName)) {
+ return true;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Get URI to image received from capture by camera.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ */
+ public static Uri getCaptureImageOutputUri(@NonNull Context context) {
+ Uri outputFileUri = null;
+ File getImage = context.getExternalCacheDir();
+ if (getImage != null) {
+ outputFileUri = Uri.fromFile(new File(getImage.getPath(), "pickImageResult.jpeg"));
+ }
+ return outputFileUri;
+ }
+
+ /**
+ * Get the URI of the selected image from {@link #getPickImageChooserIntent(Context)}.
+ * Will return the correct URI for camera and gallery image.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param data the returned data of the activity result
+ */
+ public static Uri getPickImageResultUri(@NonNull Context context, @Nullable Intent data) {
+ boolean isCamera = true;
+ if (data != null && data.getData() != null) {
+ String action = data.getAction();
+ isCamera = action != null && action.equals(MediaStore.ACTION_IMAGE_CAPTURE);
+ }
+ return isCamera || data.getData() == null ? getCaptureImageOutputUri(context) : data.getData();
+ }
+
+ /**
+ * Check if the given picked image URI requires READ_EXTERNAL_STORAGE permissions.
+ * Only relevant for API version 23 and above and not required for all URI's depends on the
+ * implementation of the app that was used for picking the image. So we just test if we can open
+ * the stream or do we get an exception when we try, Android is awesome.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param uri the result URI of image pick.
+ * @return true - required permission are not granted, false - either no need for permissions or
+ * they are granted
+ */
+ public static boolean isReadExternalStoragePermissionsRequired(
+ @NonNull Context context, @NonNull Uri uri) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED
+ && isUriRequiresPermissions(context, uri);
+ }
+
+ /**
+ * Test if we can open the given Android URI to test if permission required error is thrown.
+ * Only relevant for API version 23 and above.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param uri the result URI of image pick.
+ */
+ public static boolean isUriRequiresPermissions(@NonNull Context context, @NonNull Uri uri) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ InputStream stream = resolver.openInputStream(uri);
+ if (stream != null) {
+ stream.close();
+ }
+ return false;
+ } catch (Exception e) {
+ return true;
+ }
+ }
+
+ /**
+ * Create {@link ActivityBuilder} instance to open image picker for cropping and then start {@link
+ * CropImageActivity} to crop the selected image.
+ * Result will be received in {@link Activity#onActivityResult(int, int, Intent)} and can be
+ * retrieved using {@link #getActivityResult(Intent)}.
+ *
+ * @return builder for Crop Image Activity
+ */
+ public static ActivityBuilder activity() {
+ return new ActivityBuilder(null);
+ }
+
+ /**
+ * Create {@link ActivityBuilder} instance to start {@link CropImageActivity} to crop the given
+ * image.
+ * Result will be received in {@link Activity#onActivityResult(int, int, Intent)} and can be
+ * retrieved using {@link #getActivityResult(Intent)}.
+ *
+ * @param uri the image Android uri source to crop or null to start a picker
+ * @return builder for Crop Image Activity
+ */
+ public static ActivityBuilder activity(@Nullable Uri uri) {
+ return new ActivityBuilder(uri);
+ }
+
+ /**
+ * Get {@link CropImageActivity} result data object for crop image activity started using {@link
+ * #activity(Uri)}.
+ *
+ * @param data result data intent as received in {@link Activity#onActivityResult(int, int,
+ * Intent)}.
+ * @return Crop Image Activity Result object or null if none exists
+ */
+ public static ActivityResult getActivityResult(@Nullable Intent data) {
+ return data != null ? (ActivityResult) data.getParcelableExtra(CROP_IMAGE_EXTRA_RESULT) : null;
+ }
+
+ // region: Inner class: ActivityBuilder
+
+ /** Builder used for creating Image Crop Activity by user request. */
+ public static final class ActivityBuilder {
+
+ /** The image to crop source Android uri. */
+ @Nullable private final Uri mSource;
+
+ /** Options for image crop UX */
+ private final CropImageOptions mOptions;
+
+ private ActivityBuilder(@Nullable Uri source) {
+ mSource = source;
+ mOptions = new CropImageOptions();
+ }
+
+ /** Get {@link CropImageActivity} intent to start the activity. */
+ public Intent getIntent(@NonNull Context context) {
+ return getIntent(context, CropImageActivity.class);
+ }
+
+ /** Get {@link CropImageActivity} intent to start the activity. */
+ public Intent getIntent(@NonNull Context context, @Nullable Class> cls) {
+ mOptions.validate();
+
+ Intent intent = new Intent();
+ intent.setClass(context, cls);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(CROP_IMAGE_EXTRA_SOURCE, mSource);
+ bundle.putParcelable(CROP_IMAGE_EXTRA_OPTIONS, mOptions);
+ intent.putExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE, bundle);
+ return intent;
+ }
/**
- * The key used to pass crop image source URI to {@link CropImageActivity}.
+ * Start {@link CropImageActivity}.
+ *
+ * @param activity activity to receive result
*/
- public static final String CROP_IMAGE_EXTRA_SOURCE = "CROP_IMAGE_EXTRA_SOURCE";
+ public void start(@NonNull Activity activity) {
+ mOptions.validate();
+ activity.startActivityForResult(getIntent(activity), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The key used to pass crop image options to {@link CropImageActivity}.
+ * Start {@link CropImageActivity}.
+ *
+ * @param activity activity to receive result
*/
- public static final String CROP_IMAGE_EXTRA_OPTIONS = "CROP_IMAGE_EXTRA_OPTIONS";
+ public void start(@NonNull Activity activity, @Nullable Class> cls) {
+ mOptions.validate();
+ activity.startActivityForResult(getIntent(activity, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The key used to pass crop image result data back from {@link CropImageActivity}.
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
*/
- public static final String CROP_IMAGE_EXTRA_RESULT = "CROP_IMAGE_EXTRA_RESULT";
+ public void start(@NonNull Context context, @NonNull Fragment fragment) {
+ fragment.startActivityForResult(getIntent(context), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The request code used to start pick image activity to be used on result to identify the this specific request.
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
*/
- public static final int PICK_IMAGE_CHOOSER_REQUEST_CODE = 200;
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ public void start(@NonNull Context context, @NonNull android.app.Fragment fragment) {
+ fragment.startActivityForResult(getIntent(context), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The request code used to request permission to pick image from external storage.
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
*/
- public static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 201;
+ public void start(
+ @NonNull Context context, @NonNull Fragment fragment, @Nullable Class> cls) {
+ fragment.startActivityForResult(getIntent(context, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The request code used to request permission to capture image from camera.
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
*/
- public static final int CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE = 2011;
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ public void start(
+ @NonNull Context context, @NonNull android.app.Fragment fragment, @Nullable Class> cls) {
+ fragment.startActivityForResult(getIntent(context, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
/**
- * The request code used to start {@link CropImageActivity} to be used on result to identify the this specific
- * request.
+ * The shape of the cropping window.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ * Default: RECTANGLE
*/
- public static final int CROP_IMAGE_ACTIVITY_REQUEST_CODE = 203;
+ public ActivityBuilder setCropShape(@NonNull CropImageView.CropShape cropShape) {
+ mOptions.cropShape = cropShape;
+ return this;
+ }
/**
- * The result code used to return error from {@link CropImageActivity}.
+ * An edge of the crop window will snap to the corresponding edge of a specified bounding box
+ * when the crop window edge is less than or equal to this distance (in pixels) away from the
+ * bounding box edge (in pixels).
+ * Default: 3dp
*/
- public static final int CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE = 204;
- //endregion
-
- private CropImage() {
+ public ActivityBuilder setSnapRadius(float snapRadius) {
+ mOptions.snapRadius = snapRadius;
+ return this;
}
/**
- * Create a new bitmap that has all pixels beyond the oval shape transparent.
- * Old bitmap is recycled.
+ * The radius of the touchable area around the handle (in pixels).
+ * We are basing this value off of the recommended 48dp Rhythm.
+ * See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
+ * Default: 48dp
*/
- public static Bitmap toOvalBitmap(@NonNull Bitmap bitmap) {
- int width = bitmap.getWidth();
- int height = bitmap.getHeight();
- Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-
- Canvas canvas = new Canvas(output);
-
- int color = 0xff424242;
- Paint paint = new Paint();
-
- paint.setAntiAlias(true);
- canvas.drawARGB(0, 0, 0, 0);
- paint.setColor(color);
-
- RectF rect = new RectF(0, 0, width, height);
- canvas.drawOval(rect, paint);
- paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
- canvas.drawBitmap(bitmap, 0, 0, paint);
-
- bitmap.recycle();
-
- return output;
+ public ActivityBuilder setTouchRadius(float touchRadius) {
+ mOptions.touchRadius = touchRadius;
+ return this;
}
/**
- * Start an activity to get image for cropping using chooser intent that will have all the available
- * applications for the device like camera (MyCamera), galery (Photos), store apps (Dropbox), etc.
- * Use "pick_image_intent_chooser_title" string resource to override pick chooser title.
- *
- * @param activity the activity to be used to start activity from
+ * whether the guidelines should be on, off, or only showing when resizing.
+ * Default: ON_TOUCH
*/
- public static void startPickImageActivity(@NonNull Activity activity) {
- activity.startActivityForResult(getPickImageChooserIntent(activity), PICK_IMAGE_CHOOSER_REQUEST_CODE);
+ public ActivityBuilder setGuidelines(@NonNull CropImageView.Guidelines guidelines) {
+ mOptions.guidelines = guidelines;
+ return this;
}
/**
- * Create a chooser intent to select the source to get image from.
- * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
- * All possible sources are added to the intent chooser.
- * Use "pick_image_intent_chooser_title" string resource to override chooser title.
- *
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
+ * The initial scale type of the image in the crop image view
+ * Default: FIT_CENTER
*/
- public static Intent getPickImageChooserIntent(@NonNull Context context) {
- return getPickImageChooserIntent(context, context.getString(R.string.pick_image_intent_chooser_title), false);
+ public ActivityBuilder setScaleType(@NonNull CropImageView.ScaleType scaleType) {
+ mOptions.scaleType = scaleType;
+ return this;
}
/**
- * Create a chooser intent to select the source to get image from.
- * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
- * All possible sources are added to the intent chooser.
- *
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
- * @param title the title to use for the chooser UI
- * @param includeDocuments if to include KitKat documents activity containing all sources
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
*/
- public static Intent getPickImageChooserIntent(@NonNull Context context, CharSequence title, boolean includeDocuments) {
-
- List allIntents = new ArrayList<>();
- PackageManager packageManager = context.getPackageManager();
-
- // collect all camera intents
- allIntents.addAll(getCameraIntents(context, packageManager));
-
- List galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, includeDocuments);
- if (galleryIntents.size() == 0) {
- // if no intents found for get-content try pick intent action (Huawei P9).
- galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_PICK, includeDocuments);
- }
- allIntents.addAll(galleryIntents);
-
- Intent target;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- target = new Intent();
- } else {
- target = allIntents.get(allIntents.size() - 1);
- allIntents.remove(allIntents.size() - 1);
- }
-
- // Create a chooser from the main intent
- Intent chooserIntent = Intent.createChooser(target, title);
-
- // Add all other intents
- chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));
-
- return chooserIntent;
+ public ActivityBuilder setShowCropOverlay(boolean showCropOverlay) {
+ mOptions.showCropOverlay = showCropOverlay;
+ return this;
}
/**
- * Get all Camera intents for capturing image using device camera apps.
+ * if auto-zoom functionality is enabled.
+ * default: true.
*/
- public static List getCameraIntents(@NonNull Context context, @NonNull PackageManager packageManager) {
-
- List allIntents = new ArrayList<>();
-
- // Determine Uri of camera image to save.
- Uri outputFileUri = getCaptureImageOutputUri(context);
-
- Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
- List listCam = packageManager.queryIntentActivities(captureIntent, 0);
- for (ResolveInfo res : listCam) {
- Intent intent = new Intent(captureIntent);
- intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
- intent.setPackage(res.activityInfo.packageName);
- if (outputFileUri != null) {
- intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
- }
- allIntents.add(intent);
- }
-
- return allIntents;
+ public ActivityBuilder setAutoZoomEnabled(boolean autoZoomEnabled) {
+ mOptions.autoZoomEnabled = autoZoomEnabled;
+ return this;
}
/**
- * Get all Gallery intents for getting image from one of the apps of the device that handle images.
+ * if multi touch functionality is enabled.
+ * default: true.
*/
- public static List getGalleryIntents(@NonNull PackageManager packageManager, String action, boolean includeDocuments) {
- List intents = new ArrayList<>();
- Intent galleryIntent = action == Intent.ACTION_GET_CONTENT ? new Intent(action)
- : new Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
- galleryIntent.setType("image/*");
- List listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
- for (ResolveInfo res : listGallery) {
- Intent intent = new Intent(galleryIntent);
- intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
- intent.setPackage(res.activityInfo.packageName);
- intents.add(intent);
- }
-
- // remove documents intent
- if (!includeDocuments) {
- for (Intent intent : intents) {
- if (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity")) {
- intents.remove(intent);
- break;
- }
- }
- }
- return intents;
+ public ActivityBuilder setMultiTouchEnabled(boolean multiTouchEnabled) {
+ mOptions.multiTouchEnabled = multiTouchEnabled;
+ return this;
}
/**
- * Check if explicetly requesting camera permission is required.
- * It is required in Android Marshmellow and above if "CAMERA" permission is requested in the manifest.
- * See StackOverflow
- * question.
+ * The max zoom allowed during cropping.
+ * Default: 4
*/
- public static boolean isExplicitCameraPermissionRequired(@NonNull Context context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return hasPermissionInManifest(context, "android.permission.CAMERA") &&
- context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED;
- }
- return false;
+ public ActivityBuilder setMaxZoom(int maxZoom) {
+ mOptions.maxZoom = maxZoom;
+ return this;
}
/**
- * Check if the app requests a specific permission in the manifest.
- *
- * @param permissionName the permission to check
- * @return true - the permission in requested in manifest, false - not.
- */
- public static boolean hasPermissionInManifest(@NonNull Context context, @NonNull String permissionName) {
- String packageName = context.getPackageName();
- try {
- PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
- final String[] declaredPermisisons = packageInfo.requestedPermissions;
- if (declaredPermisisons != null && declaredPermisisons.length > 0) {
- for (String p : declaredPermisisons) {
- if (p.equalsIgnoreCase(permissionName)) {
- return true;
- }
- }
- }
- } catch (PackageManager.NameNotFoundException e) {
- }
- return false;
+ * The initial crop window padding from image borders in percentage of the cropping image
+ * dimensions.
+ * Default: 0.1
+ */
+ public ActivityBuilder setInitialCropWindowPaddingRatio(float initialCropWindowPaddingRatio) {
+ mOptions.initialCropWindowPaddingRatio = initialCropWindowPaddingRatio;
+ return this;
}
/**
- * Get URI to image received from capture by camera.
- *
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
+ * whether the width to height aspect ratio should be maintained or free to change.
+ * Default: false
*/
- public static Uri getCaptureImageOutputUri(@NonNull Context context) {
- Uri outputFileUri = null;
- File getImage = context.getExternalCacheDir();
- if (getImage != null) {
- outputFileUri = Uri.fromFile(new File(getImage.getPath(), "pickImageResult.jpeg"));
- }
- return outputFileUri;
+ public ActivityBuilder setFixAspectRatio(boolean fixAspectRatio) {
+ mOptions.fixAspectRatio = fixAspectRatio;
+ return this;
}
/**
- * Get the URI of the selected image from {@link #getPickImageChooserIntent(Context)}.
- * Will return the correct URI for camera and gallery image.
+ * the X,Y value of the aspect ratio.
+ * Also sets fixes aspect ratio to TRUE.
+ * Default: 1/1
*
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
- * @param data the returned data of the activity result
- */
- public static Uri getPickImageResultUri(@NonNull Context context, @Nullable Intent data) {
- boolean isCamera = true;
- if (data != null && data.getData() != null) {
- String action = data.getAction();
- isCamera = action != null && action.equals(MediaStore.ACTION_IMAGE_CAPTURE);
- }
- return isCamera || data.getData() == null ? getCaptureImageOutputUri(context) : data.getData();
+ * @param aspectRatioX the width
+ * @param aspectRatioY the height
+ */
+ public ActivityBuilder setAspectRatio(int aspectRatioX, int aspectRatioY) {
+ mOptions.aspectRatioX = aspectRatioX;
+ mOptions.aspectRatioY = aspectRatioY;
+ mOptions.fixAspectRatio = true;
+ return this;
}
/**
- * Check if the given picked image URI requires READ_EXTERNAL_STORAGE permissions.
- * Only relevant for API version 23 and above and not required for all URI's depends on the
- * implementation of the app that was used for picking the image. So we just test if we can open the stream or
- * do we get an exception when we try, Android is awesome.
- *
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
- * @param uri the result URI of image pick.
- * @return true - required permission are not granted, false - either no need for permissions or they are granted
+ * the thickness of the guidelines lines (in pixels).
+ * Default: 3dp
*/
- public static boolean isReadExternalStoragePermissionsRequired(@NonNull Context context, @NonNull Uri uri) {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
- context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED &&
- isUriRequiresPermissions(context, uri);
+ public ActivityBuilder setBorderLineThickness(float borderLineThickness) {
+ mOptions.borderLineThickness = borderLineThickness;
+ return this;
}
/**
- * Test if we can open the given Android URI to test if permission required error is thrown.
- * Only relevant for API version 23 and above.
- *
- * @param context used to access Android APIs, like content resolve, it is your activity/fragment/widget.
- * @param uri the result URI of image pick.
- */
- public static boolean isUriRequiresPermissions(@NonNull Context context, @NonNull Uri uri) {
- try {
- ContentResolver resolver = context.getContentResolver();
- InputStream stream = resolver.openInputStream(uri);
- stream.close();
- return false;
- } catch (Exception e) {
- return true;
- }
+ * the color of the guidelines lines.
+ * Default: Color.argb(170, 255, 255, 255)
+ */
+ public ActivityBuilder setBorderLineColor(int borderLineColor) {
+ mOptions.borderLineColor = borderLineColor;
+ return this;
}
/**
- * Create {@link ActivityBuilder} instance to start {@link CropImageActivity} to crop the given image.
- * Result will be recieved in {@link Activity#onActivityResult(int, int, Intent)} and can be retrieved
- * using {@link #getActivityResult(Intent)}.
- *
- * @param uri the image Android uri source to crop
- * @return builder for Crop Image Activity
+ * thickness of the corner line (in pixels).
+ * Default: 2dp
*/
- public static ActivityBuilder activity(@NonNull Uri uri) {
- if (uri == null || uri.equals(Uri.EMPTY)) {
- throw new IllegalArgumentException("Uri must be non null or empty");
- }
- return new ActivityBuilder(uri);
+ public ActivityBuilder setBorderCornerThickness(float borderCornerThickness) {
+ mOptions.borderCornerThickness = borderCornerThickness;
+ return this;
}
/**
- * Get {@link CropImageActivity} result data object for crop image activity started using {@link #activity(Uri)}.
- *
- * @param data result data intent as received in {@link Activity#onActivityResult(int, int, Intent)}.
- * @return Crop Image Activity Result object or null if none exists
+ * the offset of corner line from crop window border (in pixels).
+ * Default: 5dp
*/
- public static ActivityResult getActivityResult(@Nullable Intent data) {
- return data != null ? (ActivityResult) data.getParcelableExtra(CROP_IMAGE_EXTRA_RESULT) : null;
+ public ActivityBuilder setBorderCornerOffset(float borderCornerOffset) {
+ mOptions.borderCornerOffset = borderCornerOffset;
+ return this;
}
- //region: Inner class: ActivityBuilder
-
/**
- * Builder used for creating Image Crop Activity by user request.
+ * the length of the corner line away from the corner (in pixels).
+ * Default: 14dp
*/
- public static final class ActivityBuilder {
-
- /**
- * The image to crop source Android uri.
- */
- private final Uri mSource;
-
- /**
- * Options for image crop UX
- */
- private final CropImageOptions mOptions;
-
- private ActivityBuilder(@NonNull Uri source) {
- mSource = source;
- mOptions = new CropImageOptions();
- }
-
- /**
- * Get {@link CropImageActivity} intent to start the activity.
- */
- public Intent getIntent(@NonNull Context context) {
- return getIntent(context, CropImageActivity.class);
- }
-
- /**
- * Get {@link CropImageActivity} intent to start the activity.
- */
- public Intent getIntent(@NonNull Context context, @Nullable Class> cls) {
- mOptions.validate();
-
- Intent intent = new Intent();
- intent.setClass(context, cls);
- intent.putExtra(CROP_IMAGE_EXTRA_SOURCE, mSource);
- intent.putExtra(CROP_IMAGE_EXTRA_OPTIONS, mOptions);
- return intent;
- }
-
- /**
- * Start {@link CropImageActivity}.
- *
- * @param activity activity to receive result
- */
- public void start(@NonNull Activity activity) {
- mOptions.validate();
- activity.startActivityForResult(getIntent(activity), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
- }
-
- /**
- * Start {@link CropImageActivity}.
- *
- * @param activity activity to receive result
- */
- public void start(@NonNull Activity activity, @Nullable Class> cls) {
- mOptions.validate();
- activity.startActivityForResult(getIntent(activity, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
- }
-
- /**
- * Start {@link CropImageActivity}.
- *
- * @param fragment fragment to receive result
- */
- public void start(@NonNull Context context, @NonNull Fragment fragment) {
- fragment.startActivityForResult(getIntent(context), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
- }
-
- /**
- * Start {@link CropImageActivity}.
- *
- * @param fragment fragment to receive result
- */
- public void start(@NonNull Context context, @NonNull Fragment fragment, @Nullable Class> cls) {
- fragment.startActivityForResult(getIntent(context, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
- }
-
- /**
- * The shape of the cropping window.
- * To set square/circle crop shape set aspect ratio to 1:1.
- * Default: RECTANGLE
- */
- public ActivityBuilder setCropShape(@NonNull CropImageView.CropShape cropShape) {
- mOptions.cropShape = cropShape;
- return this;
- }
-
- /**
- * An edge of the crop window will snap to the corresponding edge of a specified bounding box
- * when the crop window edge is less than or equal to this distance (in pixels) away from the bounding box
- * edge (in pixels).
- * Default: 3dp
- */
- public ActivityBuilder setSnapRadius(float snapRadius) {
- mOptions.snapRadius = snapRadius;
- return this;
- }
-
- /**
- * The radius of the touchable area around the handle (in pixels).
- * We are basing this value off of the recommended 48dp Rhythm.
- * See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
- * Default: 48dp
- */
- public ActivityBuilder setTouchRadius(float touchRadius) {
- mOptions.touchRadius = touchRadius;
- return this;
- }
-
- /**
- * whether the guidelines should be on, off, or only showing when resizing.
- * Default: ON_TOUCH
- */
- public ActivityBuilder setGuidelines(@NonNull CropImageView.Guidelines guidelines) {
- mOptions.guidelines = guidelines;
- return this;
- }
-
- /**
- * The initial scale type of the image in the crop image view
- * Default: FIT_CENTER
- */
- public ActivityBuilder setScaleType(@NonNull CropImageView.ScaleType scaleType) {
- mOptions.scaleType = scaleType;
- return this;
- }
-
- /**
- * if to show crop overlay UI what contains the crop window UI surrounded by background over the cropping
- * image.
- * default: true, may disable for animation or frame transition.
- */
- public ActivityBuilder setShowCropOverlay(boolean showCropOverlay) {
- mOptions.showCropOverlay = showCropOverlay;
- return this;
- }
-
- /**
- * if auto-zoom functionality is enabled.
- * default: true.
- */
- public ActivityBuilder setAutoZoomEnabled(boolean autoZoomEnabled) {
- mOptions.autoZoomEnabled = autoZoomEnabled;
- return this;
- }
-
- /**
- * if multi touch functionality is enabled.
- * default: true.
- */
- public ActivityBuilder setMultiTouchEnabled(boolean multiTouchEnabled) {
- mOptions.multiTouchEnabled = multiTouchEnabled;
- return this;
- }
-
- /**
- * The max zoom allowed during cropping.
- * Default: 4
- */
- public ActivityBuilder setMaxZoom(int maxZoom) {
- mOptions.maxZoom = maxZoom;
- return this;
- }
-
- /**
- * The initial crop window padding from image borders in percentage of the cropping image dimensions.
- * Default: 0.1
- */
- public ActivityBuilder setInitialCropWindowPaddingRatio(float initialCropWindowPaddingRatio) {
- mOptions.initialCropWindowPaddingRatio = initialCropWindowPaddingRatio;
- return this;
- }
-
- /**
- * whether the width to height aspect ratio should be maintained or free to change.
- * Default: false
- */
- public ActivityBuilder setFixAspectRatio(boolean fixAspectRatio) {
- mOptions.fixAspectRatio = fixAspectRatio;
- return this;
- }
-
- /**
- * the X,Y value of the aspect ratio.
- * Also sets fixes aspect ratio to TRUE.
- * Default: 1/1
- *
- * @param aspectRatioX the width
- * @param aspectRatioY the height
- */
- public ActivityBuilder setAspectRatio(int aspectRatioX, int aspectRatioY) {
- mOptions.aspectRatioX = aspectRatioX;
- mOptions.aspectRatioY = aspectRatioY;
- mOptions.fixAspectRatio = true;
- return this;
- }
-
- /**
- * the thickness of the guidelines lines (in pixels).
- * Default: 3dp
- */
- public ActivityBuilder setBorderLineThickness(float borderLineThickness) {
- mOptions.borderLineThickness = borderLineThickness;
- return this;
- }
-
- /**
- * the color of the guidelines lines.
- * Default: Color.argb(170, 255, 255, 255)
- */
- public ActivityBuilder setBorderLineColor(int borderLineColor) {
- mOptions.borderLineColor = borderLineColor;
- return this;
- }
+ public ActivityBuilder setBorderCornerLength(float borderCornerLength) {
+ mOptions.borderCornerLength = borderCornerLength;
+ return this;
+ }
- /**
- * thickness of the corner line (in pixels).
- * Default: 2dp
- */
- public ActivityBuilder setBorderCornerThickness(float borderCornerThickness) {
- mOptions.borderCornerThickness = borderCornerThickness;
- return this;
- }
+ /**
+ * the color of the corner line.
+ * Default: WHITE
+ */
+ public ActivityBuilder setBorderCornerColor(int borderCornerColor) {
+ mOptions.borderCornerColor = borderCornerColor;
+ return this;
+ }
- /**
- * the offset of corner line from crop window border (in pixels).
- * Default: 5dp
- */
- public ActivityBuilder setBorderCornerOffset(float borderCornerOffset) {
- mOptions.borderCornerOffset = borderCornerOffset;
- return this;
- }
+ /**
+ * the thickness of the guidelines lines (in pixels).
+ * Default: 1dp
+ */
+ public ActivityBuilder setGuidelinesThickness(float guidelinesThickness) {
+ mOptions.guidelinesThickness = guidelinesThickness;
+ return this;
+ }
- /**
- * the length of the corner line away from the corner (in pixels).
- * Default: 14dp
- */
- public ActivityBuilder setBorderCornerLength(float borderCornerLength) {
- mOptions.borderCornerLength = borderCornerLength;
- return this;
- }
+ /**
+ * the color of the guidelines lines.
+ * Default: Color.argb(170, 255, 255, 255)
+ */
+ public ActivityBuilder setGuidelinesColor(int guidelinesColor) {
+ mOptions.guidelinesColor = guidelinesColor;
+ return this;
+ }
- /**
- * the color of the corner line.
- * Default: WHITE
- */
- public ActivityBuilder setBorderCornerColor(int borderCornerColor) {
- mOptions.borderCornerColor = borderCornerColor;
- return this;
- }
+ /**
+ * the color of the overlay background around the crop window cover the image parts not in the
+ * crop window.
+ * Default: Color.argb(119, 0, 0, 0)
+ */
+ public ActivityBuilder setBackgroundColor(int backgroundColor) {
+ mOptions.backgroundColor = backgroundColor;
+ return this;
+ }
- /**
- * the thickness of the guidelines lines (in pixels).
- * Default: 1dp
- */
- public ActivityBuilder setGuidelinesThickness(float guidelinesThickness) {
- mOptions.guidelinesThickness = guidelinesThickness;
- return this;
- }
+ /**
+ * the min size the crop window is allowed to be (in pixels).
+ * Default: 42dp, 42dp
+ */
+ public ActivityBuilder setMinCropWindowSize(int minCropWindowWidth, int minCropWindowHeight) {
+ mOptions.minCropWindowWidth = minCropWindowWidth;
+ mOptions.minCropWindowHeight = minCropWindowHeight;
+ return this;
+ }
- /**
- * the color of the guidelines lines.
- * Default: Color.argb(170, 255, 255, 255)
- */
- public ActivityBuilder setGuidelinesColor(int guidelinesColor) {
- mOptions.guidelinesColor = guidelinesColor;
- return this;
- }
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window
+ * limits (in pixels).
+ * Default: 40px, 40px
+ */
+ public ActivityBuilder setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mOptions.minCropResultWidth = minCropResultWidth;
+ mOptions.minCropResultHeight = minCropResultHeight;
+ return this;
+ }
- /**
- * the color of the overlay background around the crop window cover the image parts not in the crop window.
- * Default: Color.argb(119, 0, 0, 0)
- */
- public ActivityBuilder setBackgroundColor(int backgroundColor) {
- mOptions.backgroundColor = backgroundColor;
- return this;
- }
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window
+ * limits (in pixels).
+ * Default: 99999, 99999
+ */
+ public ActivityBuilder setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mOptions.maxCropResultWidth = maxCropResultWidth;
+ mOptions.maxCropResultHeight = maxCropResultHeight;
+ return this;
+ }
- /**
- * the min size the crop window is allowed to be (in pixels).
- * Default: 42dp, 42dp
- */
- public ActivityBuilder setMinCropWindowSize(int minCropWindowWidth, int minCropWindowHeight) {
- mOptions.minCropWindowWidth = minCropWindowWidth;
- mOptions.minCropWindowHeight = minCropWindowHeight;
- return this;
- }
+ /**
+ * the title of the {@link CropImageActivity}.
+ * Default: ""
+ */
+ public ActivityBuilder setActivityTitle(CharSequence activityTitle) {
+ mOptions.activityTitle = activityTitle;
+ return this;
+ }
- /**
- * the min size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- * Default: 40px, 40px
- */
- public ActivityBuilder setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
- mOptions.minCropResultWidth = minCropResultWidth;
- mOptions.minCropResultHeight = minCropResultHeight;
- return this;
- }
+ /**
+ * the color to use for action bar items icons.
+ * Default: NONE
+ */
+ public ActivityBuilder setActivityMenuIconColor(int activityMenuIconColor) {
+ mOptions.activityMenuIconColor = activityMenuIconColor;
+ return this;
+ }
- /**
- * the max size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- * Default: 99999, 99999
- */
- public ActivityBuilder setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
- mOptions.maxCropResultWidth = maxCropResultWidth;
- mOptions.maxCropResultHeight = maxCropResultHeight;
- return this;
- }
+ /**
+ * the Android Uri to save the cropped image to.
+ * Default: NONE, will create a temp file
+ */
+ public ActivityBuilder setOutputUri(Uri outputUri) {
+ mOptions.outputUri = outputUri;
+ return this;
+ }
- /**
- * the title of the {@link CropImageActivity}.
- * Default: ""
- */
- public ActivityBuilder setActivityTitle(String activityTitle) {
- mOptions.activityTitle = activityTitle;
- return this;
- }
+ /**
+ * the compression format to use when writting the image.
+ * Default: JPEG
+ */
+ public ActivityBuilder setOutputCompressFormat(Bitmap.CompressFormat outputCompressFormat) {
+ mOptions.outputCompressFormat = outputCompressFormat;
+ return this;
+ }
- /**
- * the color to use for action bar items icons.
- * Default: NONE
- */
- public ActivityBuilder setActivityMenuIconColor(int activityMenuIconColor) {
- mOptions.activityMenuIconColor = activityMenuIconColor;
- return this;
- }
+ /**
+ * the quility (if applicable) to use when writting the image (0 - 100).
+ * Default: 90
+ */
+ public ActivityBuilder setOutputCompressQuality(int outputCompressQuality) {
+ mOptions.outputCompressQuality = outputCompressQuality;
+ return this;
+ }
- /**
- * the Android Uri to save the cropped image to.
- * Default: NONE, will create a temp file
- */
- public ActivityBuilder setOutputUri(Uri outputUri) {
- mOptions.outputUri = outputUri;
- return this;
- }
+ /**
+ * the size to resize the cropped image to.
+ * Uses {@link CropImageView.RequestSizeOptions#RESIZE_INSIDE} option.
+ * Default: 0, 0 - not set, will not resize
+ */
+ public ActivityBuilder setRequestedSize(int reqWidth, int reqHeight) {
+ return setRequestedSize(reqWidth, reqHeight, CropImageView.RequestSizeOptions.RESIZE_INSIDE);
+ }
- /**
- * the compression format to use when writting the image.
- * Default: JPEG
- */
- public ActivityBuilder setOutputCompressFormat(Bitmap.CompressFormat outputCompressFormat) {
- mOptions.outputCompressFormat = outputCompressFormat;
- return this;
- }
+ /**
+ * the size to resize the cropped image to.
+ * Default: 0, 0 - not set, will not resize
+ */
+ public ActivityBuilder setRequestedSize(
+ int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
+ mOptions.outputRequestWidth = reqWidth;
+ mOptions.outputRequestHeight = reqHeight;
+ mOptions.outputRequestSizeOptions = options;
+ return this;
+ }
- /**
- * the quility (if applicable) to use when writting the image (0 - 100).
- * Default: 90
- */
- public ActivityBuilder setOutputCompressQuality(int outputCompressQuality) {
- mOptions.outputCompressQuality = outputCompressQuality;
- return this;
- }
+ /**
+ * if the result of crop image activity should not save the cropped image bitmap.
+ * Used if you want to crop the image manually and need only the crop rectangle and rotation
+ * data.
+ * Default: false
+ */
+ public ActivityBuilder setNoOutputImage(boolean noOutputImage) {
+ mOptions.noOutputImage = noOutputImage;
+ return this;
+ }
- /**
- * the size to resize the cropped image to.
- * Uses {@link CropImageView.RequestSizeOptions#RESIZE_INSIDE} option.
- * Default: 0, 0 - not set, will not resize
- */
- public ActivityBuilder setRequestedSize(int reqWidth, int reqHeight) {
- return setRequestedSize(reqWidth, reqHeight, CropImageView.RequestSizeOptions.RESIZE_INSIDE);
- }
+ /**
+ * the initial rectangle to set on the cropping image after loading.
+ * Default: NONE - will initialize using initial crop window padding ratio
+ */
+ public ActivityBuilder setInitialCropWindowRectangle(Rect initialCropWindowRectangle) {
+ mOptions.initialCropWindowRectangle = initialCropWindowRectangle;
+ return this;
+ }
- /**
- * the size to resize the cropped image to.
- * Default: 0, 0 - not set, will not resize
- */
- public ActivityBuilder setRequestedSize(int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
- mOptions.outputRequestWidth = reqWidth;
- mOptions.outputRequestHeight = reqHeight;
- mOptions.outputRequestSizeOptions = options;
- return this;
- }
+ /**
+ * the initial rotation to set on the cropping image after loading (0-360 degrees clockwise).
+ *
+ * Default: NONE - will read image exif data
+ */
+ public ActivityBuilder setInitialRotation(int initialRotation) {
+ mOptions.initialRotation = (initialRotation + 360) % 360;
+ return this;
+ }
- /**
- * if the result of crop image activity should not save the cropped image bitmap.
- * Used if you want to crop the image manually and need only the crop rectangle and rotation data.
- * Default: false
- */
- public ActivityBuilder setNoOutputImage(boolean noOutputImage) {
- mOptions.noOutputImage = noOutputImage;
- return this;
- }
+ /**
+ * if to allow rotation during cropping.
+ * Default: true
+ */
+ public ActivityBuilder setAllowRotation(boolean allowRotation) {
+ mOptions.allowRotation = allowRotation;
+ return this;
+ }
- /**
- * the initial rectangle to set on the cropping image after loading.
- * Default: NONE - will initialize using initial crop window padding ratio
- */
- public ActivityBuilder setInitialCropWindowRectangle(Rect initialCropWindowRectangle) {
- mOptions.initialCropWindowRectangle = initialCropWindowRectangle;
- return this;
- }
+ /**
+ * if to allow flipping during cropping.
+ * Default: true
+ */
+ public ActivityBuilder setAllowFlipping(boolean allowFlipping) {
+ mOptions.allowFlipping = allowFlipping;
+ return this;
+ }
- /**
- * the initial rotation to set on the cropping image after loading (0-360 degrees clockwise).
- * Default: NONE - will read image exif data
- */
- public ActivityBuilder setInitialRotation(int initialRotation) {
- mOptions.initialRotation = initialRotation;
- return this;
- }
+ /**
+ * if to allow counter-clockwise rotation during cropping.
+ * Note: if rotation is disabled this option has no effect.
+ * Default: false
+ */
+ public ActivityBuilder setAllowCounterRotation(boolean allowCounterRotation) {
+ mOptions.allowCounterRotation = allowCounterRotation;
+ return this;
+ }
- /**
- * if to allow rotation during cropping.
- * Default: true
- */
- public ActivityBuilder setAllowRotation(boolean allowRotation) {
- mOptions.allowRotation = allowRotation;
- return this;
- }
+ /**
+ * The amount of degreees to rotate clockwise or counter-clockwise (0-360).
+ * Default: 90
+ */
+ public ActivityBuilder setRotationDegrees(int rotationDegrees) {
+ mOptions.rotationDegrees = (rotationDegrees + 360) % 360;
+ return this;
+ }
- /**
- * if to allow counter-clockwise rotation during cropping.
- * Note: if rotation is disabled this option has no effect.
- * Default: false
- */
- public ActivityBuilder setAllowCounterRotation(boolean allowCounterRotation) {
- mOptions.allowCounterRotation = allowCounterRotation;
- return this;
- }
+ /**
+ * whether the image should be flipped horizontally.
+ * Default: false
+ */
+ public ActivityBuilder setFlipHorizontally(boolean flipHorizontally) {
+ mOptions.flipHorizontally = flipHorizontally;
+ return this;
+ }
- /**
- * The amount of degreees to rotate clockwise or counter-clockwise (0-360).
- * Default: 90
- */
- public ActivityBuilder setRotationDegrees(int rotationDegrees) {
- mOptions.rotationDegrees = rotationDegrees;
- return this;
- }
+ /**
+ * whether the image should be flipped vertically.
+ * Default: false
+ */
+ public ActivityBuilder setFlipVertically(boolean flipVertically) {
+ mOptions.flipVertically = flipVertically;
+ return this;
}
- //endregion
- //region: Inner class: ActivityResult
+ /**
+ * optional, set crop menu crop button title.
+ * Default: null, will use resource string: crop_image_menu_crop
+ */
+ public ActivityBuilder setCropMenuCropButtonTitle(CharSequence title) {
+ mOptions.cropMenuCropButtonTitle = title;
+ return this;
+ }
/**
- * Result data of Crop Image Activity.
+ * Image resource id to use for crop icon instead of text.
+ * Default: 0
*/
- public static final class ActivityResult extends CropImageView.CropResult implements Parcelable {
+ public ActivityBuilder setCropMenuCropButtonIcon(@DrawableRes int drawableResource) {
+ mOptions.cropMenuCropButtonIcon = drawableResource;
+ return this;
+ }
+ }
+ // endregion
+
+ // region: Inner class: ActivityResult
+
+ /** Result data of Crop Image Activity. */
+ public static final class ActivityResult extends CropImageView.CropResult implements Parcelable {
- public static final Creator CREATOR = new Creator() {
- @Override
- public ActivityResult createFromParcel(Parcel in) {
- return new ActivityResult(in);
- }
+ public static final Creator CREATOR =
+ new Creator() {
+ @Override
+ public ActivityResult createFromParcel(Parcel in) {
+ return new ActivityResult(in);
+ }
- @Override
- public ActivityResult[] newArray(int size) {
- return new ActivityResult[size];
- }
+ @Override
+ public ActivityResult[] newArray(int size) {
+ return new ActivityResult[size];
+ }
};
- public ActivityResult(Bitmap bitmap, Uri uri, Exception error, float[] cropPoints, Rect cropRect, int rotation, int sampleSize) {
- super(bitmap, uri, error, cropPoints, cropRect, rotation, sampleSize);
- }
+ public ActivityResult(
+ Uri originalUri,
+ Uri uri,
+ Exception error,
+ float[] cropPoints,
+ Rect cropRect,
+ int rotation,
+ Rect wholeImageRect,
+ int sampleSize) {
+ super(
+ null,
+ originalUri,
+ null,
+ uri,
+ error,
+ cropPoints,
+ cropRect,
+ wholeImageRect,
+ rotation,
+ sampleSize);
+ }
- protected ActivityResult(Parcel in) {
- super(null,
- (Uri) in.readParcelable(Uri.class.getClassLoader()),
- (Exception) in.readSerializable(),
- in.createFloatArray(),
- (Rect) in.readParcelable(Rect.class.getClassLoader()),
- in.readInt(), in.readInt());
- }
+ protected ActivityResult(Parcel in) {
+ super(
+ null,
+ (Uri) in.readParcelable(Uri.class.getClassLoader()),
+ null,
+ (Uri) in.readParcelable(Uri.class.getClassLoader()),
+ (Exception) in.readSerializable(),
+ in.createFloatArray(),
+ (Rect) in.readParcelable(Rect.class.getClassLoader()),
+ (Rect) in.readParcelable(Rect.class.getClassLoader()),
+ in.readInt(),
+ in.readInt());
+ }
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(getUri(), flags);
- dest.writeSerializable(getError());
- dest.writeFloatArray(getCropPoints());
- dest.writeParcelable(getCropRect(), flags);
- dest.writeInt(getRotation());
- dest.writeInt(getSampleSize());
- }
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(getOriginalUri(), flags);
+ dest.writeParcelable(getUri(), flags);
+ dest.writeSerializable(getError());
+ dest.writeFloatArray(getCropPoints());
+ dest.writeParcelable(getCropRect(), flags);
+ dest.writeParcelable(getWholeImageRect(), flags);
+ dest.writeInt(getRotation());
+ dest.writeInt(getSampleSize());
+ }
- @Override
- public int describeContents() {
- return 0;
- }
+ @Override
+ public int describeContents() {
+ return 0;
}
- //endregion
-}
\ No newline at end of file
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java
index bb7f0321..d1afa9f0 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java
@@ -12,17 +12,24 @@
package com.theartofdev.edmodo.cropper;
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
+import android.widget.Toast;
import java.io.File;
import java.io.IOException;
@@ -31,228 +38,313 @@
* Built-in activity for image cropping.
* Use {@link CropImage#activity(Uri)} to create a builder to start this activity.
*/
-public class CropImageActivity extends AppCompatActivity implements CropImageView.OnSetImageUriCompleteListener, CropImageView.OnCropImageCompleteListener {
+public class CropImageActivity extends AppCompatActivity
+ implements CropImageView.OnSetImageUriCompleteListener,
+ CropImageView.OnCropImageCompleteListener {
- /**
- * The crop image view library widget used in the activity
- */
- private CropImageView mCropImageView;
+ /** The crop image view library widget used in the activity */
+ private CropImageView mCropImageView;
- /**
- * the options that were set for the crop image
- */
- private CropImageOptions mOptions;
+ /** Persist URI image to crop URI if specific permissions are required */
+ private Uri mCropImageUri;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.crop_image_activity);
+ /** the options that were set for the crop image */
+ private CropImageOptions mOptions;
- mCropImageView = (CropImageView) findViewById(R.id.cropImageView);
+ @Override
+ @SuppressLint("NewApi")
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.crop_image_activity);
- Intent intent = getIntent();
- Uri source = intent.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_SOURCE);
- mOptions = intent.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
+ mCropImageView = findViewById(R.id.cropImageView);
- if (savedInstanceState == null) {
- mCropImageView.setImageUriAsync(source);
- }
+ Bundle bundle = getIntent().getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
+ mCropImageUri = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE);
+ mOptions = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- String title = mOptions.activityTitle != null && !mOptions.activityTitle.isEmpty()
- ? mOptions.activityTitle
- : getResources().getString(R.string.crop_image_activity_title);
- actionBar.setTitle(title);
- actionBar.setDisplayHomeAsUpEnabled(true);
+ if (savedInstanceState == null) {
+ if (mCropImageUri == null || mCropImageUri.equals(Uri.EMPTY)) {
+ if (CropImage.isExplicitCameraPermissionRequired(this)) {
+ // request permissions and handle the result in onRequestPermissionsResult()
+ requestPermissions(
+ new String[] {Manifest.permission.CAMERA},
+ CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
+ } else {
+ CropImage.startPickImageActivity(this);
}
+ } else if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
+ // request permissions and handle the result in onRequestPermissionsResult()
+ requestPermissions(
+ new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+ CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
+ } else {
+ // no permissions required or already grunted, can start crop image activity
+ mCropImageView.setImageUriAsync(mCropImageUri);
+ }
}
- @Override
- protected void onStart() {
- super.onStart();
- mCropImageView.setOnSetImageUriCompleteListener(this);
- mCropImageView.setOnCropImageCompleteListener(this);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ CharSequence title = mOptions != null &&
+ mOptions.activityTitle != null && mOptions.activityTitle.length() > 0
+ ? mOptions.activityTitle
+ : getResources().getString(R.string.crop_image_activity_title);
+ actionBar.setTitle(title);
+ actionBar.setDisplayHomeAsUpEnabled(true);
}
+ }
- @Override
- protected void onStop() {
- super.onStop();
- mCropImageView.setOnSetImageUriCompleteListener(null);
- mCropImageView.setOnCropImageCompleteListener(null);
- }
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mCropImageView.setOnSetImageUriCompleteListener(this);
+ mCropImageView.setOnCropImageCompleteListener(this);
+ }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.crop_image_menu, menu);
-
- if (!mOptions.allowRotation) {
- menu.removeItem(R.id.crop_image_menu_rotate_left);
- menu.removeItem(R.id.crop_image_menu_rotate_right);
- } else if (mOptions.allowCounterRotation) {
- menu.findItem(R.id.crop_image_menu_rotate_left).setVisible(true);
- }
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mCropImageView.setOnSetImageUriCompleteListener(null);
+ mCropImageView.setOnCropImageCompleteListener(null);
+ }
- Drawable cropIcon = null;
- try {
- cropIcon = ContextCompat.getDrawable(this, R.drawable.crop_image_menu_crop);
- if (cropIcon != null) {
- menu.findItem(R.id.crop_image_menu_crop).setIcon(cropIcon);
- }
- } catch (Exception e) {
- }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.crop_image_menu, menu);
- if (mOptions.activityMenuIconColor != 0) {
- updateMenuItemIconColor(menu, R.id.crop_image_menu_rotate_left, mOptions.activityMenuIconColor);
- updateMenuItemIconColor(menu, R.id.crop_image_menu_rotate_right, mOptions.activityMenuIconColor);
- if (cropIcon != null) {
- updateMenuItemIconColor(menu, R.id.crop_image_menu_crop, mOptions.activityMenuIconColor);
- }
- }
+ if (!mOptions.allowRotation) {
+ menu.removeItem(R.id.crop_image_menu_rotate_left);
+ menu.removeItem(R.id.crop_image_menu_rotate_right);
+ } else if (mOptions.allowCounterRotation) {
+ menu.findItem(R.id.crop_image_menu_rotate_left).setVisible(true);
+ }
- return true;
+ if (!mOptions.allowFlipping) {
+ menu.removeItem(R.id.crop_image_menu_flip);
}
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.crop_image_menu_crop) {
- cropImage();
- return true;
- }
- if (item.getItemId() == R.id.crop_image_menu_rotate_left) {
- rotateImage(-mOptions.rotationDegrees);
- return true;
- }
- if (item.getItemId() == R.id.crop_image_menu_rotate_right) {
- rotateImage(mOptions.rotationDegrees);
- return true;
- }
- if (item.getItemId() == android.R.id.home) {
- setResultCancel();
- return true;
- }
- return super.onOptionsItemSelected(item);
+ if (mOptions.cropMenuCropButtonTitle != null) {
+ menu.findItem(R.id.crop_image_menu_crop).setTitle(mOptions.cropMenuCropButtonTitle);
}
- @Override
- public void onBackPressed() {
- super.onBackPressed();
- setResultCancel();
+ Drawable cropIcon = null;
+ try {
+ if (mOptions.cropMenuCropButtonIcon != 0) {
+ cropIcon = ContextCompat.getDrawable(this, mOptions.cropMenuCropButtonIcon);
+ menu.findItem(R.id.crop_image_menu_crop).setIcon(cropIcon);
+ }
+ } catch (Exception e) {
+ Log.w("AIC", "Failed to read menu crop drawable", e);
}
- @Override
- public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
- if (error == null) {
- if (mOptions.initialCropWindowRectangle != null) {
- mCropImageView.setCropRect(mOptions.initialCropWindowRectangle);
- }
- if (mOptions.initialRotation > -1) {
- mCropImageView.setRotatedDegrees(mOptions.initialRotation);
- }
- } else {
- setResult(null, error, 1);
- }
+ if (mOptions.activityMenuIconColor != 0) {
+ updateMenuItemIconColor(
+ menu, R.id.crop_image_menu_rotate_left, mOptions.activityMenuIconColor);
+ updateMenuItemIconColor(
+ menu, R.id.crop_image_menu_rotate_right, mOptions.activityMenuIconColor);
+ updateMenuItemIconColor(menu, R.id.crop_image_menu_flip, mOptions.activityMenuIconColor);
+ if (cropIcon != null) {
+ updateMenuItemIconColor(menu, R.id.crop_image_menu_crop, mOptions.activityMenuIconColor);
+ }
}
+ return true;
+ }
- @Override
- public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
- setResult(result.getUri(), result.getError(), result.getSampleSize());
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.crop_image_menu_crop) {
+ cropImage();
+ return true;
+ }
+ if (item.getItemId() == R.id.crop_image_menu_rotate_left) {
+ rotateImage(-mOptions.rotationDegrees);
+ return true;
+ }
+ if (item.getItemId() == R.id.crop_image_menu_rotate_right) {
+ rotateImage(mOptions.rotationDegrees);
+ return true;
}
+ if (item.getItemId() == R.id.crop_image_menu_flip_horizontally) {
+ mCropImageView.flipImageHorizontally();
+ return true;
+ }
+ if (item.getItemId() == R.id.crop_image_menu_flip_vertically) {
+ mCropImageView.flipImageVertically();
+ return true;
+ }
+ if (item.getItemId() == android.R.id.home) {
+ setResultCancel();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
- //region: Private methods
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ setResultCancel();
+ }
- /**
- * Execute crop image and save the result tou output uri.
- */
- protected void cropImage() {
- if (mOptions.noOutputImage) {
- setResult(null, null, 1);
+ @Override
+ @SuppressLint("NewApi")
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ // handle result of pick image chooser
+ if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ // User cancelled the picker. We don't have anything to crop
+ setResultCancel();
+ }
+
+ if (resultCode == Activity.RESULT_OK) {
+ mCropImageUri = CropImage.getPickImageResultUri(this, data);
+
+ // For API >= 23 we need to check specifically that we have permissions to read external
+ // storage.
+ if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
+ // request permissions and handle the result in onRequestPermissionsResult()
+ requestPermissions(
+ new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+ CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
} else {
- Uri outputUri = getOutputUri();
- mCropImageView.saveCroppedImageAsync(outputUri,
- mOptions.outputCompressFormat,
- mOptions.outputCompressQuality,
- mOptions.outputRequestWidth,
- mOptions.outputRequestHeight,
- mOptions.outputRequestSizeOptions);
+ // no permissions required or already grunted, can start crop image activity
+ mCropImageView.setImageUriAsync(mCropImageUri);
}
+ }
}
+ }
- /**
- * Rotate the image in the crop image view.
- */
- protected void rotateImage(int degrees) {
- mCropImageView.rotateImage(degrees);
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+ if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
+ if (mCropImageUri != null
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // required permissions granted, start crop image activity
+ mCropImageView.setImageUriAsync(mCropImageUri);
+ } else {
+ Toast.makeText(this, R.string.crop_image_activity_no_permissions, Toast.LENGTH_LONG).show();
+ setResultCancel();
+ }
}
- /**
- * Get Android uri to save the cropped image into.
- * Use the given in options or create a temp file.
- */
- protected Uri getOutputUri() {
- Uri outputUri = mOptions.outputUri;
- if (outputUri.equals(Uri.EMPTY)) {
- try {
- String ext = mOptions.outputCompressFormat == Bitmap.CompressFormat.JPEG ? ".jpg" :
- mOptions.outputCompressFormat == Bitmap.CompressFormat.PNG ? ".png" : ".webp";
- outputUri = Uri.fromFile(File.createTempFile("cropped", ext, getCacheDir()));
- } catch (IOException e) {
- throw new RuntimeException("Failed to create temp file for output image", e);
- }
- }
- return outputUri;
+ if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
+ // Irrespective of whether camera permission was given or not, we show the picker
+ // The picker will not add the camera intent if permission is not available
+ CropImage.startPickImageActivity(this);
}
+ }
- /**
- * Result with cropped image data or error if failed.
- */
- protected void setResult(Uri uri, Exception error, int sampleSize) {
- int resultCode = error == null ? RESULT_OK : CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE;
- setResult(resultCode, getResultIntent(uri, error, sampleSize));
- finish();
+ @Override
+ public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
+ if (error == null) {
+ if (mOptions.initialCropWindowRectangle != null) {
+ mCropImageView.setCropRect(mOptions.initialCropWindowRectangle);
+ }
+ if (mOptions.initialRotation > -1) {
+ mCropImageView.setRotatedDegrees(mOptions.initialRotation);
+ }
+ } else {
+ setResult(null, error, 1);
}
+ }
- /**
- * Cancel of cropping activity.
- */
- protected void setResultCancel() {
- setResult(RESULT_CANCELED);
- finish();
+ @Override
+ public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
+ setResult(result.getUri(), result.getError(), result.getSampleSize());
+ }
+
+ // region: Private methods
+
+ /** Execute crop image and save the result tou output uri. */
+ protected void cropImage() {
+ if (mOptions.noOutputImage) {
+ setResult(null, null, 1);
+ } else {
+ Uri outputUri = getOutputUri();
+ mCropImageView.saveCroppedImageAsync(
+ outputUri,
+ mOptions.outputCompressFormat,
+ mOptions.outputCompressQuality,
+ mOptions.outputRequestWidth,
+ mOptions.outputRequestHeight,
+ mOptions.outputRequestSizeOptions);
}
+ }
+
+ /** Rotate the image in the crop image view. */
+ protected void rotateImage(int degrees) {
+ mCropImageView.rotateImage(degrees);
+ }
- /**
- * Get intent instance to be used for the result of this activity.
- */
- protected Intent getResultIntent(Uri uri, Exception error, int sampleSize) {
- CropImage.ActivityResult result = new CropImage.ActivityResult(null,
- uri,
- error,
- mCropImageView.getCropPoints(),
- mCropImageView.getCropRect(),
- mCropImageView.getRotatedDegrees(),
- sampleSize);
- Intent intent = new Intent();
- intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result);
- return intent;
+ /**
+ * Get Android uri to save the cropped image into.
+ * Use the given in options or create a temp file.
+ */
+ protected Uri getOutputUri() {
+ Uri outputUri = mOptions.outputUri;
+ if (outputUri == null || outputUri.equals(Uri.EMPTY)) {
+ try {
+ String ext =
+ mOptions.outputCompressFormat == Bitmap.CompressFormat.JPEG
+ ? ".jpg"
+ : mOptions.outputCompressFormat == Bitmap.CompressFormat.PNG ? ".png" : ".webp";
+ outputUri = Uri.fromFile(File.createTempFile("cropped", ext, getCacheDir()));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to create temp file for output image", e);
+ }
}
+ return outputUri;
+ }
+
+ /** Result with cropped image data or error if failed. */
+ protected void setResult(Uri uri, Exception error, int sampleSize) {
+ int resultCode = error == null ? RESULT_OK : CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE;
+ setResult(resultCode, getResultIntent(uri, error, sampleSize));
+ finish();
+ }
+
+ /** Cancel of cropping activity. */
+ protected void setResultCancel() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
- /**
- * Update the color of a specific menu item to the given color.
- */
- private void updateMenuItemIconColor(Menu menu, int itemId, int color) {
- MenuItem menuItem = menu.findItem(itemId);
- if (menuItem != null) {
- Drawable menuItemIcon = menuItem.getIcon();
- if (menuItemIcon != null) {
- try {
- menuItemIcon.mutate();
- menuItemIcon.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
- menuItem.setIcon(menuItemIcon);
- } catch (Exception e) {
- }
- }
+ /** Get intent instance to be used for the result of this activity. */
+ protected Intent getResultIntent(Uri uri, Exception error, int sampleSize) {
+ CropImage.ActivityResult result =
+ new CropImage.ActivityResult(
+ mCropImageView.getImageUri(),
+ uri,
+ error,
+ mCropImageView.getCropPoints(),
+ mCropImageView.getCropRect(),
+ mCropImageView.getRotatedDegrees(),
+ mCropImageView.getWholeImageRect(),
+ sampleSize);
+ Intent intent = new Intent();
+ intent.putExtras(getIntent());
+ intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result);
+ return intent;
+ }
+
+ /** Update the color of a specific menu item to the given color. */
+ private void updateMenuItemIconColor(Menu menu, int itemId, int color) {
+ MenuItem menuItem = menu.findItem(itemId);
+ if (menuItem != null) {
+ Drawable menuItemIcon = menuItem.getIcon();
+ if (menuItemIcon != null) {
+ try {
+ menuItemIcon.mutate();
+ menuItemIcon.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ menuItem.setIcon(menuItemIcon);
+ } catch (Exception e) {
+ Log.w("AIC", "Failed to update menu item color", e);
}
+ }
}
- //endregion
+ }
+ // endregion
}
-
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java
index 5382ceca..0acb89a7 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java
@@ -20,96 +20,102 @@
import android.widget.ImageView;
/**
- * Animation to handle smooth cropping image matrix transformation change, specifically for zoom-in/out.
+ * Animation to handle smooth cropping image matrix transformation change, specifically for
+ * zoom-in/out.
*/
final class CropImageAnimation extends Animation implements Animation.AnimationListener {
- //region: Fields and Consts
+ // region: Fields and Consts
- private final ImageView mImageView;
+ private final ImageView mImageView;
- private final CropOverlayView mCropOverlayView;
+ private final CropOverlayView mCropOverlayView;
- private final float[] mStartBoundPoints = new float[8];
+ private final float[] mStartBoundPoints = new float[8];
- private final float[] mEndBoundPoints = new float[8];
+ private final float[] mEndBoundPoints = new float[8];
- private final RectF mStartCropWindowRect = new RectF();
+ private final RectF mStartCropWindowRect = new RectF();
- private final RectF mEndCropWindowRect = new RectF();
+ private final RectF mEndCropWindowRect = new RectF();
- private final float[] mStartImageMatrix = new float[9];
+ private final float[] mStartImageMatrix = new float[9];
- private final float[] mEndImageMatrix = new float[9];
+ private final float[] mEndImageMatrix = new float[9];
- private final RectF mAnimRect = new RectF();
+ private final RectF mAnimRect = new RectF();
- private final float[] mAnimPoints = new float[8];
+ private final float[] mAnimPoints = new float[8];
- private final float[] mAnimMatrix = new float[9];
- //endregion
+ private final float[] mAnimMatrix = new float[9];
+ // endregion
- public CropImageAnimation(ImageView cropImageView, CropOverlayView cropOverlayView) {
- mImageView = cropImageView;
- mCropOverlayView = cropOverlayView;
+ public CropImageAnimation(ImageView cropImageView, CropOverlayView cropOverlayView) {
+ mImageView = cropImageView;
+ mCropOverlayView = cropOverlayView;
- setDuration(300);
- setFillAfter(true);
- setInterpolator(new AccelerateDecelerateInterpolator());
- setAnimationListener(this);
- }
+ setDuration(300);
+ setFillAfter(true);
+ setInterpolator(new AccelerateDecelerateInterpolator());
+ setAnimationListener(this);
+ }
- public void setStartState(float[] boundPoints, Matrix imageMatrix) {
- reset();
- System.arraycopy(boundPoints, 0, mStartBoundPoints, 0, 8);
- mStartCropWindowRect.set(mCropOverlayView.getCropWindowRect());
- imageMatrix.getValues(mStartImageMatrix);
- }
+ public void setStartState(float[] boundPoints, Matrix imageMatrix) {
+ reset();
+ System.arraycopy(boundPoints, 0, mStartBoundPoints, 0, 8);
+ mStartCropWindowRect.set(mCropOverlayView.getCropWindowRect());
+ imageMatrix.getValues(mStartImageMatrix);
+ }
- public void setEndState(float[] boundPoints, Matrix imageMatrix) {
- System.arraycopy(boundPoints, 0, mEndBoundPoints, 0, 8);
- mEndCropWindowRect.set(mCropOverlayView.getCropWindowRect());
- imageMatrix.getValues(mEndImageMatrix);
- }
+ public void setEndState(float[] boundPoints, Matrix imageMatrix) {
+ System.arraycopy(boundPoints, 0, mEndBoundPoints, 0, 8);
+ mEndCropWindowRect.set(mCropOverlayView.getCropWindowRect());
+ imageMatrix.getValues(mEndImageMatrix);
+ }
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
-
- mAnimRect.left = mStartCropWindowRect.left + (mEndCropWindowRect.left - mStartCropWindowRect.left) * interpolatedTime;
- mAnimRect.top = mStartCropWindowRect.top + (mEndCropWindowRect.top - mStartCropWindowRect.top) * interpolatedTime;
- mAnimRect.right = mStartCropWindowRect.right + (mEndCropWindowRect.right - mStartCropWindowRect.right) * interpolatedTime;
- mAnimRect.bottom = mStartCropWindowRect.bottom + (mEndCropWindowRect.bottom - mStartCropWindowRect.bottom) * interpolatedTime;
- mCropOverlayView.setCropWindowRect(mAnimRect);
-
- for (int i = 0; i < mAnimPoints.length; i++) {
- mAnimPoints[i] = mStartBoundPoints[i] + (mEndBoundPoints[i] - mStartBoundPoints[i]) * interpolatedTime;
- }
- mCropOverlayView.setBounds(mAnimPoints, mImageView.getWidth(), mImageView.getHeight());
-
- for (int i = 0; i < mAnimMatrix.length; i++) {
- mAnimMatrix[i] = mStartImageMatrix[i] + (mEndImageMatrix[i] - mStartImageMatrix[i]) * interpolatedTime;
- }
- Matrix m = mImageView.getImageMatrix();
- m.setValues(mAnimMatrix);
- mImageView.setImageMatrix(m);
-
- mImageView.invalidate();
- mCropOverlayView.invalidate();
- }
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
- @Override
- public void onAnimationStart(Animation animation) {
+ mAnimRect.left =
+ mStartCropWindowRect.left
+ + (mEndCropWindowRect.left - mStartCropWindowRect.left) * interpolatedTime;
+ mAnimRect.top =
+ mStartCropWindowRect.top
+ + (mEndCropWindowRect.top - mStartCropWindowRect.top) * interpolatedTime;
+ mAnimRect.right =
+ mStartCropWindowRect.right
+ + (mEndCropWindowRect.right - mStartCropWindowRect.right) * interpolatedTime;
+ mAnimRect.bottom =
+ mStartCropWindowRect.bottom
+ + (mEndCropWindowRect.bottom - mStartCropWindowRect.bottom) * interpolatedTime;
+ mCropOverlayView.setCropWindowRect(mAnimRect);
+ for (int i = 0; i < mAnimPoints.length; i++) {
+ mAnimPoints[i] =
+ mStartBoundPoints[i] + (mEndBoundPoints[i] - mStartBoundPoints[i]) * interpolatedTime;
}
+ mCropOverlayView.setBounds(mAnimPoints, mImageView.getWidth(), mImageView.getHeight());
- @Override
- public void onAnimationEnd(Animation animation) {
- mImageView.clearAnimation();
+ for (int i = 0; i < mAnimMatrix.length; i++) {
+ mAnimMatrix[i] =
+ mStartImageMatrix[i] + (mEndImageMatrix[i] - mStartImageMatrix[i]) * interpolatedTime;
}
+ Matrix m = mImageView.getImageMatrix();
+ m.setValues(mAnimMatrix);
+ mImageView.setImageMatrix(m);
- @Override
- public void onAnimationRepeat(Animation animation) {
+ mImageView.invalidate();
+ mCropOverlayView.invalidate();
+ }
- }
-}
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mImageView.clearAnimation();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java
index 26cc1b80..240fe9b3 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java
@@ -19,6 +19,7 @@
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -28,455 +29,435 @@
*/
public class CropImageOptions implements Parcelable {
- public static final Creator CREATOR = new Creator() {
+ public static final Creator CREATOR =
+ new Creator() {
@Override
public CropImageOptions createFromParcel(Parcel in) {
- return new CropImageOptions(in);
+ return new CropImageOptions(in);
}
@Override
public CropImageOptions[] newArray(int size) {
- return new CropImageOptions[size];
+ return new CropImageOptions[size];
}
- };
-
- /**
- * The shape of the cropping window.
- */
- public CropImageView.CropShape cropShape;
-
- /**
- * An edge of the crop window will snap to the corresponding edge of a specified bounding box when the crop
- * window edge is less than or equal to this distance (in pixels) away from the bounding box edge. (in pixels)
- */
- public float snapRadius;
-
- /**
- * The radius of the touchable area around the handle. (in pixels)
- * We are basing this value off of the recommended 48dp Rhythm.
- * See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
- */
- public float touchRadius;
-
- /**
- * whether the guidelines should be on, off, or only showing when resizing.
- */
- public CropImageView.Guidelines guidelines;
-
- /**
- * The initial scale type of the image in the crop image view
- */
- public CropImageView.ScaleType scaleType;
-
- /**
- * if to show crop overlay UI what contains the crop window UI surrounded by background over the cropping
- * image.
- * default: true, may disable for animation or frame transition.
- */
- public boolean showCropOverlay;
-
- /**
- * if to show progress bar when image async loading/cropping is in progress.
- * default: true, disable to provide custom progress bar UI.
- */
- public boolean showProgressBar;
-
- /**
- * if auto-zoom functionality is enabled.
- * default: true.
- */
- public boolean autoZoomEnabled;
+ };
+
+ /** The shape of the cropping window. */
+ public CropImageView.CropShape cropShape;
/**
- * if multi-touch should be enabled on the crop box
- * default: false
+ * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
+ * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
+ * box edge. (in pixels)
*/
- public boolean multiTouchEnabled;
-
- /**
- * The max zoom allowed during cropping.
- */
- public int maxZoom;
-
- /**
- * The initial crop window padding from image borders in percentage of the cropping image dimensions.
- */
- public float initialCropWindowPaddingRatio;
-
- /**
- * whether the width to height aspect ratio should be maintained or free to change.
- */
- public boolean fixAspectRatio;
-
- /**
- * the X value of the aspect ratio.
- */
- public int aspectRatioX;
-
- /**
- * the Y value of the aspect ratio.
- */
- public int aspectRatioY;
-
- /**
- * the thickness of the guidelines lines in pixels. (in pixels)
- */
- public float borderLineThickness;
-
- /**
- * the color of the guidelines lines
- */
- public int borderLineColor;
-
- /**
- * thickness of the corner line. (in pixels)
- */
- public float borderCornerThickness;
-
- /**
- * the offset of corner line from crop window border. (in pixels)
- */
- public float borderCornerOffset;
-
- /**
- * the length of the corner line away from the corner. (in pixels)
- */
- public float borderCornerLength;
-
- /**
- * the color of the corner line
- */
- public int borderCornerColor;
-
- /**
- * the thickness of the guidelines lines. (in pixels)
- */
- public float guidelinesThickness;
-
- /**
- * the color of the guidelines lines
- */
- public int guidelinesColor;
-
- /**
- * the color of the overlay background around the crop window cover the image parts not in the crop window.
- */
- public int backgroundColor;
-
- /**
- * the min width the crop window is allowed to be. (in pixels)
- */
- public int minCropWindowWidth;
-
- /**
- * the min height the crop window is allowed to be. (in pixels)
- */
- public int minCropWindowHeight;
-
- /**
- * the min width the resulting cropping image is allowed to be, affects the cropping window limits. (in pixels)
- */
- public int minCropResultWidth;
-
- /**
- * the min height the resulting cropping image is allowed to be, affects the cropping window limits. (in pixels)
- */
- public int minCropResultHeight;
-
- /**
- * the max width the resulting cropping image is allowed to be, affects the cropping window limits. (in pixels)
- */
- public int maxCropResultWidth;
-
- /**
- * the max height the resulting cropping image is allowed to be, affects the cropping window limits. (in pixels)
- */
- public int maxCropResultHeight;
-
- /**
- * the title of the {@link CropImageActivity}
- */
- public String activityTitle;
-
- /**
- * the color to use for action bar items icons
- */
- public int activityMenuIconColor;
-
- /**
- * the Android Uri to save the cropped image to
- */
- public Uri outputUri;
-
- /**
- * the compression format to use when writing the image
- */
- public Bitmap.CompressFormat outputCompressFormat;
-
- /**
- * the quality (if applicable) to use when writing the image (0 - 100)
- */
- public int outputCompressQuality;
-
- /**
- * the width to resize the cropped image to (see options)
- */
- public int outputRequestWidth;
-
- /**
- * the height to resize the cropped image to (see options)
- */
- public int outputRequestHeight;
-
- /**
- * the resize method to use on the cropped bitmap (see options documentation)
- */
- public CropImageView.RequestSizeOptions outputRequestSizeOptions;
-
- /**
- * if the result of crop image activity should not save the cropped image bitmap
- */
- public boolean noOutputImage;
-
- /**
- * the initial rectangle to set on the cropping image after loading
- */
- public Rect initialCropWindowRectangle;
-
- /**
- * the initial rotation to set on the cropping image after loading (0-360 degrees clockwise)
- */
- public int initialRotation;
-
- /**
- * if to allow (all) rotation during cropping (activity)
- */
- public boolean allowRotation;
-
- /**
- * if to allow counter-clockwise rotation during cropping (activity)
- */
- public boolean allowCounterRotation;
-
- /**
- * the amount of degreees to rotate clockwise or counter-clockwise
- */
- public int rotationDegrees;
-
- /**
- * Init options with defaults.
- */
- public CropImageOptions() {
-
- DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
-
- cropShape = CropImageView.CropShape.RECTANGLE;
- snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
- touchRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm);
- guidelines = CropImageView.Guidelines.ON_TOUCH;
- scaleType = CropImageView.ScaleType.FIT_CENTER;
- showCropOverlay = true;
- showProgressBar = true;
- autoZoomEnabled = true;
- multiTouchEnabled = false;
- maxZoom = 4;
- initialCropWindowPaddingRatio = 0.1f;
-
- fixAspectRatio = false;
- aspectRatioX = 1;
- aspectRatioY = 1;
-
- borderLineThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
- borderLineColor = Color.argb(170, 255, 255, 255);
- borderCornerThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
- borderCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
- borderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dm);
- borderCornerColor = Color.WHITE;
-
- guidelinesThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm);
- guidelinesColor = Color.argb(170, 255, 255, 255);
- backgroundColor = Color.argb(119, 0, 0, 0);
-
- minCropWindowWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
- minCropWindowHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
- minCropResultWidth = 40;
- minCropResultHeight = 40;
- maxCropResultWidth = 99999;
- maxCropResultHeight = 99999;
-
- activityTitle = "";
- activityMenuIconColor = 0;
-
- outputUri = Uri.EMPTY;
- outputCompressFormat = Bitmap.CompressFormat.JPEG;
- outputCompressQuality = 90;
- outputRequestWidth = 0;
- outputRequestHeight = 0;
- outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE;
- noOutputImage = false;
-
- initialCropWindowRectangle = null;
- initialRotation = -1;
- allowRotation = true;
- allowCounterRotation = false;
- rotationDegrees = 90;
- }
+ public float snapRadius;
- /**
- * Create object from parcel.
- */
- protected CropImageOptions(Parcel in) {
- cropShape = CropImageView.CropShape.values()[in.readInt()];
- snapRadius = in.readFloat();
- touchRadius = in.readFloat();
- guidelines = CropImageView.Guidelines.values()[in.readInt()];
- scaleType = CropImageView.ScaleType.values()[in.readInt()];
- showCropOverlay = in.readByte() != 0;
- showProgressBar = in.readByte() != 0;
- autoZoomEnabled = in.readByte() != 0;
- multiTouchEnabled = in.readByte() != 0;
- maxZoom = in.readInt();
- initialCropWindowPaddingRatio = in.readFloat();
- fixAspectRatio = in.readByte() != 0;
- aspectRatioX = in.readInt();
- aspectRatioY = in.readInt();
- borderLineThickness = in.readFloat();
- borderLineColor = in.readInt();
- borderCornerThickness = in.readFloat();
- borderCornerOffset = in.readFloat();
- borderCornerLength = in.readFloat();
- borderCornerColor = in.readInt();
- guidelinesThickness = in.readFloat();
- guidelinesColor = in.readInt();
- backgroundColor = in.readInt();
- minCropWindowWidth = in.readInt();
- minCropWindowHeight = in.readInt();
- minCropResultWidth = in.readInt();
- minCropResultHeight = in.readInt();
- maxCropResultWidth = in.readInt();
- maxCropResultHeight = in.readInt();
- activityTitle = in.readString();
- activityMenuIconColor = in.readInt();
- outputUri = in.readParcelable(Uri.class.getClassLoader());
- outputCompressFormat = Bitmap.CompressFormat.valueOf(in.readString());
- outputCompressQuality = in.readInt();
- outputRequestWidth = in.readInt();
- outputRequestHeight = in.readInt();
- outputRequestSizeOptions = CropImageView.RequestSizeOptions.values()[in.readInt()];
- noOutputImage = in.readByte() != 0;
- initialCropWindowRectangle = in.readParcelable(Rect.class.getClassLoader());
- initialRotation = in.readInt();
- allowRotation = in.readByte() != 0;
- allowCounterRotation = in.readByte() != 0;
- rotationDegrees = in.readInt();
- }
+ /**
+ * The radius of the touchable area around the handle. (in pixels)
+ * We are basing this value off of the recommended 48dp Rhythm.
+ * See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
+ */
+ public float touchRadius;
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(cropShape.ordinal());
- dest.writeFloat(snapRadius);
- dest.writeFloat(touchRadius);
- dest.writeInt(guidelines.ordinal());
- dest.writeInt(scaleType.ordinal());
- dest.writeByte((byte) (showCropOverlay ? 1 : 0));
- dest.writeByte((byte) (showProgressBar ? 1 : 0));
- dest.writeByte((byte) (autoZoomEnabled ? 1 : 0));
- dest.writeByte((byte) (multiTouchEnabled ? 1 : 0));
- dest.writeInt(maxZoom);
- dest.writeFloat(initialCropWindowPaddingRatio);
- dest.writeByte((byte) (fixAspectRatio ? 1 : 0));
- dest.writeInt(aspectRatioX);
- dest.writeInt(aspectRatioY);
- dest.writeFloat(borderLineThickness);
- dest.writeInt(borderLineColor);
- dest.writeFloat(borderCornerThickness);
- dest.writeFloat(borderCornerOffset);
- dest.writeFloat(borderCornerLength);
- dest.writeInt(borderCornerColor);
- dest.writeFloat(guidelinesThickness);
- dest.writeInt(guidelinesColor);
- dest.writeInt(backgroundColor);
- dest.writeInt(minCropWindowWidth);
- dest.writeInt(minCropWindowHeight);
- dest.writeInt(minCropResultWidth);
- dest.writeInt(minCropResultHeight);
- dest.writeInt(maxCropResultWidth);
- dest.writeInt(maxCropResultHeight);
- dest.writeString(activityTitle);
- dest.writeInt(activityMenuIconColor);
- dest.writeParcelable(outputUri, flags);
- dest.writeString(outputCompressFormat.name());
- dest.writeInt(outputCompressQuality);
- dest.writeInt(outputRequestWidth);
- dest.writeInt(outputRequestHeight);
- dest.writeInt(outputRequestSizeOptions.ordinal());
- dest.writeInt(noOutputImage ? 1 : 0);
- dest.writeParcelable(initialCropWindowRectangle, flags);
- dest.writeInt(initialRotation);
- dest.writeByte((byte) (allowRotation ? 1 : 0));
- dest.writeByte((byte) (allowCounterRotation ? 1 : 0));
- dest.writeInt(rotationDegrees);
- }
+ /** whether the guidelines should be on, off, or only showing when resizing. */
+ public CropImageView.Guidelines guidelines;
- @Override
- public int describeContents() {
- return 0;
- }
+ /** The initial scale type of the image in the crop image view */
+ public CropImageView.ScaleType scaleType;
- /**
- * Validate all the options are withing valid range.
- *
- * @throws IllegalArgumentException if any of the options is not valid
- */
- public void validate() {
- if (maxZoom < 0) {
- throw new IllegalArgumentException("Cannot set max zoom to a number < 1");
- }
- if (touchRadius < 0) {
- throw new IllegalArgumentException("Cannot set touch radius value to a number <= 0 ");
- }
- if (initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5) {
- throw new IllegalArgumentException("Cannot set initial crop window padding value to a number < 0 or >= 0.5");
- }
- if (aspectRatioX <= 0) {
- throw new IllegalArgumentException("Cannot set aspect ratio value to a number less than or equal to 0.");
- }
- if (aspectRatioY <= 0) {
- throw new IllegalArgumentException("Cannot set aspect ratio value to a number less than or equal to 0.");
- }
- if (borderLineThickness < 0) {
- throw new IllegalArgumentException("Cannot set line thickness value to a number less than 0.");
- }
- if (borderCornerThickness < 0) {
- throw new IllegalArgumentException("Cannot set corner thickness value to a number less than 0.");
- }
- if (guidelinesThickness < 0) {
- throw new IllegalArgumentException("Cannot set guidelines thickness value to a number less than 0.");
- }
- if (minCropWindowHeight < 0) {
- throw new IllegalArgumentException("Cannot set min crop window height value to a number < 0 ");
- }
- if (minCropResultWidth < 0) {
- throw new IllegalArgumentException("Cannot set min crop result width value to a number < 0 ");
- }
- if (minCropResultHeight < 0) {
- throw new IllegalArgumentException("Cannot set min crop result height value to a number < 0 ");
- }
- if (maxCropResultWidth < minCropResultWidth) {
- throw new IllegalArgumentException("Cannot set max crop result width to smaller value than min crop result width");
- }
- if (maxCropResultHeight < minCropResultHeight) {
- throw new IllegalArgumentException("Cannot set max crop result height to smaller value than min crop result height");
- }
- if (outputRequestWidth < 0) {
- throw new IllegalArgumentException("Cannot set request width value to a number < 0 ");
- }
- if (outputRequestHeight < 0) {
- throw new IllegalArgumentException("Cannot set request height value to a number < 0 ");
- }
- if (rotationDegrees < 0 || rotationDegrees > 360) {
- throw new IllegalArgumentException("Cannot set rotation degrees value to a number < 0 or > 360");
- }
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public boolean showCropOverlay;
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public boolean showProgressBar;
+
+ /**
+ * if auto-zoom functionality is enabled.
+ * default: true.
+ */
+ public boolean autoZoomEnabled;
+
+ /** if multi-touch should be enabled on the crop box default: false */
+ public boolean multiTouchEnabled;
+
+ /** The max zoom allowed during cropping. */
+ public int maxZoom;
+
+ /**
+ * The initial crop window padding from image borders in percentage of the cropping image
+ * dimensions.
+ */
+ public float initialCropWindowPaddingRatio;
+
+ /** whether the width to height aspect ratio should be maintained or free to change. */
+ public boolean fixAspectRatio;
+
+ /** the X value of the aspect ratio. */
+ public int aspectRatioX;
+
+ /** the Y value of the aspect ratio. */
+ public int aspectRatioY;
+
+ /** the thickness of the guidelines lines in pixels. (in pixels) */
+ public float borderLineThickness;
+
+ /** the color of the guidelines lines */
+ public int borderLineColor;
+
+ /** thickness of the corner line. (in pixels) */
+ public float borderCornerThickness;
+
+ /** the offset of corner line from crop window border. (in pixels) */
+ public float borderCornerOffset;
+
+ /** the length of the corner line away from the corner. (in pixels) */
+ public float borderCornerLength;
+
+ /** the color of the corner line */
+ public int borderCornerColor;
+
+ /** the thickness of the guidelines lines. (in pixels) */
+ public float guidelinesThickness;
+
+ /** the color of the guidelines lines */
+ public int guidelinesColor;
+
+ /**
+ * the color of the overlay background around the crop window cover the image parts not in the
+ * crop window.
+ */
+ public int backgroundColor;
+
+ /** the min width the crop window is allowed to be. (in pixels) */
+ public int minCropWindowWidth;
+
+ /** the min height the crop window is allowed to be. (in pixels) */
+ public int minCropWindowHeight;
+
+ /**
+ * the min width the resulting cropping image is allowed to be, affects the cropping window
+ * limits. (in pixels)
+ */
+ public int minCropResultWidth;
+
+ /**
+ * the min height the resulting cropping image is allowed to be, affects the cropping window
+ * limits. (in pixels)
+ */
+ public int minCropResultHeight;
+
+ /**
+ * the max width the resulting cropping image is allowed to be, affects the cropping window
+ * limits. (in pixels)
+ */
+ public int maxCropResultWidth;
+
+ /**
+ * the max height the resulting cropping image is allowed to be, affects the cropping window
+ * limits. (in pixels)
+ */
+ public int maxCropResultHeight;
+
+ /** the title of the {@link CropImageActivity} */
+ public CharSequence activityTitle;
+
+ /** the color to use for action bar items icons */
+ public int activityMenuIconColor;
+
+ /** the Android Uri to save the cropped image to */
+ public Uri outputUri;
+
+ /** the compression format to use when writing the image */
+ public Bitmap.CompressFormat outputCompressFormat;
+
+ /** the quality (if applicable) to use when writing the image (0 - 100) */
+ public int outputCompressQuality;
+
+ /** the width to resize the cropped image to (see options) */
+ public int outputRequestWidth;
+
+ /** the height to resize the cropped image to (see options) */
+ public int outputRequestHeight;
+
+ /** the resize method to use on the cropped bitmap (see options documentation) */
+ public CropImageView.RequestSizeOptions outputRequestSizeOptions;
+
+ /** if the result of crop image activity should not save the cropped image bitmap */
+ public boolean noOutputImage;
+
+ /** the initial rectangle to set on the cropping image after loading */
+ public Rect initialCropWindowRectangle;
+
+ /** the initial rotation to set on the cropping image after loading (0-360 degrees clockwise) */
+ public int initialRotation;
+
+ /** if to allow (all) rotation during cropping (activity) */
+ public boolean allowRotation;
+
+ /** if to allow (all) flipping during cropping (activity) */
+ public boolean allowFlipping;
+
+ /** if to allow counter-clockwise rotation during cropping (activity) */
+ public boolean allowCounterRotation;
+
+ /** the amount of degrees to rotate clockwise or counter-clockwise */
+ public int rotationDegrees;
+
+ /** whether the image should be flipped horizontally */
+ public boolean flipHorizontally;
+
+ /** whether the image should be flipped vertically */
+ public boolean flipVertically;
+
+ /** optional, the text of the crop menu crop button */
+ public CharSequence cropMenuCropButtonTitle;
+
+ /** optional image resource to be used for crop menu crop icon instead of text */
+ public int cropMenuCropButtonIcon;
+
+ /** Init options with defaults. */
+ public CropImageOptions() {
+
+ DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+
+ cropShape = CropImageView.CropShape.RECTANGLE;
+ snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
+ touchRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm);
+ guidelines = CropImageView.Guidelines.ON_TOUCH;
+ scaleType = CropImageView.ScaleType.FIT_CENTER;
+ showCropOverlay = true;
+ showProgressBar = true;
+ autoZoomEnabled = true;
+ multiTouchEnabled = false;
+ maxZoom = 4;
+ initialCropWindowPaddingRatio = 0.1f;
+
+ fixAspectRatio = false;
+ aspectRatioX = 1;
+ aspectRatioY = 1;
+
+ borderLineThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
+ borderLineColor = Color.argb(170, 255, 255, 255);
+ borderCornerThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
+ borderCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
+ borderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dm);
+ borderCornerColor = Color.WHITE;
+
+ guidelinesThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm);
+ guidelinesColor = Color.argb(170, 255, 255, 255);
+ backgroundColor = Color.argb(119, 0, 0, 0);
+
+ minCropWindowWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
+ minCropWindowHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
+ minCropResultWidth = 40;
+ minCropResultHeight = 40;
+ maxCropResultWidth = 99999;
+ maxCropResultHeight = 99999;
+
+ activityTitle = "";
+ activityMenuIconColor = 0;
+
+ outputUri = Uri.EMPTY;
+ outputCompressFormat = Bitmap.CompressFormat.JPEG;
+ outputCompressQuality = 90;
+ outputRequestWidth = 0;
+ outputRequestHeight = 0;
+ outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE;
+ noOutputImage = false;
+
+ initialCropWindowRectangle = null;
+ initialRotation = -1;
+ allowRotation = true;
+ allowFlipping = true;
+ allowCounterRotation = false;
+ rotationDegrees = 90;
+ flipHorizontally = false;
+ flipVertically = false;
+ cropMenuCropButtonTitle = null;
+
+ cropMenuCropButtonIcon = 0;
+ }
+
+ /** Create object from parcel. */
+ protected CropImageOptions(Parcel in) {
+ cropShape = CropImageView.CropShape.values()[in.readInt()];
+ snapRadius = in.readFloat();
+ touchRadius = in.readFloat();
+ guidelines = CropImageView.Guidelines.values()[in.readInt()];
+ scaleType = CropImageView.ScaleType.values()[in.readInt()];
+ showCropOverlay = in.readByte() != 0;
+ showProgressBar = in.readByte() != 0;
+ autoZoomEnabled = in.readByte() != 0;
+ multiTouchEnabled = in.readByte() != 0;
+ maxZoom = in.readInt();
+ initialCropWindowPaddingRatio = in.readFloat();
+ fixAspectRatio = in.readByte() != 0;
+ aspectRatioX = in.readInt();
+ aspectRatioY = in.readInt();
+ borderLineThickness = in.readFloat();
+ borderLineColor = in.readInt();
+ borderCornerThickness = in.readFloat();
+ borderCornerOffset = in.readFloat();
+ borderCornerLength = in.readFloat();
+ borderCornerColor = in.readInt();
+ guidelinesThickness = in.readFloat();
+ guidelinesColor = in.readInt();
+ backgroundColor = in.readInt();
+ minCropWindowWidth = in.readInt();
+ minCropWindowHeight = in.readInt();
+ minCropResultWidth = in.readInt();
+ minCropResultHeight = in.readInt();
+ maxCropResultWidth = in.readInt();
+ maxCropResultHeight = in.readInt();
+ activityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ activityMenuIconColor = in.readInt();
+ outputUri = in.readParcelable(Uri.class.getClassLoader());
+ outputCompressFormat = Bitmap.CompressFormat.valueOf(in.readString());
+ outputCompressQuality = in.readInt();
+ outputRequestWidth = in.readInt();
+ outputRequestHeight = in.readInt();
+ outputRequestSizeOptions = CropImageView.RequestSizeOptions.values()[in.readInt()];
+ noOutputImage = in.readByte() != 0;
+ initialCropWindowRectangle = in.readParcelable(Rect.class.getClassLoader());
+ initialRotation = in.readInt();
+ allowRotation = in.readByte() != 0;
+ allowFlipping = in.readByte() != 0;
+ allowCounterRotation = in.readByte() != 0;
+ rotationDegrees = in.readInt();
+ flipHorizontally = in.readByte() != 0;
+ flipVertically = in.readByte() != 0;
+ cropMenuCropButtonTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ cropMenuCropButtonIcon = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(cropShape.ordinal());
+ dest.writeFloat(snapRadius);
+ dest.writeFloat(touchRadius);
+ dest.writeInt(guidelines.ordinal());
+ dest.writeInt(scaleType.ordinal());
+ dest.writeByte((byte) (showCropOverlay ? 1 : 0));
+ dest.writeByte((byte) (showProgressBar ? 1 : 0));
+ dest.writeByte((byte) (autoZoomEnabled ? 1 : 0));
+ dest.writeByte((byte) (multiTouchEnabled ? 1 : 0));
+ dest.writeInt(maxZoom);
+ dest.writeFloat(initialCropWindowPaddingRatio);
+ dest.writeByte((byte) (fixAspectRatio ? 1 : 0));
+ dest.writeInt(aspectRatioX);
+ dest.writeInt(aspectRatioY);
+ dest.writeFloat(borderLineThickness);
+ dest.writeInt(borderLineColor);
+ dest.writeFloat(borderCornerThickness);
+ dest.writeFloat(borderCornerOffset);
+ dest.writeFloat(borderCornerLength);
+ dest.writeInt(borderCornerColor);
+ dest.writeFloat(guidelinesThickness);
+ dest.writeInt(guidelinesColor);
+ dest.writeInt(backgroundColor);
+ dest.writeInt(minCropWindowWidth);
+ dest.writeInt(minCropWindowHeight);
+ dest.writeInt(minCropResultWidth);
+ dest.writeInt(minCropResultHeight);
+ dest.writeInt(maxCropResultWidth);
+ dest.writeInt(maxCropResultHeight);
+ TextUtils.writeToParcel(activityTitle, dest, flags);
+ dest.writeInt(activityMenuIconColor);
+ dest.writeParcelable(outputUri, flags);
+ dest.writeString(outputCompressFormat.name());
+ dest.writeInt(outputCompressQuality);
+ dest.writeInt(outputRequestWidth);
+ dest.writeInt(outputRequestHeight);
+ dest.writeInt(outputRequestSizeOptions.ordinal());
+ dest.writeInt(noOutputImage ? 1 : 0);
+ dest.writeParcelable(initialCropWindowRectangle, flags);
+ dest.writeInt(initialRotation);
+ dest.writeByte((byte) (allowRotation ? 1 : 0));
+ dest.writeByte((byte) (allowFlipping ? 1 : 0));
+ dest.writeByte((byte) (allowCounterRotation ? 1 : 0));
+ dest.writeInt(rotationDegrees);
+ dest.writeByte((byte) (flipHorizontally ? 1 : 0));
+ dest.writeByte((byte) (flipVertically ? 1 : 0));
+ TextUtils.writeToParcel(cropMenuCropButtonTitle, dest, flags);
+ dest.writeInt(cropMenuCropButtonIcon);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Validate all the options are withing valid range.
+ *
+ * @throws IllegalArgumentException if any of the options is not valid
+ */
+ public void validate() {
+ if (maxZoom < 0) {
+ throw new IllegalArgumentException("Cannot set max zoom to a number < 1");
+ }
+ if (touchRadius < 0) {
+ throw new IllegalArgumentException("Cannot set touch radius value to a number <= 0 ");
+ }
+ if (initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5) {
+ throw new IllegalArgumentException(
+ "Cannot set initial crop window padding value to a number < 0 or >= 0.5");
+ }
+ if (aspectRatioX <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ }
+ if (aspectRatioY <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ }
+ if (borderLineThickness < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set line thickness value to a number less than 0.");
}
+ if (borderCornerThickness < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set corner thickness value to a number less than 0.");
+ }
+ if (guidelinesThickness < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set guidelines thickness value to a number less than 0.");
+ }
+ if (minCropWindowHeight < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set min crop window height value to a number < 0 ");
+ }
+ if (minCropResultWidth < 0) {
+ throw new IllegalArgumentException("Cannot set min crop result width value to a number < 0 ");
+ }
+ if (minCropResultHeight < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set min crop result height value to a number < 0 ");
+ }
+ if (maxCropResultWidth < minCropResultWidth) {
+ throw new IllegalArgumentException(
+ "Cannot set max crop result width to smaller value than min crop result width");
+ }
+ if (maxCropResultHeight < minCropResultHeight) {
+ throw new IllegalArgumentException(
+ "Cannot set max crop result height to smaller value than min crop result height");
+ }
+ if (outputRequestWidth < 0) {
+ throw new IllegalArgumentException("Cannot set request width value to a number < 0 ");
+ }
+ if (outputRequestHeight < 0) {
+ throw new IllegalArgumentException("Cannot set request height value to a number < 0 ");
+ }
+ if (rotationDegrees < 0 || rotationDegrees > 360) {
+ throw new IllegalArgumentException(
+ "Cannot set rotation degrees value to a number < 0 or > 360");
+ }
+ }
}
-
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java
index 92e4b541..77b18613 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java
@@ -21,10 +21,11 @@
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
-import android.media.ExifInterface;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
+import androidx.exifinterface.media.ExifInterface;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
@@ -37,1763 +38,2098 @@
import java.lang.ref.WeakReference;
import java.util.UUID;
-/**
- * Custom view that provides cropping capabilities to an image.
- */
+/** Custom view that provides cropping capabilities to an image. */
public class CropImageView extends FrameLayout {
- //region: Fields and Consts
+ // region: Fields and Consts
- /**
- * Image view widget used to show the image for cropping.
- */
- private final ImageView mImageView;
-
- /**
- * Overlay over the image view to show cropping UI.
- */
- private final CropOverlayView mCropOverlayView;
-
- /**
- * The matrix used to transform the cropping image in the image view
- */
- private final Matrix mImageMatrix = new Matrix();
-
- /**
- * Reusing matrix instance for reverse matrix calculations.
- */
- private final Matrix mImageInverseMatrix = new Matrix();
-
- /**
- * Progress bar widget to show progress bar on async image loading and cropping.
- */
- private final ProgressBar mProgressBar;
+ /** Image view widget used to show the image for cropping. */
+ private final ImageView mImageView;
- /**
- * Rectengale used in image matrix transformation calculation (reusing rect instance)
- */
- private final float[] mImagePoints = new float[8];
-
- /**
- * Animation class to smooth animate zoom-in/out
- */
- private CropImageAnimation mAnimation;
-
- private Bitmap mBitmap;
-
- private int mDegreesRotated;
+ /** Overlay over the image view to show cropping UI. */
+ private final CropOverlayView mCropOverlayView;
- private int mLayoutWidth;
+ /** The matrix used to transform the cropping image in the image view */
+ private final Matrix mImageMatrix = new Matrix();
- private int mLayoutHeight;
+ /** Reusing matrix instance for reverse matrix calculations. */
+ private final Matrix mImageInverseMatrix = new Matrix();
- private int mImageResource;
+ /** Progress bar widget to show progress bar on async image loading and cropping. */
+ private final ProgressBar mProgressBar;
- /**
- * The initial scale type of the image in the crop image view
- */
- private ScaleType mScaleType;
+ /** Rectangle used in image matrix transformation calculation (reusing rect instance) */
+ private final float[] mImagePoints = new float[8];
- /**
- * if to show crop overlay UI what contains the crop window UI surrounded by background over the cropping
- * image.
- * default: true, may disable for animation or frame transition.
- */
- private boolean mShowCropOverlay = true;
+ /** Rectangle used in image matrix transformation for scale calculation (reusing rect instance) */
+ private final float[] mScaleImagePoints = new float[8];
- /**
- * if to show progress bar when image async loading/cropping is in progress.
- * default: true, disable to provide custom progress bar UI.
- */
- private boolean mShowProgressBar = true;
+ /** Animation class to smooth animate zoom-in/out */
+ private CropImageAnimation mAnimation;
- /**
- * if auto-zoom functionality is enabled.
- * default: true.
- */
- private boolean mAutoZoomEnabled = true;
+ private Bitmap mBitmap;
- /**
- * The max zoom allowed during cropping
- */
- private int mMaxZoom;
+ /** The image rotation value used during loading of the image so we can reset to it */
+ private int mInitialDegreesRotated;
- /**
- * callback to be invoked when image async loading is complete.
- */
- private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;
+ /** How much the image is rotated from original clockwise */
+ private int mDegreesRotated;
- /**
- * callback to be invoked when image async cropping is complete.
- */
- private OnCropImageCompleteListener mOnCropImageCompleteListener;
+ /** if the image flipped horizontally */
+ private boolean mFlipHorizontally;
- /**
- * callback to be invoked when image async cropping is complete (get bitmap)
- */
- @Deprecated
- private OnGetCroppedImageCompleteListener mOnGetCroppedImageCompleteListener;
+ /** if the image flipped vertically */
+ private boolean mFlipVertically;
- /**
- * callback to be invoked when image async cropping is complete (save to uri)
- */
- @Deprecated
- private OnSaveCroppedImageCompleteListener mOnSaveCroppedImageCompleteListener;
+ private int mLayoutWidth;
- /**
- * The URI that the image was loaded from (if loaded from URI)
- */
- private Uri mLoadedImageUri;
+ private int mLayoutHeight;
- /**
- * The sample size the image was loaded by if was loaded by URI
- */
- private int mLoadedSampleSize = 1;
+ private int mImageResource;
- /**
- * The current zoom level to to scale the cropping image
- */
- private float mZoom = 1;
+ /** The initial scale type of the image in the crop image view */
+ private ScaleType mScaleType;
- /**
- * The X offset that the cropping image was translated after zooming
- */
- private float mZoomOffsetX;
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ private boolean mSaveBitmapToInstanceState = false;
- /**
- * The Y offset that the cropping image was translated after zooming
- */
- private float mZoomOffsetY;
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ private boolean mShowCropOverlay = true;
- /**
- * Used to restore the cropping windows rectangle after state restore
- */
- private RectF mRestoreCropWindowRect;
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ private boolean mShowProgressBar = true;
- /**
- * Task used to load bitmap async from UI thread
- */
- private WeakReference mBitmapLoadingWorkerTask;
+ /**
+ * if auto-zoom functionality is enabled.
+ * default: true.
+ */
+ private boolean mAutoZoomEnabled = true;
- /**
- * Task used to crop bitmap async from UI thread
- */
- private WeakReference mBitmapCroppingWorkerTask;
- //endregion
+ /** The max zoom allowed during cropping */
+ private int mMaxZoom;
- public CropImageView(Context context) {
- this(context, null);
- }
+ /** callback to be invoked when crop overlay is released. */
+ private OnSetCropOverlayReleasedListener mOnCropOverlayReleasedListener;
- public CropImageView(Context context, AttributeSet attrs) {
- super(context, attrs);
+ /** callback to be invoked when crop overlay is moved. */
+ private OnSetCropOverlayMovedListener mOnSetCropOverlayMovedListener;
- CropImageOptions options = null;
- Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
- if (intent != null) {
- options = intent.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
+ /** callback to be invoked when crop window is changed. */
+ private OnSetCropWindowChangeListener mOnSetCropWindowChangeListener;
+
+ /** callback to be invoked when image async loading is complete. */
+ private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;
+
+ /** callback to be invoked when image async cropping is complete. */
+ private OnCropImageCompleteListener mOnCropImageCompleteListener;
+
+ /** The URI that the image was loaded from (if loaded from URI) */
+ private Uri mLoadedImageUri;
+
+ /** The sample size the image was loaded by if was loaded by URI */
+ private int mLoadedSampleSize = 1;
+
+ /** The current zoom level to to scale the cropping image */
+ private float mZoom = 1;
+
+ /** The X offset that the cropping image was translated after zooming */
+ private float mZoomOffsetX;
+
+ /** The Y offset that the cropping image was translated after zooming */
+ private float mZoomOffsetY;
+
+ /** Used to restore the cropping windows rectangle after state restore */
+ private RectF mRestoreCropWindowRect;
+
+ /** Used to restore image rotation after state restore */
+ private int mRestoreDegreesRotated;
+
+ /**
+ * Used to detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean,
+ * boolean)} in {@link #layout(int, int, int, int)}.
+ */
+ private boolean mSizeChanged;
+
+ /**
+ * Temp URI used to save bitmap image to disk to preserve for instance state in case cropped was
+ * set with bitmap
+ */
+ private Uri mSaveInstanceStateBitmapUri;
+
+ /** Task used to load bitmap async from UI thread */
+ private WeakReference mBitmapLoadingWorkerTask;
+
+ /** Task used to crop bitmap async from UI thread */
+ private WeakReference mBitmapCroppingWorkerTask;
+ // endregion
+
+ public CropImageView(Context context) {
+ this(context, null);
+ }
+
+ public CropImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ CropImageOptions options = null;
+ Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
+ if (intent != null) {
+ Bundle bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
+ if (bundle != null) {
+ options = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
+ }
+ }
+
+ if (options == null) {
+
+ options = new CropImageOptions();
+
+ if (attrs != null) {
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
+ try {
+ options.fixAspectRatio =
+ ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
+ options.aspectRatioX =
+ ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
+ options.aspectRatioY =
+ ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
+ options.scaleType =
+ ScaleType.values()[
+ ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
+ options.autoZoomEnabled =
+ ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
+ options.multiTouchEnabled =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
+ options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
+ options.cropShape =
+ CropShape.values()[
+ ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
+ options.guidelines =
+ Guidelines.values()[
+ ta.getInt(
+ R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
+ options.snapRadius =
+ ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
+ options.touchRadius =
+ ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
+ options.initialCropWindowPaddingRatio =
+ ta.getFloat(
+ R.styleable.CropImageView_cropInitialCropWindowPaddingRatio,
+ options.initialCropWindowPaddingRatio);
+ options.borderLineThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
+ options.borderLineColor =
+ ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
+ options.borderCornerThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerThickness,
+ options.borderCornerThickness);
+ options.borderCornerOffset =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
+ options.borderCornerLength =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
+ options.borderCornerColor =
+ ta.getInteger(
+ R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
+ options.guidelinesThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
+ options.guidelinesColor =
+ ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
+ options.backgroundColor =
+ ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
+ options.showCropOverlay =
+ ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
+ options.showProgressBar =
+ ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
+ options.borderCornerThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerThickness,
+ options.borderCornerThickness);
+ options.minCropWindowWidth =
+ (int)
+ ta.getDimension(
+ R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
+ options.minCropWindowHeight =
+ (int)
+ ta.getDimension(
+ R.styleable.CropImageView_cropMinCropWindowHeight,
+ options.minCropWindowHeight);
+ options.minCropResultWidth =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMinCropResultWidthPX,
+ options.minCropResultWidth);
+ options.minCropResultHeight =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMinCropResultHeightPX,
+ options.minCropResultHeight);
+ options.maxCropResultWidth =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMaxCropResultWidthPX,
+ options.maxCropResultWidth);
+ options.maxCropResultHeight =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMaxCropResultHeightPX,
+ options.maxCropResultHeight);
+ options.flipHorizontally =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropFlipHorizontally, options.flipHorizontally);
+ options.flipVertically =
+ ta.getBoolean(R.styleable.CropImageView_cropFlipHorizontally, options.flipVertically);
+
+ mSaveBitmapToInstanceState =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropSaveBitmapToInstanceState,
+ mSaveBitmapToInstanceState);
+
+ // if aspect ratio is set then set fixed to true
+ if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
+ && ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
+ && !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
+ options.fixAspectRatio = true;
+ }
+ } finally {
+ ta.recycle();
}
-
- if (options == null) {
-
- options = new CropImageOptions();
-
- if (attrs != null) {
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
- try {
- options.fixAspectRatio = ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
- options.aspectRatioX = ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
- options.aspectRatioY = ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
- options.scaleType = ScaleType.values()[ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
- options.autoZoomEnabled = ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
- options.multiTouchEnabled = ta.getBoolean(R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
- options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
- options.cropShape = CropShape.values()[ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
- options.guidelines = Guidelines.values()[ta.getInt(R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
- options.snapRadius = ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
- options.touchRadius = ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
- options.initialCropWindowPaddingRatio = ta.getFloat(R.styleable.CropImageView_cropInitialCropWindowPaddingRatio, options.initialCropWindowPaddingRatio);
- options.borderLineThickness = ta.getDimension(R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
- options.borderLineColor = ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
- options.borderCornerThickness = ta.getDimension(R.styleable.CropImageView_cropBorderCornerThickness, options.borderCornerThickness);
- options.borderCornerOffset = ta.getDimension(R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
- options.borderCornerLength = ta.getDimension(R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
- options.borderCornerColor = ta.getInteger(R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
- options.guidelinesThickness = ta.getDimension(R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
- options.guidelinesColor = ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
- options.backgroundColor = ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
- options.showCropOverlay = ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
- options.showProgressBar = ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
- options.borderCornerThickness = ta.getDimension(R.styleable.CropImageView_cropBorderCornerThickness, options.borderCornerThickness);
- options.minCropWindowWidth = (int) ta.getDimension(R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
- options.minCropWindowHeight = (int) ta.getDimension(R.styleable.CropImageView_cropMinCropWindowHeight, options.minCropWindowHeight);
- options.minCropResultWidth = (int) ta.getFloat(R.styleable.CropImageView_cropMinCropResultWidthPX, options.minCropResultWidth);
- options.minCropResultHeight = (int) ta.getFloat(R.styleable.CropImageView_cropMinCropResultHeightPX, options.minCropResultHeight);
- options.maxCropResultWidth = (int) ta.getFloat(R.styleable.CropImageView_cropMaxCropResultWidthPX, options.maxCropResultWidth);
- options.maxCropResultHeight = (int) ta.getFloat(R.styleable.CropImageView_cropMaxCropResultHeightPX, options.maxCropResultHeight);
-
- // if aspect ratio is set then set fixed to true
- if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX) &&
- ta.hasValue(R.styleable.CropImageView_cropAspectRatioX) &&
- !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
- options.fixAspectRatio = true;
- }
- } finally {
- ta.recycle();
- }
+ }
+ }
+
+ options.validate();
+
+ mScaleType = options.scaleType;
+ mAutoZoomEnabled = options.autoZoomEnabled;
+ mMaxZoom = options.maxZoom;
+ mShowCropOverlay = options.showCropOverlay;
+ mShowProgressBar = options.showProgressBar;
+ mFlipHorizontally = options.flipHorizontally;
+ mFlipVertically = options.flipVertically;
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View v = inflater.inflate(R.layout.crop_image_view, this, true);
+
+ mImageView = v.findViewById(R.id.ImageView_image);
+ mImageView.setScaleType(ImageView.ScaleType.MATRIX);
+
+ mCropOverlayView = v.findViewById(R.id.CropOverlayView);
+ mCropOverlayView.setCropWindowChangeListener(
+ new CropOverlayView.CropWindowChangeListener() {
+ @Override
+ public void onCropWindowChanged(boolean inProgress) {
+ handleCropWindowChanged(inProgress, true);
+ OnSetCropOverlayReleasedListener listener = mOnCropOverlayReleasedListener;
+ if (listener != null && !inProgress) {
+ listener.onCropOverlayReleased(getCropRect());
}
- }
-
- options.validate();
-
- mScaleType = options.scaleType;
- mAutoZoomEnabled = options.autoZoomEnabled;
- mMaxZoom = options.maxZoom;
- mShowCropOverlay = options.showCropOverlay;
- mShowProgressBar = options.showProgressBar;
-
- LayoutInflater inflater = LayoutInflater.from(context);
- View v = inflater.inflate(R.layout.crop_image_view, this, true);
-
- mImageView = (ImageView) v.findViewById(R.id.ImageView_image);
- mImageView.setScaleType(ImageView.ScaleType.MATRIX);
-
- mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView);
- mCropOverlayView.setCropWindowChangeListener(new CropOverlayView.CropWindowChangeListener() {
- @Override
- public void onCropWindowChanged(boolean inProgress) {
- handleCropWindowChanged(inProgress, true);
+ OnSetCropOverlayMovedListener movedListener = mOnSetCropOverlayMovedListener;
+ if (movedListener != null && inProgress) {
+ movedListener.onCropOverlayMoved(getCropRect());
}
+ }
});
- mCropOverlayView.setInitialAttributeValues(options);
-
- mProgressBar = (ProgressBar) v.findViewById(R.id.CropProgressBar);
- setProgressBarVisibility();
- }
-
- /**
- * Get the scale type of the image in the crop view.
- */
- public ScaleType getScaleType() {
- return mScaleType;
- }
+ mCropOverlayView.setInitialAttributeValues(options);
+
+ mProgressBar = v.findViewById(R.id.CropProgressBar);
+ setProgressBarVisibility();
+ }
+
+ /** Get the scale type of the image in the crop view. */
+ public ScaleType getScaleType() {
+ return mScaleType;
+ }
+
+ /** Set the scale type of the image in the crop view */
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType != mScaleType) {
+ mScaleType = scaleType;
+ mZoom = 1;
+ mZoomOffsetX = mZoomOffsetY = 0;
+ mCropOverlayView.resetCropOverlayView();
+ requestLayout();
+ }
+ }
+
+ /** The shape of the cropping area - rectangle/circular. */
+ public CropShape getCropShape() {
+ return mCropOverlayView.getCropShape();
+ }
+
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ */
+ public void setCropShape(CropShape cropShape) {
+ mCropOverlayView.setCropShape(cropShape);
+ }
+
+ /** if auto-zoom functionality is enabled. default: true. */
+ public boolean isAutoZoomEnabled() {
+ return mAutoZoomEnabled;
+ }
+
+ /** Set auto-zoom functionality to enabled/disabled. */
+ public void setAutoZoomEnabled(boolean autoZoomEnabled) {
+ if (mAutoZoomEnabled != autoZoomEnabled) {
+ mAutoZoomEnabled = autoZoomEnabled;
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /** Set multi touch functionality to enabled/disabled. */
+ public void setMultiTouchEnabled(boolean multiTouchEnabled) {
+ if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /** The max zoom allowed during cropping. */
+ public int getMaxZoom() {
+ return mMaxZoom;
+ }
+
+ /** The max zoom allowed during cropping. */
+ public void setMaxZoom(int maxZoom) {
+ if (mMaxZoom != maxZoom && maxZoom > 0) {
+ mMaxZoom = maxZoom;
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
+ }
+
+ /**
+ * Get the amount of degrees the cropping image is rotated cloackwise.
+ *
+ * @return 0-360
+ */
+ public int getRotatedDegrees() {
+ return mDegreesRotated;
+ }
+
+ /**
+ * Set the amount of degrees the cropping image is rotated cloackwise.
+ *
+ * @param degrees 0-360
+ */
+ public void setRotatedDegrees(int degrees) {
+ if (mDegreesRotated != degrees) {
+ rotateImage(degrees - mDegreesRotated);
+ }
+ }
+
+ /**
+ * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
+ * be changed.
+ */
+ public boolean isFixAspectRatio() {
+ return mCropOverlayView.isFixAspectRatio();
+ }
+
+ /**
+ * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
+ * it to be changed.
+ */
+ public void setFixedAspectRatio(boolean fixAspectRatio) {
+ mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
+ }
+
+ /** whether the image should be flipped horizontally */
+ public boolean isFlippedHorizontally() {
+ return mFlipHorizontally;
+ }
+
+ /** Sets whether the image should be flipped horizontally */
+ public void setFlippedHorizontally(boolean flipHorizontally) {
+ if (mFlipHorizontally != flipHorizontally) {
+ mFlipHorizontally = flipHorizontally;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+ }
+
+ /** whether the image should be flipped vertically */
+ public boolean isFlippedVertically() {
+ return mFlipVertically;
+ }
+
+ /** Sets whether the image should be flipped vertically */
+ public void setFlippedVertically(boolean flipVertically) {
+ if (mFlipVertically != flipVertically) {
+ mFlipVertically = flipVertically;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+ }
+
+ /** Get the current guidelines option set. */
+ public Guidelines getGuidelines() {
+ return mCropOverlayView.getGuidelines();
+ }
+
+ /**
+ * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
+ * application.
+ */
+ public void setGuidelines(Guidelines guidelines) {
+ mCropOverlayView.setGuidelines(guidelines);
+ }
+
+ /** both the X and Y values of the aspectRatio. */
+ public Pair getAspectRatio() {
+ return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
+ }
+
+ /**
+ * Sets the both the X and Y values of the aspectRatio.
+ * Sets fixed aspect ratio to TRUE.
+ *
+ * @param aspectRatioX int that specifies the new X value of the aspect ratio
+ * @param aspectRatioY int that specifies the new Y value of the aspect ratio
+ */
+ public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
+ mCropOverlayView.setAspectRatioX(aspectRatioX);
+ mCropOverlayView.setAspectRatioY(aspectRatioY);
+ setFixedAspectRatio(true);
+ }
+
+ /** Clears set aspect ratio values and sets fixed aspect ratio to FALSE. */
+ public void clearAspectRatio() {
+ mCropOverlayView.setAspectRatioX(1);
+ mCropOverlayView.setAspectRatioY(1);
+ setFixedAspectRatio(false);
+ }
+
+ /**
+ * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
+ * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
+ * box edge. (default: 3dp)
+ */
+ public void setSnapRadius(float snapRadius) {
+ if (snapRadius >= 0) {
+ mCropOverlayView.setSnapRadius(snapRadius);
+ }
+ }
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public boolean isShowProgressBar() {
+ return mShowProgressBar;
+ }
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public void setShowProgressBar(boolean showProgressBar) {
+ if (mShowProgressBar != showProgressBar) {
+ mShowProgressBar = showProgressBar;
+ setProgressBarVisibility();
+ }
+ }
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public boolean isShowCropOverlay() {
+ return mShowCropOverlay;
+ }
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public void setShowCropOverlay(boolean showCropOverlay) {
+ if (mShowCropOverlay != showCropOverlay) {
+ mShowCropOverlay = showCropOverlay;
+ setCropOverlayVisibility();
+ }
+ }
+
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ public boolean isSaveBitmapToInstanceState() {
+ return mSaveBitmapToInstanceState;
+ }
+
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ public void setSaveBitmapToInstanceState(boolean saveBitmapToInstanceState) {
+ mSaveBitmapToInstanceState = saveBitmapToInstanceState;
+ }
+
+ /** Returns the integer of the imageResource */
+ public int getImageResource() {
+ return mImageResource;
+ }
+
+ /** Get the URI of an image that was set by URI, null otherwise. */
+ public Uri getImageUri() {
+ return mLoadedImageUri;
+ }
+
+ /**
+ * Gets the source Bitmap's dimensions. This represents the largest possible crop rectangle.
+ *
+ * @return a Rect instance dimensions of the source Bitmap
+ */
+ public Rect getWholeImageRect() {
+ int loadedSampleSize = mLoadedSampleSize;
+ Bitmap bitmap = mBitmap;
+ if (bitmap == null) {
+ return null;
+ }
+
+ int orgWidth = bitmap.getWidth() * loadedSampleSize;
+ int orgHeight = bitmap.getHeight() * loadedSampleSize;
+ return new Rect(0, 0, orgWidth, orgHeight);
+ }
+
+ /**
+ * Gets the crop window's position relative to the source Bitmap (not the image displayed in the
+ * CropImageView) using the original image rotation.
+ *
+ * @return a Rect instance containing cropped area boundaries of the source Bitmap
+ */
+ public Rect getCropRect() {
+ int loadedSampleSize = mLoadedSampleSize;
+ Bitmap bitmap = mBitmap;
+ if (bitmap == null) {
+ return null;
+ }
+
+ // get the points of the crop rectangle adjusted to source bitmap
+ float[] points = getCropPoints();
+
+ int orgWidth = bitmap.getWidth() * loadedSampleSize;
+ int orgHeight = bitmap.getHeight() * loadedSampleSize;
+
+ // get the rectangle for the points (it may be larger than original if rotation is not stright)
+ return BitmapUtils.getRectFromPoints(
+ points,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY());
+ }
+
+ /**
+ * Gets the crop window's position relative to the parent's view at screen.
+ *
+ * @return a Rect instance containing cropped area boundaries of the source Bitmap
+ */
+ public RectF getCropWindowRect() {
+ if (mCropOverlayView == null) {
+ return null;
+ }
+ return mCropOverlayView.getCropWindowRect();
+ }
+
+ /**
+ * Gets the 4 points of crop window's position relative to the source Bitmap (not the image
+ * displayed in the CropImageView) using the original image rotation.
+ * Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!=
+ * 90/180/270).
+ *
+ * @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
+ */
+ public float[] getCropPoints() {
+
+ // Get crop window position relative to the displayed image.
+ RectF cropWindowRect = mCropOverlayView.getCropWindowRect();
+
+ float[] points =
+ new float[] {
+ cropWindowRect.left,
+ cropWindowRect.top,
+ cropWindowRect.right,
+ cropWindowRect.top,
+ cropWindowRect.right,
+ cropWindowRect.bottom,
+ cropWindowRect.left,
+ cropWindowRect.bottom
+ };
- /**
- * Set the scale type of the image in the crop view
- */
- public void setScaleType(ScaleType scaleType) {
- if (scaleType != mScaleType) {
- mScaleType = scaleType;
- mZoom = 1;
- mZoomOffsetX = mZoomOffsetY = 0;
- mCropOverlayView.resetCropOverlayView();
- requestLayout();
+ mImageMatrix.invert(mImageInverseMatrix);
+ mImageInverseMatrix.mapPoints(points);
+
+ for (int i = 0; i < points.length; i++) {
+ points[i] *= mLoadedSampleSize;
+ }
+
+ return points;
+ }
+
+ /**
+ * Set the crop window position and size to the given rectangle.
+ * Image to crop must be first set before invoking this, for async - after complete callback.
+ *
+ * @param rect window rectangle (position and size) relative to source bitmap
+ */
+ public void setCropRect(Rect rect) {
+ mCropOverlayView.setInitialCropWindowRect(rect);
+ }
+
+ /** Reset crop window to initial rectangle. */
+ public void resetCropRect() {
+ mZoom = 1;
+ mZoomOffsetX = 0;
+ mZoomOffsetY = 0;
+ mDegreesRotated = mInitialDegreesRotated;
+ mFlipHorizontally = false;
+ mFlipVertically = false;
+ applyImageMatrix(getWidth(), getHeight(), false, false);
+ mCropOverlayView.resetCropWindowRect();
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ *
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage() {
+ return getCroppedImage(0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ *
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
+ return getCroppedImage(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
+ Bitmap croppedBitmap = null;
+ if (mBitmap != null) {
+ mImageView.clearAnimation();
+
+ reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
+ reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
+
+ if (mLoadedImageUri != null
+ && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
+ int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
+ int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
+ BitmapUtils.BitmapSampled bitmapSampled =
+ BitmapUtils.cropBitmap(
+ getContext(),
+ mLoadedImageUri,
+ getCropPoints(),
+ mDegreesRotated,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically);
+ croppedBitmap = bitmapSampled.bitmap;
+ } else {
+ croppedBitmap =
+ BitmapUtils.cropBitmapObjectHandleOOM(
+ mBitmap,
+ getCropPoints(),
+ mDegreesRotated,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ mFlipHorizontally,
+ mFlipVertically)
+ .bitmap;
+ }
+
+ croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
+ }
+
+ return croppedBitmap;
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ */
+ public void getCroppedImageAsync() {
+ getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ */
+ public void getCroppedImageAsync(int reqWidth, int reqHeight) {
+ getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ */
+ public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
+ if (mOnCropImageCompleteListener == null) {
+ throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
+ }
+ startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * Uses JPEG image compression with 90 compression quality.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ */
+ public void saveCroppedImageAsync(Uri saveUri) {
+ saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
+ saveCroppedImageAsync(
+ saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality,
+ int reqWidth,
+ int reqHeight) {
+ saveCroppedImageAsync(
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality,
+ reqWidth,
+ reqHeight,
+ RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality,
+ int reqWidth,
+ int reqHeight,
+ RequestSizeOptions options) {
+ if (mOnCropImageCompleteListener == null) {
+ throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
+ }
+ startCropWorkerTask(
+ reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
+ }
+
+ /** Set the callback t */
+ public void setOnSetCropOverlayReleasedListener(OnSetCropOverlayReleasedListener listener) {
+ mOnCropOverlayReleasedListener = listener;
+ }
+
+ /** Set the callback when the cropping is moved */
+ public void setOnSetCropOverlayMovedListener(OnSetCropOverlayMovedListener listener) {
+ mOnSetCropOverlayMovedListener = listener;
+ }
+
+ /** Set the callback when the crop window is changed */
+ public void setOnCropWindowChangedListener(OnSetCropWindowChangeListener listener) {
+ mOnSetCropWindowChangeListener = listener;
+ }
+
+ /**
+ * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) is
+ * complete (successful or failed).
+ */
+ public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
+ mOnSetImageUriCompleteListener = listener;
+ }
+
+ /**
+ * Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()}
+ * or {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
+ */
+ public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
+ mOnCropImageCompleteListener = listener;
+ }
+
+ /**
+ * Sets a Bitmap as the content of the CropImageView.
+ *
+ * @param bitmap the Bitmap to set
+ */
+ public void setImageBitmap(Bitmap bitmap) {
+ mCropOverlayView.setInitialCropWindowRect(null);
+ setBitmap(bitmap, 0, null, 1, 0);
+ }
+
+ /**
+ * Sets a Bitmap and initializes the image rotation according to the EXIT data.
+ *
+ * The EXIF can be retrieved by doing the following:
+ * ExifInterface exif = new ExifInterface(path);
+ *
+ * @param bitmap the original bitmap to set; if null, this
+ * @param exif the EXIF information about this bitmap; may be null
+ */
+ public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
+ Bitmap setBitmap;
+ int degreesRotated = 0;
+ if (bitmap != null && exif != null) {
+ BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
+ setBitmap = result.bitmap;
+ degreesRotated = result.degrees;
+ mInitialDegreesRotated = result.degrees;
+ } else {
+ setBitmap = bitmap;
+ }
+ mCropOverlayView.setInitialCropWindowRect(null);
+ setBitmap(setBitmap, 0, null, 1, degreesRotated);
+ }
+
+ /**
+ * Sets a Drawable as the content of the CropImageView.
+ *
+ * @param resId the drawable resource ID to set
+ */
+ public void setImageResource(int resId) {
+ if (resId != 0) {
+ mCropOverlayView.setInitialCropWindowRect(null);
+ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
+ setBitmap(bitmap, resId, null, 1, 0);
+ }
+ }
+
+ /**
+ * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.
+ * Can be used with URI from gallery or camera source.
+ * Will rotate the image by exif data.
+ *
+ * @param uri the URI to load the image from
+ */
+ public void setImageUriAsync(Uri uri) {
+ if (uri != null) {
+ BitmapLoadingWorkerTask currentTask =
+ mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
+ if (currentTask != null) {
+ // cancel previous loading (no check if the same URI because camera URI can be the same for
+ // different images)
+ currentTask.cancel(true);
+ }
+
+ // either no existing task is working or we canceled it, need to load new URI
+ clearImageInt();
+ mRestoreCropWindowRect = null;
+ mRestoreDegreesRotated = 0;
+ mCropOverlayView.setInitialCropWindowRect(null);
+ mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri));
+ mBitmapLoadingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ setProgressBarVisibility();
+ }
+ }
+
+ /** Clear the current image set for cropping. */
+ public void clearImage() {
+ clearImageInt();
+ mCropOverlayView.setInitialCropWindowRect(null);
+ }
+
+ /**
+ * Rotates image by the specified number of degrees clockwise.
+ * Negative values represent counter-clockwise rotations.
+ *
+ * @param degrees Integer specifying the number of degrees to rotate.
+ */
+ public void rotateImage(int degrees) {
+ if (mBitmap != null) {
+ // Force degrees to be a non-zero value between 0 and 360 (inclusive)
+ if (degrees < 0) {
+ degrees = (degrees % 360) + 360;
+ } else {
+ degrees = degrees % 360;
+ }
+
+ boolean flipAxes =
+ !mCropOverlayView.isFixAspectRatio()
+ && ((degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305));
+ BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
+ float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
+ float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
+ if (flipAxes) {
+ boolean isFlippedHorizontally = mFlipHorizontally;
+ mFlipHorizontally = mFlipVertically;
+ mFlipVertically = isFlippedHorizontally;
+ }
+
+ mImageMatrix.invert(mImageInverseMatrix);
+
+ BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
+ BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
+ BitmapUtils.POINTS[2] = 0;
+ BitmapUtils.POINTS[3] = 0;
+ BitmapUtils.POINTS[4] = 1;
+ BitmapUtils.POINTS[5] = 0;
+ mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);
+
+ // This is valid because degrees is not negative.
+ mDegreesRotated = (mDegreesRotated + degrees) % 360;
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ // adjust the zoom so the crop window size remains the same even after image scale change
+ mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
+ mZoom /=
+ Math.sqrt(
+ Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
+ mZoom = Math.max(mZoom, 1);
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
+
+ // adjust the width/height by the changes in scaling to the image
+ double change =
+ Math.sqrt(
+ Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
+ halfWidth *= change;
+ halfHeight *= change;
+
+ // calculate the new crop window rectangle to center in the same location and have proper
+ // width/height
+ BitmapUtils.RECT.set(
+ BitmapUtils.POINTS2[0] - halfWidth,
+ BitmapUtils.POINTS2[1] - halfHeight,
+ BitmapUtils.POINTS2[0] + halfWidth,
+ BitmapUtils.POINTS2[1] + halfHeight);
+
+ mCropOverlayView.resetCropOverlayView();
+ mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ handleCropWindowChanged(false, false);
+
+ // make sure the crop window rectangle is within the cropping image bounds after all the
+ // changes
+ mCropOverlayView.fixCurrentCropWindowRect();
+ }
+ }
+
+ /** Flips the image horizontally. */
+ public void flipImageHorizontally() {
+ mFlipHorizontally = !mFlipHorizontally;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+
+ /** Flips the image vertically. */
+ public void flipImageVertically() {
+ mFlipVertically = !mFlipVertically;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+
+ // region: Private methods
+
+ /**
+ * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result to the
+ * widget if still relevant and call listener if set.
+ *
+ * @param result the result of bitmap loading
+ */
+ void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {
+
+ mBitmapLoadingWorkerTask = null;
+ setProgressBarVisibility();
+
+ if (result.error == null) {
+ mInitialDegreesRotated = result.degreesRotated;
+ setBitmap(result.bitmap, 0, result.uri, result.loadSampleSize, result.degreesRotated);
+ }
+
+ OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
+ if (listener != null) {
+ listener.onSetImageUriComplete(this, result.uri, result.error);
+ }
+ }
+
+ /**
+ * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if
+ * set.
+ *
+ * @param result the result of bitmap cropping
+ */
+ void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {
+
+ mBitmapCroppingWorkerTask = null;
+ setProgressBarVisibility();
+
+ OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
+ if (listener != null) {
+ CropResult cropResult =
+ new CropResult(
+ mBitmap,
+ mLoadedImageUri,
+ result.bitmap,
+ result.uri,
+ result.error,
+ getCropPoints(),
+ getCropRect(),
+ getWholeImageRect(),
+ getRotatedDegrees(),
+ result.sampleSize);
+ listener.onCropImageComplete(this, cropResult);
+ }
+ }
+
+ /**
+ * Set the given bitmap to be used in for cropping
+ * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
+ * manipulated.
+ */
+ private void setBitmap(
+ Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
+ if (mBitmap == null || !mBitmap.equals(bitmap)) {
+
+ mImageView.clearAnimation();
+
+ clearImageInt();
+
+ mBitmap = bitmap;
+ mImageView.setImageBitmap(mBitmap);
+
+ mLoadedImageUri = imageUri;
+ mImageResource = imageResource;
+ mLoadedSampleSize = loadSampleSize;
+ mDegreesRotated = degreesRotated;
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ if (mCropOverlayView != null) {
+ mCropOverlayView.resetCropOverlayView();
+ setCropOverlayVisibility();
+ }
+ }
+ }
+
+ /**
+ * Clear the current image set for cropping.
+ * Full clear will also clear the data of the set image like Uri or Resource id while partial
+ * clear will only clear the bitmap and recycle if required.
+ */
+ private void clearImageInt() {
+
+ // if we allocated the bitmap, release it as fast as possible
+ if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
+ mBitmap.recycle();
+ }
+ mBitmap = null;
+
+ // clean the loaded image flags for new image
+ mImageResource = 0;
+ mLoadedImageUri = null;
+ mLoadedSampleSize = 1;
+ mDegreesRotated = 0;
+ mZoom = 1;
+ mZoomOffsetX = 0;
+ mZoomOffsetY = 0;
+ mImageMatrix.reset();
+ mSaveInstanceStateBitmapUri = null;
+
+ mImageView.setImageBitmap(null);
+
+ setCropOverlayVisibility();
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample
+ * size to fit in the requested width and height down-sampling if possible - optimization to get
+ * best size to quality.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use on the cropped bitmap
+ * @param saveUri optional: to save the cropped image to
+ * @param saveCompressFormat if saveUri is given, the given compression will be used for saving
+ * the image
+ * @param saveCompressQuality if saveUri is given, the given quality will be used for the
+ * compression.
+ */
+ public void startCropWorkerTask(
+ int reqWidth,
+ int reqHeight,
+ RequestSizeOptions options,
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality) {
+ Bitmap bitmap = mBitmap;
+ if (bitmap != null) {
+ mImageView.clearAnimation();
+
+ BitmapCroppingWorkerTask currentTask =
+ mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
+ if (currentTask != null) {
+ // cancel previous cropping
+ currentTask.cancel(true);
+ }
+
+ reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
+ reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
+
+ int orgWidth = bitmap.getWidth() * mLoadedSampleSize;
+ int orgHeight = bitmap.getHeight() * mLoadedSampleSize;
+ if (mLoadedImageUri != null
+ && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
+ mBitmapCroppingWorkerTask =
+ new WeakReference<>(
+ new BitmapCroppingWorkerTask(
+ this,
+ mLoadedImageUri,
+ getCropPoints(),
+ mDegreesRotated,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically,
+ options,
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality));
+ } else {
+ mBitmapCroppingWorkerTask =
+ new WeakReference<>(
+ new BitmapCroppingWorkerTask(
+ this,
+ bitmap,
+ getCropPoints(),
+ mDegreesRotated,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically,
+ options,
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality));
+ }
+ mBitmapCroppingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ setProgressBarVisibility();
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mLoadedImageUri == null && mBitmap == null && mImageResource < 1) {
+ return super.onSaveInstanceState();
+ }
+
+ Bundle bundle = new Bundle();
+ Uri imageUri = mLoadedImageUri;
+ if (mSaveBitmapToInstanceState && imageUri == null && mImageResource < 1) {
+ mSaveInstanceStateBitmapUri =
+ imageUri =
+ BitmapUtils.writeTempStateStoreBitmap(
+ getContext(), mBitmap, mSaveInstanceStateBitmapUri);
+ }
+ if (imageUri != null && mBitmap != null) {
+ String key = UUID.randomUUID().toString();
+ BitmapUtils.mStateBitmap = new Pair<>(key, new WeakReference<>(mBitmap));
+ bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
+ }
+ if (mBitmapLoadingWorkerTask != null) {
+ BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
+ if (task != null) {
+ bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
+ }
+ }
+ bundle.putParcelable("instanceState", super.onSaveInstanceState());
+ bundle.putParcelable("LOADED_IMAGE_URI", imageUri);
+ bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
+ bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
+ bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
+ bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());
+
+ BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
+
+ mImageMatrix.invert(mImageInverseMatrix);
+ mImageInverseMatrix.mapRect(BitmapUtils.RECT);
+
+ bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
+ bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
+ bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
+ bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
+ bundle.putBoolean("CROP_FLIP_HORIZONTALLY", mFlipHorizontally);
+ bundle.putBoolean("CROP_FLIP_VERTICALLY", mFlipVertically);
+
+ return bundle;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+
+ if (state instanceof Bundle) {
+ Bundle bundle = (Bundle) state;
+
+ // prevent restoring state if already set by outside code
+ if (mBitmapLoadingWorkerTask == null
+ && mLoadedImageUri == null
+ && mBitmap == null
+ && mImageResource == 0) {
+
+ Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
+ if (uri != null) {
+ String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
+ if (key != null) {
+ Bitmap stateBitmap =
+ BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
+ ? BitmapUtils.mStateBitmap.second.get()
+ : null;
+ BitmapUtils.mStateBitmap = null;
+ if (stateBitmap != null && !stateBitmap.isRecycled()) {
+ setBitmap(stateBitmap, 0, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
+ }
+ }
+ if (mLoadedImageUri == null) {
+ setImageUriAsync(uri);
+ }
+ } else {
+ int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
+ if (resId > 0) {
+ setImageResource(resId);
+ } else {
+ uri = bundle.getParcelable("LOADING_IMAGE_URI");
+ if (uri != null) {
+ setImageUriAsync(uri);
+ }
+ }
}
- }
- /**
- * The shape of the cropping area - rectangle/circular.
- */
- public CropShape getCropShape() {
- return mCropOverlayView.getCropShape();
- }
+ mDegreesRotated = mRestoreDegreesRotated = bundle.getInt("DEGREES_ROTATED");
- /**
- * The shape of the cropping area - rectangle/circular.
- * To set square/circle crop shape set aspect ratio to 1:1.
- */
- public void setCropShape(CropShape cropShape) {
- mCropOverlayView.setCropShape(cropShape);
- }
-
- /**
- * if auto-zoom functionality is enabled. default: true.
- */
- public boolean isAutoZoomEnabled() {
- return mAutoZoomEnabled;
- }
-
- /**
- * Set auto-zoom functionality to enabled/disabled.
- */
- public void setAutoZoomEnabled(boolean autoZoomEnabled) {
- if (mAutoZoomEnabled != autoZoomEnabled) {
- mAutoZoomEnabled = autoZoomEnabled;
- handleCropWindowChanged(false, false);
- mCropOverlayView.invalidate();
+ Rect initialCropRect = bundle.getParcelable("INITIAL_CROP_RECT");
+ if (initialCropRect != null
+ && (initialCropRect.width() > 0 || initialCropRect.height() > 0)) {
+ mCropOverlayView.setInitialCropWindowRect(initialCropRect);
}
- }
- /**
- * Set multi touch functionality to enabled/disabled.
- */
- public void setMultiTouchEnabled(boolean multiTouchEnabled) {
- if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
- handleCropWindowChanged(false, false);
- mCropOverlayView.invalidate();
+ RectF cropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
+ if (cropWindowRect != null && (cropWindowRect.width() > 0 || cropWindowRect.height() > 0)) {
+ mRestoreCropWindowRect = cropWindowRect;
}
- }
- /**
- * The max zoom allowed during cropping.
- */
- public int getMaxZoom() {
- return mMaxZoom;
- }
+ mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));
- /**
- * The max zoom allowed during cropping.
- */
- public void setMaxZoom(int maxZoom) {
- if (mMaxZoom != maxZoom && maxZoom > 0) {
- mMaxZoom = maxZoom;
- handleCropWindowChanged(false, false);
- mCropOverlayView.invalidate();
- }
- }
-
- /**
- * the min size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
- mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
-
- }
-
- /**
- * the max size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
- mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
- }
+ mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
+ mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");
- /**
- * Get the amount of degrees the cropping image is rotated cloackwise.
- *
- * @return 0-360
- */
- public int getRotatedDegrees() {
- return mDegreesRotated;
- }
+ mFlipHorizontally = bundle.getBoolean("CROP_FLIP_HORIZONTALLY");
+ mFlipVertically = bundle.getBoolean("CROP_FLIP_VERTICALLY");
+ }
- /**
- * Set the amount of degrees the cropping image is rotated cloackwise.
- *
- * @param degrees 0-360
- */
- public void setRotatedDegrees(int degrees) {
- if (mDegreesRotated != degrees) {
- rotateImage(degrees - mDegreesRotated);
- }
+ super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
+ } else {
+ super.onRestoreInstanceState(state);
}
+ }
- /**
- * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to be changed.
- */
- public boolean isFixAspectRatio() {
- return mCropOverlayView.isFixAspectRatio();
- }
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- /**
- * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to be changed.
- */
- public void setFixedAspectRatio(boolean fixAspectRatio) {
- mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
- }
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- /**
- * Get the current guidelines option set.
- */
- public Guidelines getGuidelines() {
- return mCropOverlayView.getGuidelines();
- }
+ if (mBitmap != null) {
- /**
- * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the application.
- */
- public void setGuidelines(Guidelines guidelines) {
- mCropOverlayView.setGuidelines(guidelines);
- }
+ // Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
+ if (heightSize == 0) {
+ heightSize = mBitmap.getHeight();
+ }
- /**
- * both the X and Y values of the aspectRatio.
- */
- public Pair getAspectRatio() {
- return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
- }
+ int desiredWidth;
+ int desiredHeight;
- /**
- * Sets the both the X and Y values of the aspectRatio.
- * Sets fixed aspect ratio to TRUE.
- *
- * @param aspectRatioX int that specifies the new X value of the aspect ratio
- * @param aspectRatioY int that specifies the new Y value of the aspect ratio
- */
- public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
- mCropOverlayView.setAspectRatioX(aspectRatioX);
- mCropOverlayView.setAspectRatioY(aspectRatioY);
- setFixedAspectRatio(true);
- }
+ double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
+ double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;
- /**
- * Clears set aspect ratio values and sets fixed aspect ratio to FALSE.
- */
- public void clearAspectRatio() {
- mCropOverlayView.setAspectRatioX(1);
- mCropOverlayView.setAspectRatioY(1);
- setFixedAspectRatio(false);
- }
+ // Checks if either width or height needs to be fixed
+ if (widthSize < mBitmap.getWidth()) {
+ viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
+ }
+ if (heightSize < mBitmap.getHeight()) {
+ viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
+ }
- /**
- * An edge of the crop window will snap to the corresponding edge of a
- * specified bounding box when the crop window edge is less than or equal to
- * this distance (in pixels) away from the bounding box edge. (default: 3dp)
- */
- public void setSnapRadius(float snapRadius) {
- if (snapRadius >= 0) {
- mCropOverlayView.setSnapRadius(snapRadius);
+ // If either needs to be fixed, choose smallest ratio and calculate from there
+ if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
+ || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
+ if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
+ desiredWidth = widthSize;
+ desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
+ } else {
+ desiredHeight = heightSize;
+ desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
}
- }
-
- /**
- * if to show progress bar when image async loading/cropping is in progress.
- * default: true, disable to provide custom progress bar UI.
- */
- public boolean isShowProgressBar() {
- return mShowProgressBar;
- }
-
- /**
- * if to show progress bar when image async loading/cropping is in progress.
- * default: true, disable to provide custom progress bar UI.
- */
- public void setShowProgressBar(boolean showProgressBar) {
- if (mShowProgressBar != showProgressBar) {
- mShowProgressBar = showProgressBar;
- setProgressBarVisibility();
+ } else {
+ // Otherwise, the picture is within frame layout bounds. Desired width is simply picture
+ // size
+ desiredWidth = mBitmap.getWidth();
+ desiredHeight = mBitmap.getHeight();
+ }
+
+ int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
+ int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);
+
+ mLayoutWidth = width;
+ mLayoutHeight = height;
+
+ setMeasuredDimension(mLayoutWidth, mLayoutHeight);
+
+ } else {
+ setMeasuredDimension(widthSize, heightSize);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+
+ super.onLayout(changed, l, t, r, b);
+
+ if (mLayoutWidth > 0 && mLayoutHeight > 0) {
+ // Gets original parameters, and creates the new parameters
+ ViewGroup.LayoutParams origParams = this.getLayoutParams();
+ origParams.width = mLayoutWidth;
+ origParams.height = mLayoutHeight;
+ setLayoutParams(origParams);
+
+ if (mBitmap != null) {
+ applyImageMatrix(r - l, b - t, true, false);
+
+ // after state restore we want to restore the window crop, possible only after widget size
+ // is known
+ if (mRestoreCropWindowRect != null) {
+ if (mRestoreDegreesRotated != mInitialDegreesRotated) {
+ mDegreesRotated = mRestoreDegreesRotated;
+ applyImageMatrix(r - l, b - t, true, false);
+ }
+ mImageMatrix.mapRect(mRestoreCropWindowRect);
+ mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.fixCurrentCropWindowRect();
+ mRestoreCropWindowRect = null;
+ } else if (mSizeChanged) {
+ mSizeChanged = false;
+ handleCropWindowChanged(false, false);
}
- }
-
- /**
- * if to show crop overlay UI what contains the crop window UI surrounded by background over the cropping
- * image.
- * default: true, may disable for animation or frame transition.
- */
- public boolean isShowCropOverlay() {
- return mShowCropOverlay;
- }
-
- /**
- * if to show crop overlay UI what contains the crop window UI surrounded by background over the cropping
- * image.
- * default: true, may disable for animation or frame transition.
- */
- public void setShowCropOverlay(boolean showCropOverlay) {
- if (mShowCropOverlay != showCropOverlay) {
- mShowCropOverlay = showCropOverlay;
- setCropOverlayVisibility();
+ } else {
+ updateImageBounds(true);
+ }
+ } else {
+ updateImageBounds(true);
+ }
+ }
+
+ /**
+ * Detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean, boolean)}
+ * in {@link #layout(int, int, int, int)}.
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mSizeChanged = oldw > 0 && oldh > 0;
+ }
+
+ /**
+ * Handle crop window change to:
+ * 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
+ * available view area.
+ * 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
+ *
+ *
+ * @param inProgress is the crop window change is still in progress by the user
+ * @param animate if to animate the change to the image matrix, or set it directly
+ */
+ private void handleCropWindowChanged(boolean inProgress, boolean animate) {
+ int width = getWidth();
+ int height = getHeight();
+ if (mBitmap != null && width > 0 && height > 0) {
+
+ RectF cropRect = mCropOverlayView.getCropWindowRect();
+ if (inProgress) {
+ if (cropRect.left < 0
+ || cropRect.top < 0
+ || cropRect.right > width
+ || cropRect.bottom > height) {
+ applyImageMatrix(width, height, false, false);
}
- }
-
- /**
- * Returns the integer of the imageResource
- */
- public int getImageResource() {
- return mImageResource;
- }
-
- /**
- * Get the URI of an image that was set by URI, null otherwise.
- */
- public Uri getImageUri() {
- return mLoadedImageUri;
- }
-
- /**
- * Gets the crop window's position relative to the source Bitmap (not the image
- * displayed in the CropImageView) using the original image rotation.
- *
- * @return a Rect instance containing cropped area boundaries of the source Bitmap
- */
- public Rect getCropRect() {
- if (mBitmap != null) {
-
- // get the points of the crop rectangle adjusted to source bitmap
- float[] points = getCropPoints();
-
- int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
- int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
-
- // get the rectangle for the points (it may be larger than original if rotation is not stright)
- return BitmapUtils.getRectFromPoints(points, orgWidth, orgHeight,
- mCropOverlayView.isFixAspectRatio(), mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
- } else {
- return null;
+ } else if (mAutoZoomEnabled || mZoom > 1) {
+ float newZoom = 0;
+ // keep the cropping window covered area to 50%-65% of zoomed sub-area
+ if (mZoom < mMaxZoom
+ && cropRect.width() < width * 0.5f
+ && cropRect.height() < height * 0.5f) {
+ newZoom =
+ Math.min(
+ mMaxZoom,
+ Math.min(
+ width / (cropRect.width() / mZoom / 0.64f),
+ height / (cropRect.height() / mZoom / 0.64f)));
}
- }
-
- /**
- * Gets the 4 points of crop window's position relative to the source Bitmap (not the image
- * displayed in the CropImageView) using the original image rotation.
- * Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!= 90/180/270).
- *
- * @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
- */
- public float[] getCropPoints() {
-
- // Get crop window position relative to the displayed image.
- RectF cropWindowRect = mCropOverlayView.getCropWindowRect();
-
- float[] points = new float[]{
- cropWindowRect.left,
- cropWindowRect.top,
- cropWindowRect.right,
- cropWindowRect.top,
- cropWindowRect.right,
- cropWindowRect.bottom,
- cropWindowRect.left,
- cropWindowRect.bottom
- };
-
- mImageMatrix.invert(mImageInverseMatrix);
- mImageInverseMatrix.mapPoints(points);
-
- for (int i = 0; i < points.length; i++) {
- points[i] *= mLoadedSampleSize;
+ if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
+ newZoom =
+ Math.max(
+ 1,
+ Math.min(
+ width / (cropRect.width() / mZoom / 0.51f),
+ height / (cropRect.height() / mZoom / 0.51f)));
+ }
+ if (!mAutoZoomEnabled) {
+ newZoom = 1;
}
- return points;
- }
-
- /**
- * Set the crop window position and size to the given rectangle.
- * Image to crop must be first set before invoking this, for async - after complete callback.
- *
- * @param rect window rectangle (position and size) relative to source bitmap
- */
- public void setCropRect(Rect rect) {
- mCropOverlayView.setInitialCropWindowRect(rect);
- }
-
- /**
- * Reset crop window to initial rectangle.
- */
- public void resetCropRect() {
- mZoom = 1;
- mZoomOffsetX = 0;
- mZoomOffsetY = 0;
- mDegreesRotated = 0;
- applyImageMatrix(getWidth(), getHeight(), false, false);
- mCropOverlayView.resetCropWindowRect();
- }
-
- /**
- * Gets the cropped image based on the current crop window.
- *
- * @return a new Bitmap representing the cropped image
- */
- public Bitmap getCroppedImage() {
- return getCroppedImage(0, 0, RequestSizeOptions.NONE);
- }
-
- /**
- * Gets the cropped image based on the current crop window.
- * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
- *
- * @param reqWidth the width to resize the cropped image to
- * @param reqHeight the height to resize the cropped image to
- * @return a new Bitmap representing the cropped image
- */
- public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
- return getCroppedImage(0, 0, RequestSizeOptions.RESIZE_INSIDE);
- }
-
- /**
- * Gets the cropped image based on the current crop window.
- *
- * @param reqWidth the width to resize the cropped image to (see options)
- * @param reqHeight the height to resize the cropped image to (see options)
- * @param options the resize method to use, see its documentation
- * @return a new Bitmap representing the cropped image
- */
- public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
- Bitmap croppedBitmap = null;
- if (mBitmap != null) {
- mImageView.clearAnimation();
-
- reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
- reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
-
- if (mLoadedImageUri != null && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
- int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
- int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
- BitmapUtils.BitmapSampled bitmapSampled =
- BitmapUtils.cropBitmap(getContext(), mLoadedImageUri, getCropPoints(),
- mDegreesRotated, orgWidth, orgHeight,
- mCropOverlayView.isFixAspectRatio(), mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY(),
- reqWidth, reqHeight);
- croppedBitmap = bitmapSampled.bitmap;
- } else {
- croppedBitmap = BitmapUtils.cropBitmap(mBitmap, getCropPoints(), mDegreesRotated,
- mCropOverlayView.isFixAspectRatio(), mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
+ if (newZoom > 0 && newZoom != mZoom) {
+ if (animate) {
+ if (mAnimation == null) {
+ // lazy create animation single instance
+ mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
}
+ // set the state for animation to start from
+ mAnimation.setStartState(mImagePoints, mImageMatrix);
+ }
- croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
- }
+ mZoom = newZoom;
- return croppedBitmap;
- }
+ applyImageMatrix(width, height, true, animate);
+ }
+ }
+ if (mOnSetCropWindowChangeListener != null && !inProgress) {
+ mOnSetCropWindowChangeListener.onCropWindowChanged();
+ }
+ }
+ }
+
+ /**
+ * Apply matrix to handle the image inside the image view.
+ *
+ * @param width the width of the image view
+ * @param height the height of the image view
+ */
+ private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
+ if (mBitmap != null && width > 0 && height > 0) {
+
+ mImageMatrix.invert(mImageInverseMatrix);
+ RectF cropRect = mCropOverlayView.getCropWindowRect();
+ mImageInverseMatrix.mapRect(cropRect);
+
+ mImageMatrix.reset();
+
+ // move the image to the center of the image view first so we can manipulate it from there
+ mImageMatrix.postTranslate(
+ (width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
+ mapImagePointsByImageMatrix();
+
+ // rotate the image the required degrees from center of image
+ if (mDegreesRotated > 0) {
+ mImageMatrix.postRotate(
+ mDegreesRotated,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+ }
+
+ // scale the image to the image view, image rect transformed to know new width/height
+ float scale =
+ Math.min(
+ width / BitmapUtils.getRectWidth(mImagePoints),
+ height / BitmapUtils.getRectHeight(mImagePoints));
+ if (mScaleType == ScaleType.FIT_CENTER
+ || (mScaleType == ScaleType.CENTER_INSIDE && scale < 1)
+ || (scale > 1 && mAutoZoomEnabled)) {
+ mImageMatrix.postScale(
+ scale,
+ scale,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+ }
+
+ // scale by the current zoom level
+ float scaleX = mFlipHorizontally ? -mZoom : mZoom;
+ float scaleY = mFlipVertically ? -mZoom : mZoom;
+ mImageMatrix.postScale(
+ scaleX,
+ scaleY,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+
+ mImageMatrix.mapRect(cropRect);
+
+ if (center) {
+ // set the zoomed area to be as to the center of cropping window as possible
+ mZoomOffsetX =
+ width > BitmapUtils.getRectWidth(mImagePoints)
+ ? 0
+ : Math.max(
+ Math.min(
+ width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)),
+ getWidth() - BitmapUtils.getRectRight(mImagePoints))
+ / scaleX;
+ mZoomOffsetY =
+ height > BitmapUtils.getRectHeight(mImagePoints)
+ ? 0
+ : Math.max(
+ Math.min(
+ height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)),
+ getHeight() - BitmapUtils.getRectBottom(mImagePoints))
+ / scaleY;
+ } else {
+ // adjust the zoomed area so the crop window rectangle will be inside the area in case it
+ // was moved outside
+ mZoomOffsetX =
+ Math.min(Math.max(mZoomOffsetX * scaleX, -cropRect.left), -cropRect.right + width)
+ / scaleX;
+ mZoomOffsetY =
+ Math.min(Math.max(mZoomOffsetY * scaleY, -cropRect.top), -cropRect.bottom + height)
+ / scaleY;
+ }
+
+ // apply to zoom offset translate and update the crop rectangle to offset correctly
+ mImageMatrix.postTranslate(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
+ cropRect.offset(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
+ mCropOverlayView.setCropWindowRect(cropRect);
+ mapImagePointsByImageMatrix();
+ mCropOverlayView.invalidate();
+
+ // set matrix to apply
+ if (animate) {
+ // set the state for animation to end in, start animation now
+ mAnimation.setEndState(mImagePoints, mImageMatrix);
+ mImageView.startAnimation(mAnimation);
+ } else {
+ mImageView.setImageMatrix(mImageMatrix);
+ }
+
+ // update the image rectangle in the crop overlay
+ updateImageBounds(false);
+ }
+ }
+
+ /**
+ * Adjust the given image rectangle by image transformation matrix to know the final rectangle of
+ * the image.
+ * To get the proper rectangle it must be first reset to original image rectangle.
+ */
+ private void mapImagePointsByImageMatrix() {
+ mImagePoints[0] = 0;
+ mImagePoints[1] = 0;
+ mImagePoints[2] = mBitmap.getWidth();
+ mImagePoints[3] = 0;
+ mImagePoints[4] = mBitmap.getWidth();
+ mImagePoints[5] = mBitmap.getHeight();
+ mImagePoints[6] = 0;
+ mImagePoints[7] = mBitmap.getHeight();
+ mImageMatrix.mapPoints(mImagePoints);
+ mScaleImagePoints[0] = 0;
+ mScaleImagePoints[1] = 0;
+ mScaleImagePoints[2] = 100;
+ mScaleImagePoints[3] = 0;
+ mScaleImagePoints[4] = 100;
+ mScaleImagePoints[5] = 100;
+ mScaleImagePoints[6] = 0;
+ mScaleImagePoints[7] = 100;
+ mImageMatrix.mapPoints(mScaleImagePoints);
+ }
+
+ /**
+ * Determines the specs for the onMeasure function. Calculates the width or height depending on
+ * the mode.
+ *
+ * @param measureSpecMode The mode of the measured width or height.
+ * @param measureSpecSize The size of the measured width or height.
+ * @param desiredSize The desired size of the measured width or height.
+ * @return The final size of the width or height.
+ */
+ private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {
+
+ // Measure Width
+ int spec;
+ if (measureSpecMode == MeasureSpec.EXACTLY) {
+ // Must be this size
+ spec = measureSpecSize;
+ } else if (measureSpecMode == MeasureSpec.AT_MOST) {
+ // Can't be bigger than...; match_parent value
+ spec = Math.min(desiredSize, measureSpecSize);
+ } else {
+ // Be whatever you want; wrap_content
+ spec = desiredSize;
+ }
+
+ return spec;
+ }
+
+ /**
+ * Set visibility of crop overlay to hide it when there is no image or specificly set by client.
+ */
+ private void setCropOverlayVisibility() {
+ if (mCropOverlayView != null) {
+ mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
+ }
+ }
+
+ /**
+ * Set visibility of progress bar when async loading/cropping is in process and show is enabled.
+ */
+ private void setProgressBarVisibility() {
+ boolean visible =
+ mShowProgressBar
+ && (mBitmap == null && mBitmapLoadingWorkerTask != null
+ || mBitmapCroppingWorkerTask != null);
+ mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
+ }
+
+ /** Update the scale factor between the actual image bitmap and the shown image. */
+ private void updateImageBounds(boolean clear) {
+ if (mBitmap != null && !clear) {
+
+ // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for
+ // width/height.
+ float scaleFactorWidth =
+ 100f * mLoadedSampleSize / BitmapUtils.getRectWidth(mScaleImagePoints);
+ float scaleFactorHeight =
+ 100f * mLoadedSampleSize / BitmapUtils.getRectHeight(mScaleImagePoints);
+ mCropOverlayView.setCropWindowLimits(
+ getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
+ }
+
+ // set the bitmap rectangle and update the crop window after scale factor is set
+ mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
+ }
+ // endregion
+
+ // region: Inner class: CropShape
+
+ /**
+ * The possible cropping area shape.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ */
+ public enum CropShape {
+ RECTANGLE,
+ OVAL
+ }
+ // endregion
+
+ // region: Inner class: ScaleType
+
+ /**
+ * Options for scaling the bounds of cropping image to the bounds of Crop Image View.
+ * Note: Some options are affected by auto-zoom, if enabled.
+ */
+ public enum ScaleType {
/**
- * Gets the cropped image based on the current crop window.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
+ * Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.
+ * The largest dimension will be equals to crop image view and the second dimension will be
+ * smaller.
*/
- public void getCroppedImageAsync() {
- getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
- }
+ FIT_CENTER,
/**
- * Gets the cropped image based on the current crop window.
- * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
- * The result will be invoked to listener set by {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
- *
- * @param reqWidth the width to resize the cropped image to
- * @param reqHeight the height to resize the cropped image to
+ * Center the image in the view, but perform no scaling.
+ * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
+ * will be scaled uniformly to fit the crop image view.
*/
- public void getCroppedImageAsync(int reqWidth, int reqHeight) {
- getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
- }
+ CENTER,
/**
- * Gets the cropped image based on the current crop window.
- * The result will be invoked to listener set by {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
- *
- * @param reqWidth the width to resize the cropped image to (see options)
- * @param reqHeight the height to resize the cropped image to (see options)
- * @param options the resize method to use, see its documentation
+ * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or larger than the corresponding dimension
+ * of the view (minus padding).
+ * The image is then centered in the view.
*/
- public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
- if (mOnCropImageCompleteListener == null && mOnGetCroppedImageCompleteListener == null) {
- throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
- }
- startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
- }
+ CENTER_CROP,
/**
- * Save the cropped image based on the current crop window to the given uri.
- * Uses JPEG image compression with 90 compression quality.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
- *
- * @param saveUri the Android Uri to save the cropped image to
+ * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or less than the corresponding dimension of
+ * the view (minus padding).
+ * The image is then centered in the view.
+ * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
+ * will be scaled uniformly to fit the crop image view.
*/
- public void saveCroppedImageAsync(Uri saveUri) {
- saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
- }
+ CENTER_INSIDE
+ }
+ // endregion
- /**
- * Save the cropped image based on the current crop window to the given uri.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
- *
- * @param saveUri the Android Uri to save the cropped image to
- * @param saveCompressFormat the compression format to use when writing the image
- * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
- */
- public void saveCroppedImageAsync(Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
- saveCroppedImageAsync(saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
- }
+ // region: Inner class: Guidelines
- /**
- * Save the cropped image based on the current crop window to the given uri.
- * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
- *
- * @param saveUri the Android Uri to save the cropped image to
- * @param saveCompressFormat the compression format to use when writing the image
- * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
- * @param reqWidth the width to resize the cropped image to
- * @param reqHeight the height to resize the cropped image to
- */
- public void saveCroppedImageAsync(Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality, int reqWidth, int reqHeight) {
- saveCroppedImageAsync(saveUri, saveCompressFormat, saveCompressQuality, reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
- }
+ /** The possible guidelines showing types. */
+ public enum Guidelines {
+ /** Never show */
+ OFF,
- /**
- * Save the cropped image based on the current crop window to the given uri.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
- *
- * @param saveUri the Android Uri to save the cropped image to
- * @param saveCompressFormat the compression format to use when writing the image
- * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
- * @param reqWidth the width to resize the cropped image to (see options)
- * @param reqHeight the height to resize the cropped image to (see options)
- * @param options the resize method to use, see its documentation
- */
- public void saveCroppedImageAsync(Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality, int reqWidth, int reqHeight, RequestSizeOptions options) {
- if (mOnCropImageCompleteListener == null && mOnSaveCroppedImageCompleteListener == null) {
- throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
- }
- startCropWorkerTask(reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
- }
+ /** Show when crop move action is live */
+ ON_TOUCH,
- /**
- * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)})
- * is complete (successful or failed).
- */
- public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
- mOnSetImageUriCompleteListener = listener;
- }
+ /** Always show */
+ ON
+ }
+ // endregion
- /**
- * Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()} or
- * {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
- */
- public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
- mOnCropImageCompleteListener = listener;
- }
+ // region: Inner class: RequestSizeOptions
- /**
- * Set the callback to be invoked when image async get cropping image ({@link #getCroppedImageAsync()})
- * is complete (successful or failed).
- *
- * @deprecated use {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
- */
- @Deprecated
- public void setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener listener) {
- mOnGetCroppedImageCompleteListener = listener;
- }
+ /** Possible options for handling requested width/height for cropping. */
+ public enum RequestSizeOptions {
- /**
- * Set the callback to be invoked when image async save cropping image ({@link #saveCroppedImageAsync(Uri)})
- * is complete (successful or failed).
- *
- * @deprecated use {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
- */
- @Deprecated
- public void setOnSaveCroppedImageCompleteListener(OnSaveCroppedImageCompleteListener listener) {
- mOnSaveCroppedImageCompleteListener = listener;
- }
+ /** No resize/sampling is done unless required for memory management (OOM). */
+ NONE,
/**
- * Sets a Bitmap as the content of the CropImageView.
- *
- * @param bitmap the Bitmap to set
+ * Only sample the image during loading (if image set using URI) so the smallest of the image
+ * dimensions will be between the requested size and x2 requested size.
+ * NOTE: resulting image will not be exactly requested width/height see: Loading
+ * Large Bitmaps Efficiently.
*/
- public void setImageBitmap(Bitmap bitmap) {
- mCropOverlayView.setInitialCropWindowRect(null);
- setBitmap(bitmap);
- }
+ SAMPLING,
/**
- * Sets a Bitmap and initializes the image rotation according to the EXIT data.
- *
- * The EXIF can be retrieved by doing the following:
- * ExifInterface exif = new ExifInterface(path);
- *
- * @param bitmap the original bitmap to set; if null, this
- * @param exif the EXIF information about this bitmap; may be null
+ * Resize the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or less than the corresponding requested
+ * dimension.
+ * If the image is smaller than the requested size it will NOT change.
*/
- public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
- Bitmap setBitmap;
- if (bitmap != null && exif != null) {
- BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
- setBitmap = result.bitmap;
- mDegreesRotated = result.degrees;
- } else {
- setBitmap = bitmap;
- }
- mCropOverlayView.setInitialCropWindowRect(null);
- setBitmap(setBitmap);
- }
+ RESIZE_INSIDE,
/**
- * Sets a Drawable as the content of the CropImageView.
- *
- * @param resId the drawable resource ID to set
+ * Resize the image uniformly (maintain the image's aspect ratio) to fit in the given
+ * width/height.
+ * The largest dimension will be equals to the requested and the second dimension will be
+ * smaller.
+ * If the image is smaller than the requested size it will enlarge it.
*/
- public void setImageResource(int resId) {
- if (resId != 0) {
- mCropOverlayView.setInitialCropWindowRect(null);
- Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
- setBitmap(bitmap, resId);
- }
- }
+ RESIZE_FIT,
/**
- * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.
- * Can be used with URI from gallery or camera source.
- * Will rotate the image by exif data.
- *
- * @param uri the URI to load the image from
+ * Resize the image to fit exactly in the given width/height.
+ * This resize method does NOT preserve aspect ratio.
+ * If the image is smaller than the requested size it will enlarge it.
*/
- public void setImageUriAsync(Uri uri) {
- if (uri != null) {
- BitmapLoadingWorkerTask currentTask = mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
- if (currentTask != null) {
- // cancel previous loading (no check if the same URI because camera URI can be the same for different images)
- currentTask.cancel(true);
- }
+ RESIZE_EXACT
+ }
+ // endregion
- // either no existing task is working or we canceled it, need to load new URI
- clearImageInt();
- mCropOverlayView.setInitialCropWindowRect(null);
- mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri));
- mBitmapLoadingWorkerTask.get().execute();
- setProgressBarVisibility();
- }
- }
+ // region: Inner class: OnSetImageUriCompleteListener
- /**
- * Clear the current image set for cropping.
- */
- public void clearImage() {
- clearImageInt();
- mCropOverlayView.setInitialCropWindowRect(null);
- }
+ /** Interface definition for a callback to be invoked when the crop overlay is released. */
+ public interface OnSetCropOverlayReleasedListener {
/**
- * Rotates image by the specified number of degrees clockwise.
- * Cycles from 0 to 360 degrees.
+ * Called when the crop overlay changed listener is called and inProgress is false.
*
- * @param degrees Integer specifying the number of degrees to rotate.
+ * @param rect The rect coordinates of the cropped overlay
*/
- public void rotateImage(int degrees) {
- if (mBitmap != null) {
-
- boolean flipAxes = !mCropOverlayView.isFixAspectRatio() && (degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305);
- BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
- float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
- float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
-
- mImageMatrix.invert(mImageInverseMatrix);
-
- BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
- BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
- BitmapUtils.POINTS[2] = 0;
- BitmapUtils.POINTS[3] = 0;
- BitmapUtils.POINTS[4] = 1;
- BitmapUtils.POINTS[5] = 0;
- mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);
-
- mDegreesRotated += degrees;
- mDegreesRotated = mDegreesRotated >= 0 ? mDegreesRotated % 360 : mDegreesRotated % 360 + 360;
-
- applyImageMatrix(getWidth(), getHeight(), true, false);
-
- // adjust the zoom so the crop window size remains the same even after image scale change
- mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
- mZoom /= Math.sqrt(Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2) + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
- mZoom = Math.max(mZoom, 1);
-
- applyImageMatrix(getWidth(), getHeight(), true, false);
-
- mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
-
- // adjust the width/height by the changes in scaling to the image
- double change = Math.sqrt(Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2) + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
- halfWidth *= change;
- halfHeight *= change;
-
- // calculate the new crop window rectangle to center in the same location and have proper width/height
- BitmapUtils.RECT.set(BitmapUtils.POINTS2[0] - halfWidth, BitmapUtils.POINTS2[1] - halfHeight,
- BitmapUtils.POINTS2[0] + halfWidth, BitmapUtils.POINTS2[1] + halfHeight);
+ void onCropOverlayReleased(Rect rect);
+ }
- mCropOverlayView.resetCropOverlayView();
- mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
- applyImageMatrix(getWidth(), getHeight(), true, false);
- handleCropWindowChanged(false, false);
-
- // make sure the crop window rectangle is within the cropping image bounds after all the changes
- mCropOverlayView.fixCurrentCropWindowRect();
- }
- }
-
- //region: Private methods
+ /** Interface definition for a callback to be invoked when the crop overlay is released. */
+ public interface OnSetCropOverlayMovedListener {
/**
- * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result
- * to the widget if still relevant and call listener if set.
+ * Called when the crop overlay is moved
*
- * @param result the result of bitmap loading
+ * @param rect The rect coordinates of the cropped overlay
*/
- void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {
+ void onCropOverlayMoved(Rect rect);
+ }
- mBitmapLoadingWorkerTask = null;
- setProgressBarVisibility();
+ /** Interface definition for a callback to be invoked when the crop overlay is released. */
+ public interface OnSetCropWindowChangeListener {
- if (result.error == null) {
- setBitmap(result.bitmap, result.uri, result.loadSampleSize, result.degreesRotated);
- }
+ /** Called when the crop window is changed */
+ void onCropWindowChanged();
+ }
- OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
- if (listener != null) {
- listener.onSetImageUriComplete(this, result.uri, result.error);
- }
- }
+ /** Interface definition for a callback to be invoked when image async loading is complete. */
+ public interface OnSetImageUriCompleteListener {
/**
- * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if set.
+ * Called when a crop image view has completed loading image for cropping.
+ * If loading failed error parameter will contain the error.
*
- * @param result the result of bitmap cropping
+ * @param view The crop image view that loading of image was complete.
+ * @param uri the URI of the image that was loading
+ * @param error if error occurred during loading will contain the error, otherwise null.
*/
- void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {
+ void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
+ }
+ // endregion
- mBitmapCroppingWorkerTask = null;
- setProgressBarVisibility();
+ // region: Inner class: OnGetCroppedImageCompleteListener
- OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
- if (listener != null) {
- CropResult cropResult = new CropResult(result.bitmap, result.uri, result.error, getCropPoints(), getCropRect(), getRotatedDegrees(), result.sampleSize);
- listener.onCropImageComplete(this, cropResult);
- }
-
- if (result.isSave) {
- OnSaveCroppedImageCompleteListener listener2 = mOnSaveCroppedImageCompleteListener;
- if (listener2 != null) {
- listener2.onSaveCroppedImageComplete(this, result.uri, result.error);
- }
- } else {
- OnGetCroppedImageCompleteListener listener2 = mOnGetCroppedImageCompleteListener;
- if (listener2 != null) {
- listener2.onGetCroppedImageComplete(this, result.bitmap, result.error);
- }
- }
- }
+ /** Interface definition for a callback to be invoked when image async crop is complete. */
+ public interface OnCropImageCompleteListener {
/**
- * {@link #setBitmap(Bitmap, int, Uri, int, int)}}
+ * Called when a crop image view has completed cropping image.
+ * Result object contains the cropped bitmap, saved cropped image uri, crop points data or the
+ * error occured during cropping.
+ *
+ * @param view The crop image view that cropping of image was complete.
+ * @param result the crop image result data (with cropped image or error)
*/
- private void setBitmap(Bitmap bitmap) {
- setBitmap(bitmap, 0, null, 1, 0);
- }
+ void onCropImageComplete(CropImageView view, CropResult result);
+ }
+ // endregion
- /**
- * {@link #setBitmap(Bitmap, int, Uri, int, int)}}
- */
- private void setBitmap(Bitmap bitmap, int imageResource) {
- setBitmap(bitmap, imageResource, null, 1, 0);
- }
+ // region: Inner class: ActivityResult
- /**
- * {@link #setBitmap(Bitmap, int, Uri, int, int)}}
- */
- private void setBitmap(Bitmap bitmap, Uri imageUri, int loadSampleSize, int degreesRotated) {
- setBitmap(bitmap, 0, imageUri, loadSampleSize, degreesRotated);
- }
+ /** Result data of crop image. */
+ public static class CropResult {
/**
- * Set the given bitmap to be used in for cropping
- * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been manipulated.
+ * The image bitmap of the original image loaded for cropping.
+ * Null if uri used to load image or activity result is used.
*/
- private void setBitmap(Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
- if (mBitmap == null || !mBitmap.equals(bitmap)) {
-
- mImageView.clearAnimation();
-
- clearImageInt();
-
- mBitmap = bitmap;
- mImageView.setImageBitmap(mBitmap);
-
- mLoadedImageUri = imageUri;
- mImageResource = imageResource;
- mLoadedSampleSize = loadSampleSize;
- mDegreesRotated = degreesRotated;
-
- applyImageMatrix(getWidth(), getHeight(), true, false);
-
- if (mCropOverlayView != null) {
- mCropOverlayView.resetCropOverlayView();
- setCropOverlayVisibility();
- }
- }
- }
+ private final Bitmap mOriginalBitmap;
/**
- * Clear the current image set for cropping.
- * Full clear will also clear the data of the set image like Uri or Resource id while partial clear
- * will only clear the bitmap and recycle if required.
+ * The Android uri of the original image loaded for cropping.
+ * Null if bitmap was used to load image.
*/
- private void clearImageInt() {
-
- // if we allocated the bitmap, release it as fast as possible
- if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
- mBitmap.recycle();
- }
- mBitmap = null;
-
- // clean the loaded image flags for new image
- mImageResource = 0;
- mLoadedImageUri = null;
- mLoadedSampleSize = 1;
- mDegreesRotated = 0;
- mZoom = 1;
- mZoomOffsetX = 0;
- mZoomOffsetY = 0;
- mImageMatrix.reset();
-
- mImageView.setImageBitmap(null);
-
- setCropOverlayVisibility();
- }
+ private final Uri mOriginalUri;
/**
- * Gets the cropped image based on the current crop window.
- * If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample size to fit in
- * the requested width and height down-sampling if possible - optimization to get best size to quality.
- * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}.
- *
- * @param reqWidth the width to resize the cropped image to (see options)
- * @param reqHeight the height to resize the cropped image to (see options)
- * @param options the resize method to use on the cropped bitmap
- * @param saveUri optional: to save the cropped image to
- * @param saveCompressFormat if saveUri is given, the given compression will be used for saving the image
- * @param saveCompressQuality if saveUri is given, the given quality will be used for the compression.
+ * The cropped image bitmap result.
+ * Null if save cropped image was executed, no output requested or failure.
*/
- public void startCropWorkerTask(int reqWidth, int reqHeight, RequestSizeOptions options, Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
- if (mBitmap != null) {
- mImageView.clearAnimation();
-
- BitmapCroppingWorkerTask currentTask = mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
- if (currentTask != null) {
- // cancel previous cropping
- currentTask.cancel(true);
- }
-
- reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
- reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
-
- int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
- int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
- if (mLoadedImageUri != null && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
- mBitmapCroppingWorkerTask = new WeakReference<>(new BitmapCroppingWorkerTask(this, mLoadedImageUri, getCropPoints(),
- mDegreesRotated, orgWidth, orgHeight,
- mCropOverlayView.isFixAspectRatio(), mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY(),
- reqWidth, reqHeight, options,
- saveUri, saveCompressFormat, saveCompressQuality));
- } else {
- mBitmapCroppingWorkerTask = new WeakReference<>(new BitmapCroppingWorkerTask(this, mBitmap, getCropPoints(), mDegreesRotated,
- mCropOverlayView.isFixAspectRatio(), mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY(),
- reqWidth, reqHeight, options,
- saveUri, saveCompressFormat, saveCompressQuality));
- }
- mBitmapCroppingWorkerTask.get().execute();
- setProgressBarVisibility();
- }
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Bundle bundle = new Bundle();
- bundle.putParcelable("instanceState", super.onSaveInstanceState());
- bundle.putParcelable("LOADED_IMAGE_URI", mLoadedImageUri);
- bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
- if (mLoadedImageUri == null && mImageResource < 1) {
- bundle.putParcelable("SET_BITMAP", mBitmap);
- }
- if (mLoadedImageUri != null && mBitmap != null) {
- String key = UUID.randomUUID().toString();
- BitmapUtils.mStateBitmap = new Pair<>(key, new WeakReference<>(mBitmap));
- bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
- }
- if (mBitmapLoadingWorkerTask != null) {
- BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
- if (task != null) {
- bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
- }
- }
- bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
- bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
- bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());
-
- BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
-
- mImageMatrix.invert(mImageInverseMatrix);
- mImageInverseMatrix.mapRect(BitmapUtils.RECT);
-
- bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
- bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
- bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
- bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
-
- return bundle;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
-
- if (state instanceof Bundle) {
- Bundle bundle = (Bundle) state;
-
- // prevent restoring state if already set by outside code
- if (mBitmapLoadingWorkerTask == null && mLoadedImageUri == null && mBitmap == null && mImageResource == 0) {
-
- Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
- if (uri != null) {
- String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
- if (key != null) {
- Bitmap stateBitmap = BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
- ? BitmapUtils.mStateBitmap.second.get() : null;
- if (stateBitmap != null && !stateBitmap.isRecycled()) {
- BitmapUtils.mStateBitmap = null;
- setBitmap(stateBitmap, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
- }
- }
- if (mLoadedImageUri == null) {
- setImageUriAsync(uri);
- }
- } else {
- int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
- if (resId > 0) {
- setImageResource(resId);
- } else {
- Bitmap bitmap = bundle.getParcelable("SET_BITMAP");
- if (bitmap != null) {
- setBitmap(bitmap);
- } else {
- uri = bundle.getParcelable("LOADING_IMAGE_URI");
- if (uri != null) {
- setImageUriAsync(uri);
- }
- }
- }
- }
-
- mDegreesRotated = bundle.getInt("DEGREES_ROTATED");
-
- mCropOverlayView.setInitialCropWindowRect((Rect) bundle.getParcelable("INITIAL_CROP_RECT"));
-
- mRestoreCropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
-
- mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));
-
- mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
- mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");
- }
-
- super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
- } else {
- super.onRestoreInstanceState(state);
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- if (mBitmap != null) {
-
- // Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
- if (heightSize == 0) {
- heightSize = mBitmap.getHeight();
- }
-
- int desiredWidth;
- int desiredHeight;
-
- double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
- double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;
-
- // Checks if either width or height needs to be fixed
- if (widthSize < mBitmap.getWidth()) {
- viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
- }
- if (heightSize < mBitmap.getHeight()) {
- viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
- }
-
- // If either needs to be fixed, choose smallest ratio and calculate from there
- if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
- if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
- desiredWidth = widthSize;
- desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
- } else {
- desiredHeight = heightSize;
- desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
- }
- } else {
- // Otherwise, the picture is within frame layout bounds. Desired width is simply picture size
- desiredWidth = mBitmap.getWidth();
- desiredHeight = mBitmap.getHeight();
- }
-
- int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
- int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);
-
- mLayoutWidth = width;
- mLayoutHeight = height;
-
- setMeasuredDimension(mLayoutWidth, mLayoutHeight);
-
- } else {
- setMeasuredDimension(widthSize, heightSize);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
- super.onLayout(changed, l, t, r, b);
-
- if (mLayoutWidth > 0 && mLayoutHeight > 0) {
- // Gets original parameters, and creates the new parameters
- ViewGroup.LayoutParams origParams = this.getLayoutParams();
- origParams.width = mLayoutWidth;
- origParams.height = mLayoutHeight;
- setLayoutParams(origParams);
-
- if (mBitmap != null) {
- applyImageMatrix(r - l, b - t, false, false);
-
- // after state restore we want to restore the window crop, possible only after widget size is known
- if (mBitmap != null && mRestoreCropWindowRect != null) {
- mImageMatrix.mapRect(mRestoreCropWindowRect);
- mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
- handleCropWindowChanged(false, false);
- mCropOverlayView.fixCurrentCropWindowRect();
- mRestoreCropWindowRect = null;
- }
- } else {
- updateImageBounds(true);
- }
- } else {
- updateImageBounds(true);
- }
- }
-
- /**
- * Handle crop window change to:
- * 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
- * available view area.
- * 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
- *
- * @param inProgress is the crop window change is still in progress by the user
- * @param animate if to animate the change to the image matrix, or set it directly
- */
- private void handleCropWindowChanged(boolean inProgress, boolean animate) {
- int width = getWidth();
- int height = getHeight();
- if (mBitmap != null && width > 0 && height > 0) {
-
- RectF cropRect = mCropOverlayView.getCropWindowRect();
- if (inProgress) {
- if (cropRect.left < 0 || cropRect.top < 0 || cropRect.right > width || cropRect.bottom > height) {
- applyImageMatrix(width, height, false, false);
- }
- } else if (mAutoZoomEnabled || mZoom > 1) {
- float newZoom = 0;
- // keep the cropping window covered area to 50%-65% of zoomed sub-area
- if (mZoom < mMaxZoom && cropRect.width() < width * 0.5f && cropRect.height() < height * 0.5f) {
- newZoom = Math.min(mMaxZoom, Math.min(width / (cropRect.width() / mZoom / 0.64f), height / (cropRect.height() / mZoom / 0.64f)));
- }
- if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
- newZoom = Math.max(1, Math.min(width / (cropRect.width() / mZoom / 0.51f), height / (cropRect.height() / mZoom / 0.51f)));
- }
- if (!mAutoZoomEnabled) {
- newZoom = 1;
- }
-
- if (newZoom > 0 && newZoom != mZoom) {
- if (animate) {
- if (mAnimation == null) {
- // lazy create animation single instance
- mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
- }
- // set the state for animation to start from
- mAnimation.setStartState(mImagePoints, mImageMatrix);
- }
-
- updateCropRectByZoomChange(newZoom / mZoom);
- mZoom = newZoom;
-
- applyImageMatrix(width, height, true, animate);
- }
- }
- }
- }
+ private final Bitmap mBitmap;
/**
- * Adjust the given crop window rectangle by the change in zoom, need to update the location and size
- * of the crop rectangle to cover the same area in new zoom level.
+ * The Android uri of the saved cropped image result.
+ * Null if get cropped image was executed, no output requested or failure.
*/
- private void updateCropRectByZoomChange(float zoomChange) {
- RectF cropRect = mCropOverlayView.getCropWindowRect();
- float xCenterOffset = getWidth() / 2 - cropRect.centerX();
- float yCenterOffset = getHeight() / 2 - cropRect.centerY();
- cropRect.offset(xCenterOffset - xCenterOffset * zoomChange, yCenterOffset - yCenterOffset * zoomChange);
- cropRect.inset((cropRect.width() - cropRect.width() * zoomChange) / 2f, (cropRect.height() - cropRect.height() * zoomChange) / 2f);
- mCropOverlayView.setCropWindowRect(cropRect);
- }
+ private final Uri mUri;
- /**
- * Apply matrix to handle the image inside the image view.
- *
- * @param width the width of the image view
- * @param height the height of the image view
- */
- private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
- if (mBitmap != null && width > 0 && height > 0) {
+ /** The error that failed the loading/cropping (null if successful) */
+ private final Exception mError;
- mImageMatrix.reset();
+ /** The 4 points of the cropping window in the source image */
+ private final float[] mCropPoints;
- // move the image to the center of the image view first so we can manipulate it from there
- mImageMatrix.postTranslate((width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
- mapImagePointsByImageMatrix();
+ /** The rectangle of the cropping window in the source image */
+ private final Rect mCropRect;
- // rotate the image the required degrees from center of image
- if (mDegreesRotated > 0) {
- mImageMatrix.postRotate(mDegreesRotated, BitmapUtils.getRectCenterX(mImagePoints), BitmapUtils.getRectCenterY(mImagePoints));
- mapImagePointsByImageMatrix();
- }
+ /** The rectangle of the source image dimensions */
+ private final Rect mWholeImageRect;
- // scale the image to the image view, image rect transformed to know new width/height
- float scale = Math.min(width / BitmapUtils.getRectWidth(mImagePoints), height / BitmapUtils.getRectHeight(mImagePoints));
- if (mScaleType == ScaleType.FIT_CENTER || (mScaleType == ScaleType.CENTER_INSIDE && scale < 1) || (scale > 1 && mAutoZoomEnabled)) {
- mImageMatrix.postScale(scale, scale, BitmapUtils.getRectCenterX(mImagePoints), BitmapUtils.getRectCenterY(mImagePoints));
- mapImagePointsByImageMatrix();
- }
-
- // scale by the current zoom level
- mImageMatrix.postScale(mZoom, mZoom, BitmapUtils.getRectCenterX(mImagePoints), BitmapUtils.getRectCenterY(mImagePoints));
- mapImagePointsByImageMatrix();
-
- RectF cropRect = mCropOverlayView.getCropWindowRect();
-
- // reset the crop window offset so we can update it to required value
- cropRect.offset(-mZoomOffsetX * mZoom, -mZoomOffsetY * mZoom);
-
- if (center) {
- // set the zoomed area to be as to the center of cropping window as possible
- mZoomOffsetX = width > BitmapUtils.getRectWidth(mImagePoints) ? 0
- : Math.max(Math.min(width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)), getWidth() - BitmapUtils.getRectRight(mImagePoints)) / mZoom;
- mZoomOffsetY = height > BitmapUtils.getRectHeight(mImagePoints) ? 0
- : Math.max(Math.min(height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)), getHeight() - BitmapUtils.getRectBottom(mImagePoints)) / mZoom;
- } else {
- // adjust the zoomed area so the crop window rectangle will be inside the area in case it was moved outside
- mZoomOffsetX = Math.min(Math.max(mZoomOffsetX * mZoom, -cropRect.left), -cropRect.right + width) / mZoom;
- mZoomOffsetY = Math.min(Math.max(mZoomOffsetY * mZoom, -cropRect.top), -cropRect.bottom + height) / mZoom;
- }
-
- // apply to zoom offset translate and update the crop rectangle to offset correctly
- mImageMatrix.postTranslate(mZoomOffsetX * mZoom, mZoomOffsetY * mZoom);
- cropRect.offset(mZoomOffsetX * mZoom, mZoomOffsetY * mZoom);
- mCropOverlayView.setCropWindowRect(cropRect);
- mapImagePointsByImageMatrix();
-
- // set matrix to apply
- if (animate) {
- // set the state for animation to end in, start animation now
- mAnimation.setEndState(mImagePoints, mImageMatrix);
- mImageView.startAnimation(mAnimation);
- } else {
- mImageView.setImageMatrix(mImageMatrix);
- }
-
- // update the image rectangle in the crop overlay
- updateImageBounds(false);
- }
- }
-
- /**
- * Adjust the given image rectangle by image transformation matrix to know the final rectangle of the image.
- * To get the proper rectangle it must be first reset to orginal image rectangle.
- */
- private void mapImagePointsByImageMatrix() {
- mImagePoints[0] = 0;
- mImagePoints[1] = 0;
- mImagePoints[2] = mBitmap.getWidth();
- mImagePoints[3] = 0;
- mImagePoints[4] = mBitmap.getWidth();
- mImagePoints[5] = mBitmap.getHeight();
- mImagePoints[6] = 0;
- mImagePoints[7] = mBitmap.getHeight();
- mImageMatrix.mapPoints(mImagePoints);
- }
-
- /**
- * Determines the specs for the onMeasure function. Calculates the width or height
- * depending on the mode.
- *
- * @param measureSpecMode The mode of the measured width or height.
- * @param measureSpecSize The size of the measured width or height.
- * @param desiredSize The desired size of the measured width or height.
- * @return The final size of the width or height.
- */
- private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {
-
- // Measure Width
- int spec;
- if (measureSpecMode == MeasureSpec.EXACTLY) {
- // Must be this size
- spec = measureSpecSize;
- } else if (measureSpecMode == MeasureSpec.AT_MOST) {
- // Can't be bigger than...; match_parent value
- spec = Math.min(desiredSize, measureSpecSize);
- } else {
- // Be whatever you want; wrap_content
- spec = desiredSize;
- }
+ /** The final rotation of the cropped image relative to source */
+ private final int mRotation;
- return spec;
- }
+ /** sample size used creating the crop bitmap to lower its size */
+ private final int mSampleSize;
- /**
- * Set visibility of crop overlay to hide it when there is no image or specificly set by client.
- */
- private void setCropOverlayVisibility() {
- if (mCropOverlayView != null) {
- mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
- }
+ CropResult(
+ Bitmap originalBitmap,
+ Uri originalUri,
+ Bitmap bitmap,
+ Uri uri,
+ Exception error,
+ float[] cropPoints,
+ Rect cropRect,
+ Rect wholeImageRect,
+ int rotation,
+ int sampleSize) {
+ mOriginalBitmap = originalBitmap;
+ mOriginalUri = originalUri;
+ mBitmap = bitmap;
+ mUri = uri;
+ mError = error;
+ mCropPoints = cropPoints;
+ mCropRect = cropRect;
+ mWholeImageRect = wholeImageRect;
+ mRotation = rotation;
+ mSampleSize = sampleSize;
}
/**
- * Set visibility of progress bar when async loading/cropping is in process and show is enabled.
+ * The image bitmap of the original image loaded for cropping.
+ * Null if uri used to load image or activity result is used.
*/
- private void setProgressBarVisibility() {
- boolean visible = mShowProgressBar &&
- (mBitmap == null && mBitmapLoadingWorkerTask != null || mBitmapCroppingWorkerTask != null);
- mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
+ public Bitmap getOriginalBitmap() {
+ return mOriginalBitmap;
}
/**
- * Update the scale factor between the actual image bitmap and the shown image.
+ * The Android uri of the original image loaded for cropping.
+ * Null if bitmap was used to load image.
*/
- private void updateImageBounds(boolean clear) {
- if (mBitmap != null && !clear) {
-
- // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for width/height.
- float scaleFactorWidth = mBitmap.getWidth() * mLoadedSampleSize / BitmapUtils.getRectWidth(mImagePoints);
- float scaleFactorHeight = mBitmap.getHeight() * mLoadedSampleSize / BitmapUtils.getRectHeight(mImagePoints);
- mCropOverlayView.setCropWindowLimits(getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
- }
-
- // set the bitmap rectangle and update the crop window after scale factor is set
- mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
+ public Uri getOriginalUri() {
+ return mOriginalUri;
}
- //endregion
- //region: Inner class: CropShape
-
- /**
- * The possible cropping area shape.
- * To set square/circle crop shape set aspect ratio to 1:1.
- */
- public enum CropShape {
- RECTANGLE,
- OVAL
+ /** Is the result is success or error. */
+ public boolean isSuccessful() {
+ return mError == null;
}
- //endregion
-
- //region: Inner class: ScaleType
/**
- * Options for scaling the bounds of cropping image to the bounds of Crop Image View.
- * Note: Some options are affected by auto-zoom, if enabled.
+ * The cropped image bitmap result.
+ * Null if save cropped image was executed, no output requested or failure.
*/
- public enum ScaleType {
-
- /**
- * Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.
- * The largest dimension will be equals to crop image view and the second dimension will be smaller.
- */
- FIT_CENTER,
-
- /**
- * Center the image in the view, but perform no scaling.
- * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it will be
- * scaled uniformly to fit the crop image view.
- */
- CENTER,
-
- /**
- * Scale the image uniformly (maintain the image's aspect ratio) so that both
- * dimensions (width and height) of the image will be equal to or larger than the
- * corresponding dimension of the view (minus padding).
- * The image is then centered in the view.
- */
- CENTER_CROP,
-
- /**
- * Scale the image uniformly (maintain the image's aspect ratio) so that both
- * dimensions (width and height) of the image will be equal to or less than the
- * corresponding dimension of the view (minus padding).
- * The image is then centered in the view.
- * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it will be
- * scaled uniformly to fit the crop image view.
- */
- CENTER_INSIDE
+ public Bitmap getBitmap() {
+ return mBitmap;
}
- //endregion
-
- //region: Inner class: Guidelines
/**
- * The possible guidelines showing types.
+ * The Android uri of the saved cropped image result Null if get cropped image was executed, no
+ * output requested or failure.
*/
- public enum Guidelines {
- /**
- * Never show
- */
- OFF,
-
- /**
- * Show when crop move action is live
- */
- ON_TOUCH,
-
- /**
- * Always show
- */
- ON
+ public Uri getUri() {
+ return mUri;
}
- //endregion
- //region: Inner class: RequestSizeOptions
-
- /**
- * Possible options for handling requested width/height for cropping.
- */
- public enum RequestSizeOptions {
-
- /**
- * No resize/sampling is done unless required for memory management (OOM).
- */
- NONE,
-
- /**
- * Only sample the image during loading (if image set using URI) so the smallest of the image
- * dimensions will be between the requested size and x2 requested size.
- * NOTE: resulting image will not be exactly requested width/height
- * see: Loading Large
- * Bitmaps Efficiently.
- */
- SAMPLING,
-
- /**
- * Resize the image uniformly (maintain the image's aspect ratio) so that both
- * dimensions (width and height) of the image will be equal to or less than the
- * corresponding requested dimension.
- * If the image is smaller than the requested size it will NOT change.
- */
- RESIZE_INSIDE,
-
- /**
- * Resize the image uniformly (maintain the image's aspect ratio) to fit in the given width/height.
- * The largest dimension will be equals to the requested and the second dimension will be smaller.
- * If the image is smaller than the requested size it will enlarge it.
- */
- RESIZE_FIT,
-
- /**
- * Resize the image to fit exactly in the given width/height.
- * This resize method does NOT preserve aspect ratio.
- * If the image is smaller than the requested size it will enlarge it.
- */
- RESIZE_EXACT
+ /** The error that failed the loading/cropping (null if successful) */
+ public Exception getError() {
+ return mError;
}
- //endregion
-
- //region: Inner class: OnSetImageUriCompleteListener
- /**
- * Interface definition for a callback to be invoked when image async loading is complete.
- */
- public interface OnSetImageUriCompleteListener {
-
- /**
- * Called when a crop image view has completed loading image for cropping.
- * If loading failed error parameter will contain the error.
- *
- * @param view The crop image view that loading of image was complete.
- * @param uri the URI of the image that was loading
- * @param error if error occurred during loading will contain the error, otherwise null.
- */
- void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
+ /** The 4 points of the cropping window in the source image */
+ public float[] getCropPoints() {
+ return mCropPoints;
}
- //endregion
- //region: Inner class: OnGetCroppedImageCompleteListener
-
- /**
- * Interface definition for a callback to be invoked when image async crop is complete.
- */
- public interface OnCropImageCompleteListener {
-
- /**
- * Called when a crop image view has completed cropping image.
- * Result object contains the cropped bitmap, saved cropped image uri, crop points data or
- * the error occured during cropping.
- *
- * @param view The crop image view that cropping of image was complete.
- * @param result the crop image result data (with cropped image or error)
- */
- void onCropImageComplete(CropImageView view, CropResult result);
+ /** The rectangle of the cropping window in the source image */
+ public Rect getCropRect() {
+ return mCropRect;
}
- //endregion
-
- //region: Inner class: OnGetCroppedImageCompleteListener
- /**
- * Interface definition for a callback to be invoked when image async cropping is complete.
- *
- * @deprecated use {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)} and {@link
- * OnCropImageCompleteListener}.
- */
- @Deprecated
- public interface OnGetCroppedImageCompleteListener {
-
- /**
- * Called when a crop image view has completed cropping image.
- * If cropping failed error parameter will contain the error.
- *
- * @param view The crop image view that cropping of image was complete.
- * @param bitmap the cropped image bitmap (null if failed)
- * @param error if error occurred during cropping will contain the error, otherwise null.
- */
- void onGetCroppedImageComplete(CropImageView view, Bitmap bitmap, Exception error);
+ /** The rectangle of the source image dimensions */
+ public Rect getWholeImageRect() {
+ return mWholeImageRect;
}
- //endregion
-
- //region: Inner class: OnSaveCroppedImageCompleteListener
- /**
- * Interface definition for a callback to be invoked when image async cropping is complete.
- *
- * @deprecated use {@link #setOnCropImageCompleteListener(OnCropImageCompleteListener)} and {@link
- * OnCropImageCompleteListener}.
- */
- @Deprecated
- public interface OnSaveCroppedImageCompleteListener {
-
- /**
- * Called when a crop image view has completed cropping image.
- * If cropping failed error parameter will contain the error.
- *
- * @param view The crop image view that cropping of image was complete.
- * @param uri the cropped image uri (null if failed)
- * @param error if error occurred during cropping will contain the error, otherwise null.
- */
- void onSaveCroppedImageComplete(CropImageView view, Uri uri, Exception error);
+ /** The final rotation of the cropped image relative to source */
+ public int getRotation() {
+ return mRotation;
}
- //endregion
-
- //region: Inner class: ActivityResult
-
- /**
- * Result data of crop image.
- */
- public static class CropResult {
-
- /**
- * The cropped image bitmap result.
- * Null if save cropped image was executed, no output requested or failure.
- */
- private final Bitmap mBitmap;
-
- /**
- * The Android uri of the saved cropped image result.
- * Null if get cropped image was executed, no output requested or failure.
- */
- private final Uri mUri;
-
- /**
- * The error that failed the loading/cropping (null if successful)
- */
- private final Exception mError;
-
- /**
- * The 4 points of the cropping window in the source image
- */
- private final float[] mCropPoints;
-
- /**
- * The rectangle of the cropping window in the source image
- */
- private final Rect mCropRect;
-
- /**
- * The final rotation of the cropped image relative to source
- */
- private final int mRotation;
-
- /**
- * sample size used creating the crop bitmap to lower its size
- */
- private final int mSampleSize;
-
- CropResult(Bitmap bitmap, Uri uri, Exception error, float[] cropPoints, Rect cropRect, int rotation, int sampleSize) {
- mBitmap = bitmap;
- mUri = uri;
- mError = error;
- mCropPoints = cropPoints;
- mCropRect = cropRect;
- mRotation = rotation;
- mSampleSize = sampleSize;
- }
-
- /**
- * Is the result is success or error.
- */
- public boolean isSuccessful() {
- return mError == null;
- }
- /**
- * The cropped image bitmap result.
- * Null if save cropped image was executed, no output requested or failure.
- */
- public Bitmap getBitmap() {
- return mBitmap;
- }
-
- /**
- * The Android uri of the saved cropped image result
- * Null if get cropped image was executed, no output requested or failure.
- */
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * The error that failed the loading/cropping (null if successful)
- */
- public Exception getError() {
- return mError;
- }
-
- /**
- * The 4 points of the cropping window in the source image
- */
- public float[] getCropPoints() {
- return mCropPoints;
- }
-
- /**
- * The rectangle of the cropping window in the source image
- */
- public Rect getCropRect() {
- return mCropRect;
- }
-
- /**
- * The final rotation of the cropped image relative to source
- */
- public int getRotation() {
- return mRotation;
- }
-
- /**
- * sample size used creating the crop bitmap to lower its size
- */
- public int getSampleSize() {
- return mSampleSize;
- }
+ /** sample size used creating the crop bitmap to lower its size */
+ public int getSampleSize() {
+ return mSampleSize;
}
- //endregion
+ }
+ // endregion
}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java
index 0d2ac520..542c95ab 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java
@@ -29,1036 +29,1012 @@
import java.util.Arrays;
-/**
- * A custom View representing the crop window and the shaded background outside the crop window.
- */
+/** A custom View representing the crop window and the shaded background outside the crop window. */
public class CropOverlayView extends View {
- //region: Fields and Consts
+ // region: Fields and Consts
- /**
- * Gesture detector used for multi touch box scaling
- */
- private ScaleGestureDetector mScaleDetector;
-
- /**
- * Boolean to see if multi touch is enabled for the crop rectangle
- */
- private boolean mMultiTouchEnabled;
-
- /**
- * Handler from crop window stuff, moving and knowing possition.
- */
- private final CropWindowHandler mCropWindowHandler = new CropWindowHandler();
-
- /**
- * Listener to publicj crop window changes
- */
- private CropWindowChangeListener mCropWindowChangeListener;
-
- /**
- * Rectangle used for drawing
- */
- private final RectF mDrawRect = new RectF();
-
- /**
- * The Paint used to draw the white rectangle around the crop area.
- */
- private Paint mBorderPaint;
-
- /**
- * The Paint used to draw the corners of the Border
- */
- private Paint mBorderCornerPaint;
-
- /**
- * The Paint used to draw the guidelines within the crop area when pressed.
- */
- private Paint mGuidelinePaint;
-
- /**
- * The Paint used to darken the surrounding areas outside the crop area.
- */
- private Paint mBackgroundPaint;
-
- /**
- * Used for oval crop window shape or non-straight rotation drawing.
- */
- private Path mPath = new Path();
-
- /**
- * The bounding box around the Bitmap that we are cropping.
- */
- private final float[] mBoundsPoints = new float[8];
-
- /**
- * The bounding box around the Bitmap that we are cropping.
- */
- private final RectF mCalcBounds = new RectF();
-
- /**
- * The bounding image view width used to know the crop overlay is at view edges.
- */
- private int mViewWidth;
-
- /**
- * The bounding image view height used to know the crop overlay is at view edges.
- */
- private int mViewHeight;
-
- /**
- * The offset to draw the border corener from the border
- */
- private float mBorderCornerOffset;
-
- /**
- * the length of the border corner to draw
- */
- private float mBorderCornerLength;
-
- /**
- * The initial crop window padding from image borders
- */
- private float mInitialCropWindowPaddingRatio;
-
- /**
- * The radius of the touch zone (in pixels) around a given Handle.
- */
- private float mTouchRadius;
+ /** Gesture detector used for multi touch box scaling */
+ private ScaleGestureDetector mScaleDetector;
- /**
- * An edge of the crop window will snap to the corresponding edge of a specified bounding box
- * when the crop window edge is less than or equal to this distance (in pixels) away from the bounding box edge.
- */
- private float mSnapRadius;
+ /** Boolean to see if multi touch is enabled for the crop rectangle */
+ private boolean mMultiTouchEnabled;
- /**
- * The Handle that is currently pressed; null if no Handle is pressed.
- */
- private CropWindowMoveHandler mMoveHandler;
+ /** Handler from crop window stuff, moving and knowing possition. */
+ private final CropWindowHandler mCropWindowHandler = new CropWindowHandler();
- /**
- * Flag indicating if the crop area should always be a certain aspect ratio (indicated by mTargetAspectRatio).
- */
- private boolean mFixAspectRatio;
+ /** Listener to publicj crop window changes */
+ private CropWindowChangeListener mCropWindowChangeListener;
- /**
- * save the current aspect ratio of the image
- */
- private int mAspectRatioX;
+ /** Rectangle used for drawing */
+ private final RectF mDrawRect = new RectF();
- /**
- * save the current aspect ratio of the image
- */
- private int mAspectRatioY;
+ /** The Paint used to draw the white rectangle around the crop area. */
+ private Paint mBorderPaint;
- /**
- * The aspect ratio that the crop area should maintain;
- * this variable is only used when mMaintainAspectRatio is true.
- */
- private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+ /** The Paint used to draw the corners of the Border */
+ private Paint mBorderCornerPaint;
- /**
- * Instance variables for customizable attributes
- */
- private CropImageView.Guidelines mGuidelines;
+ /** The Paint used to draw the guidelines within the crop area when pressed. */
+ private Paint mGuidelinePaint;
- /**
- * The shape of the cropping area - rectangle/circular.
- */
- private CropImageView.CropShape mCropShape;
+ /** The Paint used to darken the surrounding areas outside the crop area. */
+ private Paint mBackgroundPaint;
- /**
- * the initial crop window rectangle to set
- */
- private final Rect mInitialCropWindowRect = new Rect();
+ /** Used for oval crop window shape or non-straight rotation drawing. */
+ private Path mPath = new Path();
- /**
- * Whether the Crop View has been initialized for the first time
- */
- private boolean initializedCropWindow;
+ /** The bounding box around the Bitmap that we are cropping. */
+ private final float[] mBoundsPoints = new float[8];
- /**
- * Used to set back LayerType after changing to software.
- */
- private Integer mOriginalLayerType;
- //endregion
+ /** The bounding box around the Bitmap that we are cropping. */
+ private final RectF mCalcBounds = new RectF();
- public CropOverlayView(Context context) {
- this(context, null);
- }
+ /** The bounding image view width used to know the crop overlay is at view edges. */
+ private int mViewWidth;
- public CropOverlayView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
+ /** The bounding image view height used to know the crop overlay is at view edges. */
+ private int mViewHeight;
- /**
- * Set the crop window change listener.
- */
- public void setCropWindowChangeListener(CropWindowChangeListener listener) {
- mCropWindowChangeListener = listener;
- }
+ /** The offset to draw the border corener from the border */
+ private float mBorderCornerOffset;
- /**
- * Get the left/top/right/bottom coordinates of the crop window.
- */
- public RectF getCropWindowRect() {
- return mCropWindowHandler.getRect();
- }
+ /** the length of the border corner to draw */
+ private float mBorderCornerLength;
- /**
- * Set the left/top/right/bottom coordinates of the crop window.
- */
- public void setCropWindowRect(RectF rect) {
- mCropWindowHandler.setRect(rect);
+ /** The initial crop window padding from image borders */
+ private float mInitialCropWindowPaddingRatio;
+
+ /** The radius of the touch zone (in pixels) around a given Handle. */
+ private float mTouchRadius;
+
+ /**
+ * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
+ * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
+ * box edge.
+ */
+ private float mSnapRadius;
+
+ /** The Handle that is currently pressed; null if no Handle is pressed. */
+ private CropWindowMoveHandler mMoveHandler;
+
+ /**
+ * Flag indicating if the crop area should always be a certain aspect ratio (indicated by
+ * mTargetAspectRatio).
+ */
+ private boolean mFixAspectRatio;
+
+ /** save the current aspect ratio of the image */
+ private int mAspectRatioX;
+
+ /** save the current aspect ratio of the image */
+ private int mAspectRatioY;
+
+ /**
+ * The aspect ratio that the crop area should maintain; this variable is only used when
+ * mMaintainAspectRatio is true.
+ */
+ private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+
+ /** Instance variables for customizable attributes */
+ private CropImageView.Guidelines mGuidelines;
+
+ /** The shape of the cropping area - rectangle/circular. */
+ private CropImageView.CropShape mCropShape;
+
+ /** the initial crop window rectangle to set */
+ private final Rect mInitialCropWindowRect = new Rect();
+
+ /** Whether the Crop View has been initialized for the first time */
+ private boolean initializedCropWindow;
+
+ /** Used to set back LayerType after changing to software. */
+ private Integer mOriginalLayerType;
+ // endregion
+
+ public CropOverlayView(Context context) {
+ this(context, null);
+ }
+
+ public CropOverlayView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /** Set the crop window change listener. */
+ public void setCropWindowChangeListener(CropWindowChangeListener listener) {
+ mCropWindowChangeListener = listener;
+ }
+
+ /** Get the left/top/right/bottom coordinates of the crop window. */
+ public RectF getCropWindowRect() {
+ return mCropWindowHandler.getRect();
+ }
+
+ /** Set the left/top/right/bottom coordinates of the crop window. */
+ public void setCropWindowRect(RectF rect) {
+ mCropWindowHandler.setRect(rect);
+ }
+
+ /** Fix the current crop window rectangle if it is outside of cropping image or view bounds. */
+ public void fixCurrentCropWindowRect() {
+ RectF rect = getCropWindowRect();
+ fixCropWindowRectByRules(rect);
+ mCropWindowHandler.setRect(rect);
+ }
+
+ /**
+ * Informs the CropOverlayView of the image's position relative to the ImageView. This is
+ * necessary to call in order to draw the crop window.
+ *
+ * @param boundsPoints the image's bounding points
+ * @param viewWidth The bounding image view width.
+ * @param viewHeight The bounding image view height.
+ */
+ public void setBounds(float[] boundsPoints, int viewWidth, int viewHeight) {
+ if (boundsPoints == null || !Arrays.equals(mBoundsPoints, boundsPoints)) {
+ if (boundsPoints == null) {
+ Arrays.fill(mBoundsPoints, 0);
+ } else {
+ System.arraycopy(boundsPoints, 0, mBoundsPoints, 0, boundsPoints.length);
+ }
+ mViewWidth = viewWidth;
+ mViewHeight = viewHeight;
+ RectF cropRect = mCropWindowHandler.getRect();
+ if (cropRect.width() == 0 || cropRect.height() == 0) {
+ initCropWindow();
+ }
}
-
- /**
- * Fix the current crop window rectangle if it is outside of cropping image or view bounds.
- */
- public void fixCurrentCropWindowRect() {
- RectF rect = getCropWindowRect();
- fixCropWindowRectByRules(rect);
- mCropWindowHandler.setRect(rect);
+ }
+
+ /** Resets the crop overlay view. */
+ public void resetCropOverlayView() {
+ if (initializedCropWindow) {
+ setCropWindowRect(BitmapUtils.EMPTY_RECT_F);
+ initCropWindow();
+ invalidate();
}
-
- /**
- * Informs the CropOverlayView of the image's position relative to the
- * ImageView. This is necessary to call in order to draw the crop window.
- *
- * @param boundsPoints the image's bounding points
- * @param viewWidth The bounding image view width.
- * @param viewHeight The bounding image view height.
- */
- public void setBounds(float[] boundsPoints, int viewWidth, int viewHeight) {
- if (boundsPoints == null || !Arrays.equals(mBoundsPoints, boundsPoints)) {
- if (boundsPoints == null) {
- Arrays.fill(mBoundsPoints, 0);
- } else {
- System.arraycopy(boundsPoints, 0, mBoundsPoints, 0, boundsPoints.length);
- }
- mViewWidth = viewWidth;
- mViewHeight = viewHeight;
- RectF cropRect = mCropWindowHandler.getRect();
- if (cropRect.width() == 0 || cropRect.height() == 0) {
- initCropWindow();
- }
+ }
+
+ /** The shape of the cropping area - rectangle/circular. */
+ public CropImageView.CropShape getCropShape() {
+ return mCropShape;
+ }
+
+ /** The shape of the cropping area - rectangle/circular. */
+ public void setCropShape(CropImageView.CropShape cropShape) {
+ if (mCropShape != cropShape) {
+ mCropShape = cropShape;
+ if (Build.VERSION.SDK_INT <= 17) {
+ if (mCropShape == CropImageView.CropShape.OVAL) {
+ mOriginalLayerType = getLayerType();
+ if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) {
+ // TURN off hardware acceleration
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ } else {
+ mOriginalLayerType = null;
+ }
+ } else if (mOriginalLayerType != null) {
+ // return hardware acceleration back
+ setLayerType(mOriginalLayerType, null);
+ mOriginalLayerType = null;
}
+ }
+ invalidate();
}
-
- /**
- * Resets the crop overlay view.
- */
- public void resetCropOverlayView() {
- if (initializedCropWindow) {
- setCropWindowRect(BitmapUtils.EMPTY_RECT_F);
- initCropWindow();
- invalidate();
- }
+ }
+
+ /** Get the current guidelines option set. */
+ public CropImageView.Guidelines getGuidelines() {
+ return mGuidelines;
+ }
+
+ /**
+ * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
+ * application.
+ */
+ public void setGuidelines(CropImageView.Guidelines guidelines) {
+ if (mGuidelines != guidelines) {
+ mGuidelines = guidelines;
+ if (initializedCropWindow) {
+ invalidate();
+ }
}
-
- /**
- * The shape of the cropping area - rectangle/circular.
- */
- public CropImageView.CropShape getCropShape() {
- return mCropShape;
+ }
+
+ /**
+ * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
+ * be changed.
+ */
+ public boolean isFixAspectRatio() {
+ return mFixAspectRatio;
+ }
+
+ /**
+ * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
+ * it to be changed.
+ */
+ public void setFixedAspectRatio(boolean fixAspectRatio) {
+ if (mFixAspectRatio != fixAspectRatio) {
+ mFixAspectRatio = fixAspectRatio;
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
}
-
- /**
- * The shape of the cropping area - rectangle/circular.
- */
- public void setCropShape(CropImageView.CropShape cropShape) {
- if (mCropShape != cropShape) {
- mCropShape = cropShape;
- if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 17) {
- if (mCropShape == CropImageView.CropShape.OVAL) {
- mOriginalLayerType = getLayerType();
- if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) {
- // TURN off hardware acceleration
- setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- } else {
- mOriginalLayerType = null;
- }
- } else if (mOriginalLayerType != null) {
- // return hardware acceleration back
- setLayerType(mOriginalLayerType, null);
- mOriginalLayerType = null;
- }
- }
- invalidate();
- }
+ }
+
+ /** the X value of the aspect ratio; */
+ public int getAspectRatioX() {
+ return mAspectRatioX;
+ }
+
+ /** Sets the X value of the aspect ratio; is defaulted to 1. */
+ public void setAspectRatioX(int aspectRatioX) {
+ if (aspectRatioX <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ } else if (mAspectRatioX != aspectRatioX) {
+ mAspectRatioX = aspectRatioX;
+ mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
}
-
- /**
- * Get the current guidelines option set.
- */
- public CropImageView.Guidelines getGuidelines() {
- return mGuidelines;
+ }
+
+ /** the Y value of the aspect ratio; */
+ public int getAspectRatioY() {
+ return mAspectRatioY;
+ }
+
+ /**
+ * Sets the Y value of the aspect ratio; is defaulted to 1.
+ *
+ * @param aspectRatioY int that specifies the new Y value of the aspect ratio
+ */
+ public void setAspectRatioY(int aspectRatioY) {
+ if (aspectRatioY <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ } else if (mAspectRatioY != aspectRatioY) {
+ mAspectRatioY = aspectRatioY;
+ mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
}
-
- /**
- * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the application.
- */
- public void setGuidelines(CropImageView.Guidelines guidelines) {
- if (mGuidelines != guidelines) {
- mGuidelines = guidelines;
- if (initializedCropWindow) {
- invalidate();
- }
- }
+ }
+
+ /**
+ * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
+ * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
+ * box edge. (default: 3)
+ */
+ public void setSnapRadius(float snapRadius) {
+ mSnapRadius = snapRadius;
+ }
+
+ /** Set multi touch functionality to enabled/disabled. */
+ public boolean setMultiTouchEnabled(boolean multiTouchEnabled) {
+ if (mMultiTouchEnabled != multiTouchEnabled) {
+ mMultiTouchEnabled = multiTouchEnabled;
+ if (mMultiTouchEnabled && mScaleDetector == null) {
+ mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
+ }
+ return true;
}
-
- /**
- * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to be changed.
- */
- public boolean isFixAspectRatio() {
- return mFixAspectRatio;
+ return false;
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mCropWindowHandler.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mCropWindowHandler.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
+ }
+
+ /**
+ * set the max width/height and scale factor of the shown image to original image to scale the
+ * limits appropriately.
+ */
+ public void setCropWindowLimits(
+ float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
+ mCropWindowHandler.setCropWindowLimits(
+ maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
+ }
+
+ /** Get crop window initial rectangle. */
+ public Rect getInitialCropWindowRect() {
+ return mInitialCropWindowRect;
+ }
+
+ /** Set crop window initial rectangle to be used instead of default. */
+ public void setInitialCropWindowRect(Rect rect) {
+ mInitialCropWindowRect.set(rect != null ? rect : BitmapUtils.EMPTY_RECT);
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ callOnCropWindowChanged(false);
}
-
- /**
- * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to be changed.
- */
- public void setFixedAspectRatio(boolean fixAspectRatio) {
- if (mFixAspectRatio != fixAspectRatio) {
- mFixAspectRatio = fixAspectRatio;
- if (initializedCropWindow) {
- initCropWindow();
- invalidate();
- }
- }
+ }
+
+ /** Reset crop window to initial rectangle. */
+ public void resetCropWindowRect() {
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ callOnCropWindowChanged(false);
}
+ }
- /**
- * the X value of the aspect ratio;
- */
- public int getAspectRatioX() {
- return mAspectRatioX;
- }
+ /**
+ * Sets all initial values, but does not call initCropWindow to reset the views.
+ * Used once at the very start to initialize the attributes.
+ */
+ public void setInitialAttributeValues(CropImageOptions options) {
- /**
- * Sets the X value of the aspect ratio; is defaulted to 1.
- */
- public void setAspectRatioX(int aspectRatioX) {
- if (aspectRatioX <= 0) {
- throw new IllegalArgumentException("Cannot set aspect ratio value to a number less than or equal to 0.");
- } else if (mAspectRatioX != aspectRatioX) {
- mAspectRatioX = aspectRatioX;
- mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
-
- if (initializedCropWindow) {
- initCropWindow();
- invalidate();
- }
- }
- }
+ mCropWindowHandler.setInitialAttributeValues(options);
- /**
- * the Y value of the aspect ratio;
- */
- public int getAspectRatioY() {
- return mAspectRatioY;
- }
+ setCropShape(options.cropShape);
- /**
- * Sets the Y value of the aspect ratio; is defaulted to 1.
- *
- * @param aspectRatioY int that specifies the new Y value of the aspect
- * ratio
- */
- public void setAspectRatioY(int aspectRatioY) {
- if (aspectRatioY <= 0) {
- throw new IllegalArgumentException("Cannot set aspect ratio value to a number less than or equal to 0.");
- } else if (mAspectRatioY != aspectRatioY) {
- mAspectRatioY = aspectRatioY;
- mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
-
- if (initializedCropWindow) {
- initCropWindow();
- invalidate();
- }
- }
- }
+ setSnapRadius(options.snapRadius);
- /**
- * An edge of the crop window will snap to the corresponding edge of a
- * specified bounding box when the crop window edge is less than or equal to
- * this distance (in pixels) away from the bounding box edge. (default: 3)
- */
- public void setSnapRadius(float snapRadius) {
- mSnapRadius = snapRadius;
- }
-
- /**
- * Set multi touch functionality to enabled/disabled.
- */
- public boolean setMultiTouchEnabled(boolean multiTouchEnabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && mMultiTouchEnabled != multiTouchEnabled) {
- mMultiTouchEnabled = multiTouchEnabled;
- if (mMultiTouchEnabled && mScaleDetector == null) {
- mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
- }
- return true;
- }
- return false;
- }
-
- /**
- * the min size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
- mCropWindowHandler.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
- }
-
- /**
- * the max size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
- mCropWindowHandler.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
- }
-
- /**
- * set the max width/height and scale factor of the shown image to original image to scale the limits
- * appropriately.
- */
- public void setCropWindowLimits(float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
- mCropWindowHandler.setCropWindowLimits(maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
- }
-
- /**
- * Get crop window initial rectangle.
- */
- public Rect getInitialCropWindowRect() {
- return mInitialCropWindowRect;
- }
-
- /**
- * Set crop window initial rectangle to be used instead of default.
- */
- public void setInitialCropWindowRect(Rect rect) {
- mInitialCropWindowRect.set(rect != null ? rect : BitmapUtils.EMPTY_RECT);
- if (initializedCropWindow) {
- initCropWindow();
- invalidate();
- callOnCropWindowChanged(false);
- }
- }
-
- /**
- * Reset crop window to initial rectangle.
- */
- public void resetCropWindowRect() {
- if (initializedCropWindow) {
- initCropWindow();
- invalidate();
- callOnCropWindowChanged(false);
- }
- }
-
- /**
- * Sets all initial values, but does not call initCropWindow to reset the views.
- * Used once at the very start to initialize the attributes.
- */
- public void setInitialAttributeValues(CropImageOptions options) {
+ setGuidelines(options.guidelines);
- mCropWindowHandler.setInitialAttributeValues(options);
+ setFixedAspectRatio(options.fixAspectRatio);
- setCropShape(options.cropShape);
+ setAspectRatioX(options.aspectRatioX);
- setSnapRadius(options.snapRadius);
+ setAspectRatioY(options.aspectRatioY);
- setGuidelines(options.guidelines);
+ setMultiTouchEnabled(options.multiTouchEnabled);
- setFixedAspectRatio(options.fixAspectRatio);
+ mTouchRadius = options.touchRadius;
- setAspectRatioX(options.aspectRatioX);
+ mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio;
- setAspectRatioY(options.aspectRatioY);
+ mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor);
- setMultiTouchEnabled(options.multiTouchEnabled);
+ mBorderCornerOffset = options.borderCornerOffset;
+ mBorderCornerLength = options.borderCornerLength;
+ mBorderCornerPaint =
+ getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor);
- mTouchRadius = options.touchRadius;
+ mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor);
- mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio;
+ mBackgroundPaint = getNewPaint(options.backgroundColor);
+ }
- mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor);
+ // region: Private methods
- mBorderCornerOffset = options.borderCornerOffset;
- mBorderCornerLength = options.borderCornerLength;
- mBorderCornerPaint = getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor);
+ /**
+ * Set the initial crop window size and position. This is dependent on the size and position of
+ * the image being cropped.
+ */
+ private void initCropWindow() {
- mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor);
+ float leftLimit = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
+ float topLimit = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
+ float rightLimit = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
+ float bottomLimit = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
- mBackgroundPaint = getNewPaint(options.backgroundColor);
+ if (rightLimit <= leftLimit || bottomLimit <= topLimit) {
+ return;
}
- //region: Private methods
-
- /**
- * Set the initial crop window size and position. This is dependent on the
- * size and position of the image being cropped.
- */
- private void initCropWindow() {
-
- float leftLimit = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
- float topLimit = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
- float rightLimit = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
- float bottomLimit = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
-
- if (rightLimit <= leftLimit || bottomLimit <= topLimit) {
- return;
- }
-
- RectF rect = new RectF();
-
- // Tells the attribute functions the crop window has already been initialized
- initializedCropWindow = true;
+ RectF rect = new RectF();
- float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit);
- float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit);
+ // Tells the attribute functions the crop window has already been initialized
+ initializedCropWindow = true;
- if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) {
- // Get crop window position relative to the displayed image.
- rect.left = leftLimit + mInitialCropWindowRect.left / mCropWindowHandler.getScaleFactorWidth();
- rect.top = topLimit + mInitialCropWindowRect.top / mCropWindowHandler.getScaleFactorHeight();
- rect.right = rect.left + mInitialCropWindowRect.width() / mCropWindowHandler.getScaleFactorWidth();
- rect.bottom = rect.top + mInitialCropWindowRect.height() / mCropWindowHandler.getScaleFactorHeight();
+ float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit);
+ float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit);
- // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap bounds.
- rect.left = Math.max(leftLimit, rect.left);
- rect.top = Math.max(topLimit, rect.top);
- rect.right = Math.min(rightLimit, rect.right);
- rect.bottom = Math.min(bottomLimit, rect.bottom);
+ if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) {
+ // Get crop window position relative to the displayed image.
+ rect.left =
+ leftLimit + mInitialCropWindowRect.left / mCropWindowHandler.getScaleFactorWidth();
+ rect.top = topLimit + mInitialCropWindowRect.top / mCropWindowHandler.getScaleFactorHeight();
+ rect.right =
+ rect.left + mInitialCropWindowRect.width() / mCropWindowHandler.getScaleFactorWidth();
+ rect.bottom =
+ rect.top + mInitialCropWindowRect.height() / mCropWindowHandler.getScaleFactorHeight();
- } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) {
+ // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap
+ // bounds.
+ rect.left = Math.max(leftLimit, rect.left);
+ rect.top = Math.max(topLimit, rect.top);
+ rect.right = Math.min(rightLimit, rect.right);
+ rect.bottom = Math.min(bottomLimit, rect.bottom);
- // If the image aspect ratio is wider than the crop aspect ratio,
- // then the image height is the determining initial length. Else, vice-versa.
- float bitmapAspectRatio = (rightLimit - leftLimit) / (bottomLimit - topLimit);
- if (bitmapAspectRatio > mTargetAspectRatio) {
+ } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) {
- rect.top = topLimit + verticalPadding;
- rect.bottom = bottomLimit - verticalPadding;
+ // If the image aspect ratio is wider than the crop aspect ratio,
+ // then the image height is the determining initial length. Else, vice-versa.
+ float bitmapAspectRatio = (rightLimit - leftLimit) / (bottomLimit - topLimit);
+ if (bitmapAspectRatio > mTargetAspectRatio) {
- float centerX = getWidth() / 2f;
+ rect.top = topLimit + verticalPadding;
+ rect.bottom = bottomLimit - verticalPadding;
- //dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio
- mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY;
+ float centerX = getWidth() / 2f;
- // Limits the aspect ratio to no less than 40 wide or 40 tall
- float cropWidth = Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio);
+ // dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio
+ mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY;
- float halfCropWidth = cropWidth / 2f;
- rect.left = centerX - halfCropWidth;
- rect.right = centerX + halfCropWidth;
+ // Limits the aspect ratio to no less than 40 wide or 40 tall
+ float cropWidth =
+ Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio);
- } else {
+ float halfCropWidth = cropWidth / 2f;
+ rect.left = centerX - halfCropWidth;
+ rect.right = centerX + halfCropWidth;
- rect.left = leftLimit + horizontalPadding;
- rect.right = rightLimit - horizontalPadding;
+ } else {
- float centerY = getHeight() / 2f;
+ rect.left = leftLimit + horizontalPadding;
+ rect.right = rightLimit - horizontalPadding;
- // Limits the aspect ratio to no less than 40 wide or 40 tall
- float cropHeight = Math.max(mCropWindowHandler.getMinCropHeight(), rect.width() / mTargetAspectRatio);
+ float centerY = getHeight() / 2f;
- float halfCropHeight = cropHeight / 2f;
- rect.top = centerY - halfCropHeight;
- rect.bottom = centerY + halfCropHeight;
- }
- } else {
- // Initialize crop window to have 10% padding w/ respect to image.
- rect.left = leftLimit + horizontalPadding;
- rect.top = topLimit + verticalPadding;
- rect.right = rightLimit - horizontalPadding;
- rect.bottom = bottomLimit - verticalPadding;
- }
-
- fixCropWindowRectByRules(rect);
-
- mCropWindowHandler.setRect(rect);
- }
-
- /**
- * Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules.
- */
- private void fixCropWindowRectByRules(RectF rect) {
- if (rect.width() < mCropWindowHandler.getMinCropWidth()) {
- float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2;
- rect.left -= adj;
- rect.right += adj;
- }
- if (rect.height() < mCropWindowHandler.getMinCropHeight()) {
- float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2;
- rect.top -= adj;
- rect.bottom += adj;
- }
- if (rect.width() > mCropWindowHandler.getMaxCropWidth()) {
- float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2;
- rect.left += adj;
- rect.right -= adj;
- }
- if (rect.height() > mCropWindowHandler.getMaxCropHeight()) {
- float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2;
- rect.top += adj;
- rect.bottom -= adj;
- }
+ // Limits the aspect ratio to no less than 40 wide or 40 tall
+ float cropHeight =
+ Math.max(mCropWindowHandler.getMinCropHeight(), rect.width() / mTargetAspectRatio);
- calculateBounds(rect);
- if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) {
- float leftLimit = Math.max(mCalcBounds.left, 0);
- float topLimit = Math.max(mCalcBounds.top, 0);
- float rightLimit = Math.min(mCalcBounds.right, getWidth());
- float bottomLimit = Math.min(mCalcBounds.bottom, getHeight());
- if (rect.left < leftLimit) {
- rect.left = leftLimit;
- }
- if (rect.top < topLimit) {
- rect.top = topLimit;
- }
- if (rect.right > rightLimit) {
- rect.right = rightLimit;
- }
- if (rect.bottom > bottomLimit) {
- rect.bottom = bottomLimit;
- }
- }
- if (mFixAspectRatio && Math.abs(rect.width() - rect.height() * mTargetAspectRatio) > 0.1) {
- if (rect.width() > rect.height() * mTargetAspectRatio) {
- float adj = Math.abs(rect.height() * mTargetAspectRatio - rect.width()) / 2;
- rect.left += adj;
- rect.right -= adj;
- } else {
- float adj = Math.abs(rect.width() / mTargetAspectRatio - rect.height()) / 2;
- rect.top += adj;
- rect.bottom -= adj;
- }
- }
+ float halfCropHeight = cropHeight / 2f;
+ rect.top = centerY - halfCropHeight;
+ rect.bottom = centerY + halfCropHeight;
+ }
+ } else {
+ // Initialize crop window to have 10% padding w/ respect to image.
+ rect.left = leftLimit + horizontalPadding;
+ rect.top = topLimit + verticalPadding;
+ rect.right = rightLimit - horizontalPadding;
+ rect.bottom = bottomLimit - verticalPadding;
}
- /**
- * Draw crop overview by drawing background over image not in the cripping area, then borders and guidelines.
- */
- @Override
- protected void onDraw(Canvas canvas) {
-
- super.onDraw(canvas);
-
- // Draw translucent background for the cropped area.
- drawBackground(canvas);
-
- if (mCropWindowHandler.showGuidelines()) {
- // Determines whether guidelines should be drawn or not
- if (mGuidelines == CropImageView.Guidelines.ON) {
- drawGuidelines(canvas);
- } else if (mGuidelines == CropImageView.Guidelines.ON_TOUCH && mMoveHandler != null) {
- // Draw only when resizing
- drawGuidelines(canvas);
- }
- }
+ fixCropWindowRectByRules(rect);
- drawBorders(canvas);
+ mCropWindowHandler.setRect(rect);
+ }
- drawCorners(canvas);
+ /** Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules. */
+ private void fixCropWindowRectByRules(RectF rect) {
+ if (rect.width() < mCropWindowHandler.getMinCropWidth()) {
+ float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2;
+ rect.left -= adj;
+ rect.right += adj;
}
-
- /**
- * Draw shadow background over the image not including the crop area.
- */
- private void drawBackground(Canvas canvas) {
-
- RectF rect = mCropWindowHandler.getRect();
-
- float left = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
- float top = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
- float right = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
- float bottom = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
-
- if (mCropShape == CropImageView.CropShape.RECTANGLE) {
- if (!isNonStraightAngleRotated() || Build.VERSION.SDK_INT <= 17) {
- canvas.drawRect(left, top, right, rect.top, mBackgroundPaint);
- canvas.drawRect(left, rect.bottom, right, bottom, mBackgroundPaint);
- canvas.drawRect(left, rect.top, rect.left, rect.bottom, mBackgroundPaint);
- canvas.drawRect(rect.right, rect.top, right, rect.bottom, mBackgroundPaint);
- } else {
- mPath.reset();
- mPath.moveTo(mBoundsPoints[0], mBoundsPoints[1]);
- mPath.lineTo(mBoundsPoints[2], mBoundsPoints[3]);
- mPath.lineTo(mBoundsPoints[4], mBoundsPoints[5]);
- mPath.lineTo(mBoundsPoints[6], mBoundsPoints[7]);
- mPath.close();
-
- canvas.save();
- canvas.clipPath(mPath, Region.Op.INTERSECT);
- canvas.clipRect(rect, Region.Op.XOR);
- canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
- canvas.restore();
- }
- } else {
- mPath.reset();
- if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) {
- mDrawRect.set(rect.left + 2, rect.top + 2, rect.right - 2, rect.bottom - 2);
- } else {
- mDrawRect.set(rect.left, rect.top, rect.right, rect.bottom);
- }
- mPath.addOval(mDrawRect, Path.Direction.CW);
- canvas.save();
- canvas.clipPath(mPath, Region.Op.XOR);
- canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
- canvas.restore();
- }
+ if (rect.height() < mCropWindowHandler.getMinCropHeight()) {
+ float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2;
+ rect.top -= adj;
+ rect.bottom += adj;
}
-
- /**
- * Draw 2 veritcal and 2 horizontal guidelines inside the cropping area to split it into 9 equal parts.
- */
- private void drawGuidelines(Canvas canvas) {
- if (mGuidelinePaint != null) {
- float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
- RectF rect = mCropWindowHandler.getRect();
- rect.inset(sw, sw);
-
- float oneThirdCropWidth = rect.width() / 3;
- float oneThirdCropHeight = rect.height() / 3;
-
- if (mCropShape == CropImageView.CropShape.OVAL) {
-
- float w = rect.width() / 2 - sw;
- float h = rect.height() / 2 - sw;
-
- // Draw vertical guidelines.
- float x1 = rect.left + oneThirdCropWidth;
- float x2 = rect.right - oneThirdCropWidth;
- float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w)));
- canvas.drawLine(x1, rect.top + h - yv, x1, rect.bottom - h + yv, mGuidelinePaint);
- canvas.drawLine(x2, rect.top + h - yv, x2, rect.bottom - h + yv, mGuidelinePaint);
-
- // Draw horizontal guidelines.
- float y1 = rect.top + oneThirdCropHeight;
- float y2 = rect.bottom - oneThirdCropHeight;
- float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h)));
- canvas.drawLine(rect.left + w - xv, y1, rect.right - w + xv, y1, mGuidelinePaint);
- canvas.drawLine(rect.left + w - xv, y2, rect.right - w + xv, y2, mGuidelinePaint);
- } else {
-
- // Draw vertical guidelines.
- float x1 = rect.left + oneThirdCropWidth;
- float x2 = rect.right - oneThirdCropWidth;
- canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint);
- canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint);
-
- // Draw horizontal guidelines.
- float y1 = rect.top + oneThirdCropHeight;
- float y2 = rect.bottom - oneThirdCropHeight;
- canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint);
- canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint);
- }
- }
+ if (rect.width() > mCropWindowHandler.getMaxCropWidth()) {
+ float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2;
+ rect.left += adj;
+ rect.right -= adj;
}
-
- /**
- * Draw borders of the crop area.
- */
- private void drawBorders(Canvas canvas) {
- if (mBorderPaint != null) {
- float w = mBorderPaint.getStrokeWidth();
- RectF rect = mCropWindowHandler.getRect();
- rect.inset(w / 2, w / 2);
-
- if (mCropShape == CropImageView.CropShape.RECTANGLE) {
- // Draw rectangle crop window border.
- canvas.drawRect(rect, mBorderPaint);
- } else {
- // Draw circular crop window border
- canvas.drawOval(rect, mBorderPaint);
- }
- }
+ if (rect.height() > mCropWindowHandler.getMaxCropHeight()) {
+ float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2;
+ rect.top += adj;
+ rect.bottom -= adj;
}
- /**
- * Draw the corner of crop overlay.
- */
- private void drawCorners(Canvas canvas) {
- if (mBorderCornerPaint != null) {
-
- float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
- float cornerWidth = mBorderCornerPaint.getStrokeWidth();
- float w = cornerWidth / 2 + mBorderCornerOffset;
- RectF rect = mCropWindowHandler.getRect();
- rect.inset(w, w);
-
- float cornerOffset = (cornerWidth - lineWidth) / 2;
- float cornerExtension = cornerWidth / 2 + cornerOffset;
-
- // Top left
- canvas.drawLine(rect.left - cornerOffset, rect.top - cornerExtension, rect.left - cornerOffset, rect.top + mBorderCornerLength, mBorderCornerPaint);
- canvas.drawLine(rect.left - cornerExtension, rect.top - cornerOffset, rect.left + mBorderCornerLength, rect.top - cornerOffset, mBorderCornerPaint);
-
- // Top right
- canvas.drawLine(rect.right + cornerOffset, rect.top - cornerExtension, rect.right + cornerOffset, rect.top + mBorderCornerLength, mBorderCornerPaint);
- canvas.drawLine(rect.right + cornerExtension, rect.top - cornerOffset, rect.right - mBorderCornerLength, rect.top - cornerOffset, mBorderCornerPaint);
-
- // Bottom left
- canvas.drawLine(rect.left - cornerOffset, rect.bottom + cornerExtension, rect.left - cornerOffset, rect.bottom - mBorderCornerLength, mBorderCornerPaint);
- canvas.drawLine(rect.left - cornerExtension, rect.bottom + cornerOffset, rect.left + mBorderCornerLength, rect.bottom + cornerOffset, mBorderCornerPaint);
-
- // Bottom left
- canvas.drawLine(rect.right + cornerOffset, rect.bottom + cornerExtension, rect.right + cornerOffset, rect.bottom - mBorderCornerLength, mBorderCornerPaint);
- canvas.drawLine(rect.right + cornerExtension, rect.bottom + cornerOffset, rect.right - mBorderCornerLength, rect.bottom + cornerOffset, mBorderCornerPaint);
- }
+ calculateBounds(rect);
+ if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) {
+ float leftLimit = Math.max(mCalcBounds.left, 0);
+ float topLimit = Math.max(mCalcBounds.top, 0);
+ float rightLimit = Math.min(mCalcBounds.right, getWidth());
+ float bottomLimit = Math.min(mCalcBounds.bottom, getHeight());
+ if (rect.left < leftLimit) {
+ rect.left = leftLimit;
+ }
+ if (rect.top < topLimit) {
+ rect.top = topLimit;
+ }
+ if (rect.right > rightLimit) {
+ rect.right = rightLimit;
+ }
+ if (rect.bottom > bottomLimit) {
+ rect.bottom = bottomLimit;
+ }
}
-
- /**
- * Creates the Paint object for drawing.
- */
- private static Paint getNewPaint(int color) {
- Paint paint = new Paint();
- paint.setColor(color);
- return paint;
+ if (mFixAspectRatio && Math.abs(rect.width() - rect.height() * mTargetAspectRatio) > 0.1) {
+ if (rect.width() > rect.height() * mTargetAspectRatio) {
+ float adj = Math.abs(rect.height() * mTargetAspectRatio - rect.width()) / 2;
+ rect.left += adj;
+ rect.right -= adj;
+ } else {
+ float adj = Math.abs(rect.width() / mTargetAspectRatio - rect.height()) / 2;
+ rect.top += adj;
+ rect.bottom -= adj;
+ }
}
-
- /**
- * Creates the Paint object for given thickness and color, if thickness < 0 return null.
- */
- private static Paint getNewPaintOrNull(float thickness, int color) {
- if (thickness > 0) {
- Paint borderPaint = new Paint();
- borderPaint.setColor(color);
- borderPaint.setStrokeWidth(thickness);
- borderPaint.setStyle(Paint.Style.STROKE);
- borderPaint.setAntiAlias(true);
- return borderPaint;
- } else {
- return null;
- }
+ }
+
+ /**
+ * Draw crop overview by drawing background over image not in the cripping area, then borders and
+ * guidelines.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ super.onDraw(canvas);
+
+ // Draw translucent background for the cropped area.
+ drawBackground(canvas);
+
+ if (mCropWindowHandler.showGuidelines()) {
+ // Determines whether guidelines should be drawn or not
+ if (mGuidelines == CropImageView.Guidelines.ON) {
+ drawGuidelines(canvas);
+ } else if (mGuidelines == CropImageView.Guidelines.ON_TOUCH && mMoveHandler != null) {
+ // Draw only when resizing
+ drawGuidelines(canvas);
+ }
}
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // If this View is not enabled, don't allow for touch interactions.
- if (isEnabled()) {
- if (mMultiTouchEnabled) {
- mScaleDetector.onTouchEvent(event);
- }
-
- 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;
- }
+ drawBorders(canvas);
+
+ drawCorners(canvas);
+ }
+
+ /** Draw shadow background over the image not including the crop area. */
+ private void drawBackground(Canvas canvas) {
+
+ RectF rect = mCropWindowHandler.getRect();
+
+ float left = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
+ float top = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
+ float right = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
+ float bottom = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
+
+ if (mCropShape == CropImageView.CropShape.RECTANGLE) {
+ if (!isNonStraightAngleRotated() || Build.VERSION.SDK_INT <= 17) {
+ canvas.drawRect(left, top, right, rect.top, mBackgroundPaint);
+ canvas.drawRect(left, rect.bottom, right, bottom, mBackgroundPaint);
+ canvas.drawRect(left, rect.top, rect.left, rect.bottom, mBackgroundPaint);
+ canvas.drawRect(rect.right, rect.top, right, rect.bottom, mBackgroundPaint);
+ } else {
+ mPath.reset();
+ mPath.moveTo(mBoundsPoints[0], mBoundsPoints[1]);
+ mPath.lineTo(mBoundsPoints[2], mBoundsPoints[3]);
+ mPath.lineTo(mBoundsPoints[4], mBoundsPoints[5]);
+ mPath.lineTo(mBoundsPoints[6], mBoundsPoints[7]);
+ mPath.close();
+
+ canvas.save();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutPath(mPath);
} else {
- return false;
+ canvas.clipPath(mPath, Region.Op.INTERSECT);
}
+ canvas.clipRect(rect, Region.Op.XOR);
+ canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
+ canvas.restore();
+ }
+ } else {
+ mPath.reset();
+ if (Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) {
+ mDrawRect.set(rect.left + 2, rect.top + 2, rect.right - 2, rect.bottom - 2);
+ } else {
+ mDrawRect.set(rect.left, rect.top, rect.right, rect.bottom);
+ }
+ mPath.addOval(mDrawRect, Path.Direction.CW);
+ canvas.save();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutPath(mPath);
+ } else {
+ canvas.clipPath(mPath, Region.Op.XOR);
+ }
+ canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
+ canvas.restore();
}
-
- /**
- * On press down start crop window movment depending on the location of the press.
- * if press is far from crop window then no move handler is returned (null).
- */
- private void onActionDown(float x, float y) {
- mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, mTouchRadius, mCropShape);
- if (mMoveHandler != null) {
- invalidate();
- }
+ }
+
+ /**
+ * Draw 2 veritcal and 2 horizontal guidelines inside the cropping area to split it into 9 equal
+ * parts.
+ */
+ private void drawGuidelines(Canvas canvas) {
+ if (mGuidelinePaint != null) {
+ float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(sw, sw);
+
+ float oneThirdCropWidth = rect.width() / 3;
+ float oneThirdCropHeight = rect.height() / 3;
+
+ if (mCropShape == CropImageView.CropShape.OVAL) {
+
+ float w = rect.width() / 2 - sw;
+ float h = rect.height() / 2 - sw;
+
+ // Draw vertical guidelines.
+ float x1 = rect.left + oneThirdCropWidth;
+ float x2 = rect.right - oneThirdCropWidth;
+ float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w)));
+ canvas.drawLine(x1, rect.top + h - yv, x1, rect.bottom - h + yv, mGuidelinePaint);
+ canvas.drawLine(x2, rect.top + h - yv, x2, rect.bottom - h + yv, mGuidelinePaint);
+
+ // Draw horizontal guidelines.
+ float y1 = rect.top + oneThirdCropHeight;
+ float y2 = rect.bottom - oneThirdCropHeight;
+ float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h)));
+ canvas.drawLine(rect.left + w - xv, y1, rect.right - w + xv, y1, mGuidelinePaint);
+ canvas.drawLine(rect.left + w - xv, y2, rect.right - w + xv, y2, mGuidelinePaint);
+ } else {
+
+ // Draw vertical guidelines.
+ float x1 = rect.left + oneThirdCropWidth;
+ float x2 = rect.right - oneThirdCropWidth;
+ canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint);
+ canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint);
+
+ // Draw horizontal guidelines.
+ float y1 = rect.top + oneThirdCropHeight;
+ float y2 = rect.bottom - oneThirdCropHeight;
+ canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint);
+ canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint);
+ }
}
-
- /**
- * Clear move handler starting in {@link #onActionDown(float, float)} if exists.
- */
- private void onActionUp() {
- if (mMoveHandler != null) {
- mMoveHandler = null;
- callOnCropWindowChanged(false);
- invalidate();
- }
+ }
+
+ /** Draw borders of the crop area. */
+ private void drawBorders(Canvas canvas) {
+ if (mBorderPaint != null) {
+ float w = mBorderPaint.getStrokeWidth();
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(w / 2, w / 2);
+
+ if (mCropShape == CropImageView.CropShape.RECTANGLE) {
+ // Draw rectangle crop window border.
+ canvas.drawRect(rect, mBorderPaint);
+ } else {
+ // Draw circular crop window border
+ canvas.drawOval(rect, mBorderPaint);
+ }
}
-
- /**
- * Handle move of crop window using the move handler created in {@link #onActionDown(float, float)}.
- * The move handler will do the proper move/resize of the crop window.
- */
- private void onActionMove(float x, float y) {
- if (mMoveHandler != null) {
- float snapRadius = mSnapRadius;
- RectF rect = mCropWindowHandler.getRect();
-
- if (calculateBounds(rect)) {
- snapRadius = 0;
- }
-
- mMoveHandler.move(rect, x, y, mCalcBounds, mViewWidth, mViewHeight, snapRadius, mFixAspectRatio, mTargetAspectRatio);
- mCropWindowHandler.setRect(rect);
- callOnCropWindowChanged(true);
- invalidate();
- }
+ }
+
+ /** Draw the corner of crop overlay. */
+ private void drawCorners(Canvas canvas) {
+ if (mBorderCornerPaint != null) {
+
+ float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
+ float cornerWidth = mBorderCornerPaint.getStrokeWidth();
+
+ // for rectangle crop shape we allow the corners to be offset from the borders
+ float w =
+ cornerWidth / 2
+ + (mCropShape == CropImageView.CropShape.RECTANGLE ? mBorderCornerOffset : 0);
+
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(w, w);
+
+ float cornerOffset = (cornerWidth - lineWidth) / 2;
+ float cornerExtension = cornerWidth / 2 + cornerOffset;
+
+ // Top left
+ canvas.drawLine(
+ rect.left - cornerOffset,
+ rect.top - cornerExtension,
+ rect.left - cornerOffset,
+ rect.top + mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.left - cornerExtension,
+ rect.top - cornerOffset,
+ rect.left + mBorderCornerLength,
+ rect.top - cornerOffset,
+ mBorderCornerPaint);
+
+ // Top right
+ canvas.drawLine(
+ rect.right + cornerOffset,
+ rect.top - cornerExtension,
+ rect.right + cornerOffset,
+ rect.top + mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.right + cornerExtension,
+ rect.top - cornerOffset,
+ rect.right - mBorderCornerLength,
+ rect.top - cornerOffset,
+ mBorderCornerPaint);
+
+ // Bottom left
+ canvas.drawLine(
+ rect.left - cornerOffset,
+ rect.bottom + cornerExtension,
+ rect.left - cornerOffset,
+ rect.bottom - mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.left - cornerExtension,
+ rect.bottom + cornerOffset,
+ rect.left + mBorderCornerLength,
+ rect.bottom + cornerOffset,
+ mBorderCornerPaint);
+
+ // Bottom left
+ canvas.drawLine(
+ rect.right + cornerOffset,
+ rect.bottom + cornerExtension,
+ rect.right + cornerOffset,
+ rect.bottom - mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.right + cornerExtension,
+ rect.bottom + cornerOffset,
+ rect.right - mBorderCornerLength,
+ rect.bottom + cornerOffset,
+ mBorderCornerPaint);
}
-
- /**
- * Calculate the bounding rectangle for current crop window, handle non-straight rotation angles.
- * If the rotation angle is straight then the bounds rectangle is the bitmap rectangle,
- * otherwsie we find the max rectangle that is within the image bounds starting from the crop window rectangle.
- *
- * @param rect the crop window rectangle to start finsing bounded rectangle from
- * @return true - non straight rotation in place, false - otherwise.
- */
- private boolean calculateBounds(RectF rect) {
-
- float left = BitmapUtils.getRectLeft(mBoundsPoints);
- float top = BitmapUtils.getRectTop(mBoundsPoints);
- float right = BitmapUtils.getRectRight(mBoundsPoints);
- float bottom = BitmapUtils.getRectBottom(mBoundsPoints);
-
- if (!isNonStraightAngleRotated()) {
- mCalcBounds.set(left, top, right, bottom);
- return false;
+ }
+
+ /** Creates the Paint object for drawing. */
+ private static Paint getNewPaint(int color) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ return paint;
+ }
+
+ /** Creates the Paint object for given thickness and color, if thickness < 0 return null. */
+ private static Paint getNewPaintOrNull(float thickness, int color) {
+ if (thickness > 0) {
+ Paint borderPaint = new Paint();
+ borderPaint.setColor(color);
+ borderPaint.setStrokeWidth(thickness);
+ borderPaint.setStyle(Paint.Style.STROKE);
+ borderPaint.setAntiAlias(true);
+ return borderPaint;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // If this View is not enabled, don't allow for touch interactions.
+ if (isEnabled()) {
+ if (mMultiTouchEnabled) {
+ mScaleDetector.onTouchEvent(event);
+ }
+
+ 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;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * On press down start crop window movment depending on the location of the press.
+ * if press is far from crop window then no move handler is returned (null).
+ */
+ private void onActionDown(float x, float y) {
+ mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, mTouchRadius, mCropShape);
+ if (mMoveHandler != null) {
+ invalidate();
+ }
+ }
+
+ /** Clear move handler starting in {@link #onActionDown(float, float)} if exists. */
+ private void onActionUp() {
+ if (mMoveHandler != null) {
+ mMoveHandler = null;
+ callOnCropWindowChanged(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Handle move of crop window using the move handler created in {@link #onActionDown(float,
+ * float)}.
+ * The move handler will do the proper move/resize of the crop window.
+ */
+ private void onActionMove(float x, float y) {
+ if (mMoveHandler != null) {
+ float snapRadius = mSnapRadius;
+ RectF rect = mCropWindowHandler.getRect();
+
+ if (calculateBounds(rect)) {
+ snapRadius = 0;
+ }
+
+ mMoveHandler.move(
+ rect,
+ x,
+ y,
+ mCalcBounds,
+ mViewWidth,
+ mViewHeight,
+ snapRadius,
+ mFixAspectRatio,
+ mTargetAspectRatio);
+ mCropWindowHandler.setRect(rect);
+ callOnCropWindowChanged(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Calculate the bounding rectangle for current crop window, handle non-straight rotation angles.
+ *
+ * If the rotation angle is straight then the bounds rectangle is the bitmap rectangle, otherwsie
+ * we find the max rectangle that is within the image bounds starting from the crop window
+ * rectangle.
+ *
+ * @param rect the crop window rectangle to start finsing bounded rectangle from
+ * @return true - non straight rotation in place, false - otherwise.
+ */
+ private boolean calculateBounds(RectF rect) {
+
+ float left = BitmapUtils.getRectLeft(mBoundsPoints);
+ float top = BitmapUtils.getRectTop(mBoundsPoints);
+ float right = BitmapUtils.getRectRight(mBoundsPoints);
+ float bottom = BitmapUtils.getRectBottom(mBoundsPoints);
+
+ if (!isNonStraightAngleRotated()) {
+ mCalcBounds.set(left, top, right, bottom);
+ return false;
+ } else {
+ float x0 = mBoundsPoints[0];
+ float y0 = mBoundsPoints[1];
+ float x2 = mBoundsPoints[4];
+ float y2 = mBoundsPoints[5];
+ float x3 = mBoundsPoints[6];
+ float y3 = mBoundsPoints[7];
+
+ if (mBoundsPoints[7] < mBoundsPoints[1]) {
+ if (mBoundsPoints[1] < mBoundsPoints[3]) {
+ x0 = mBoundsPoints[6];
+ y0 = mBoundsPoints[7];
+ x2 = mBoundsPoints[2];
+ y2 = mBoundsPoints[3];
+ x3 = mBoundsPoints[4];
+ y3 = mBoundsPoints[5];
} else {
- float x0 = mBoundsPoints[0];
- float y0 = mBoundsPoints[1];
- float x2 = mBoundsPoints[4];
- float y2 = mBoundsPoints[5];
- float x3 = mBoundsPoints[6];
- float y3 = mBoundsPoints[7];
-
- if (mBoundsPoints[7] < mBoundsPoints[1]) {
- if (mBoundsPoints[1] < mBoundsPoints[3]) {
- x0 = mBoundsPoints[6];
- y0 = mBoundsPoints[7];
- x2 = mBoundsPoints[2];
- y2 = mBoundsPoints[3];
- x3 = mBoundsPoints[4];
- y3 = mBoundsPoints[5];
- } else {
- x0 = mBoundsPoints[4];
- y0 = mBoundsPoints[5];
- x2 = mBoundsPoints[0];
- y2 = mBoundsPoints[1];
- x3 = mBoundsPoints[2];
- y3 = mBoundsPoints[3];
- }
- } else if (mBoundsPoints[1] > mBoundsPoints[3]) {
- x0 = mBoundsPoints[2];
- y0 = mBoundsPoints[3];
- x2 = mBoundsPoints[6];
- y2 = mBoundsPoints[7];
- x3 = mBoundsPoints[0];
- y3 = mBoundsPoints[1];
- }
-
- float a0 = (y3 - y0) / (x3 - x0);
- float a1 = -1f / a0;
- float b0 = y0 - a0 * x0;
- float b1 = y0 - a1 * x0;
- float b2 = y2 - a0 * x2;
- float b3 = y2 - a1 * x2;
-
- float c0 = (rect.centerY() - rect.top) / (rect.centerX() - rect.left);
- float c1 = -c0;
- float d0 = rect.top - c0 * rect.left;
- float d1 = rect.top - c1 * rect.right;
-
- left = Math.max(left, (d0 - b0) / (a0 - c0) < rect.right ? (d0 - b0) / (a0 - c0) : left);
- left = Math.max(left, (d0 - b1) / (a1 - c0) < rect.right ? (d0 - b1) / (a1 - c0) : left);
- left = Math.max(left, (d1 - b3) / (a1 - c1) < rect.right ? (d1 - b3) / (a1 - c1) : left);
- right = Math.min(right, (d1 - b1) / (a1 - c1) > rect.left ? (d1 - b1) / (a1 - c1) : right);
- right = Math.min(right, (d1 - b2) / (a0 - c1) > rect.left ? (d1 - b2) / (a0 - c1) : right);
- right = Math.min(right, (d0 - b2) / (a0 - c0) > rect.left ? (d0 - b2) / (a0 - c0) : right);
-
- top = Math.max(top, Math.max(a0 * left + b0, a1 * right + b1));
- bottom = Math.min(bottom, Math.min(a1 * left + b3, a0 * right + b2));
-
- mCalcBounds.left = left;
- mCalcBounds.top = top;
- mCalcBounds.right = right;
- mCalcBounds.bottom = bottom;
- return true;
+ x0 = mBoundsPoints[4];
+ y0 = mBoundsPoints[5];
+ x2 = mBoundsPoints[0];
+ y2 = mBoundsPoints[1];
+ x3 = mBoundsPoints[2];
+ y3 = mBoundsPoints[3];
}
+ } else if (mBoundsPoints[1] > mBoundsPoints[3]) {
+ x0 = mBoundsPoints[2];
+ y0 = mBoundsPoints[3];
+ x2 = mBoundsPoints[6];
+ y2 = mBoundsPoints[7];
+ x3 = mBoundsPoints[0];
+ y3 = mBoundsPoints[1];
+ }
+
+ float a0 = (y3 - y0) / (x3 - x0);
+ float a1 = -1f / a0;
+ float b0 = y0 - a0 * x0;
+ float b1 = y0 - a1 * x0;
+ float b2 = y2 - a0 * x2;
+ float b3 = y2 - a1 * x2;
+
+ float c0 = (rect.centerY() - rect.top) / (rect.centerX() - rect.left);
+ float c1 = -c0;
+ float d0 = rect.top - c0 * rect.left;
+ float d1 = rect.top - c1 * rect.right;
+
+ left = Math.max(left, (d0 - b0) / (a0 - c0) < rect.right ? (d0 - b0) / (a0 - c0) : left);
+ left = Math.max(left, (d0 - b1) / (a1 - c0) < rect.right ? (d0 - b1) / (a1 - c0) : left);
+ left = Math.max(left, (d1 - b3) / (a1 - c1) < rect.right ? (d1 - b3) / (a1 - c1) : left);
+ right = Math.min(right, (d1 - b1) / (a1 - c1) > rect.left ? (d1 - b1) / (a1 - c1) : right);
+ right = Math.min(right, (d1 - b2) / (a0 - c1) > rect.left ? (d1 - b2) / (a0 - c1) : right);
+ right = Math.min(right, (d0 - b2) / (a0 - c0) > rect.left ? (d0 - b2) / (a0 - c0) : right);
+
+ top = Math.max(top, Math.max(a0 * left + b0, a1 * right + b1));
+ bottom = Math.min(bottom, Math.min(a1 * left + b3, a0 * right + b2));
+
+ mCalcBounds.left = left;
+ mCalcBounds.top = top;
+ mCalcBounds.right = right;
+ mCalcBounds.bottom = bottom;
+ return true;
}
-
- /**
- * Is the cropping image has been rotated by NOT 0,90,180 or 270 degrees.
- */
- private boolean isNonStraightAngleRotated() {
- return mBoundsPoints[0] != mBoundsPoints[6] && mBoundsPoints[1] != mBoundsPoints[7];
+ }
+
+ /** Is the cropping image has been rotated by NOT 0,90,180 or 270 degrees. */
+ private boolean isNonStraightAngleRotated() {
+ return mBoundsPoints[0] != mBoundsPoints[6] && mBoundsPoints[1] != mBoundsPoints[7];
+ }
+
+ /** Invoke on crop change listener safe, don't let the app crash on exception. */
+ private void callOnCropWindowChanged(boolean inProgress) {
+ try {
+ if (mCropWindowChangeListener != null) {
+ mCropWindowChangeListener.onCropWindowChanged(inProgress);
+ }
+ } catch (Exception e) {
+ Log.e("AIC", "Exception in crop window changed", e);
}
+ }
+ // endregion
- /**
- * Invoke on crop change listener safe, don't let the app crash on exception.
- */
- private void callOnCropWindowChanged(boolean inProgress) {
- try {
- if (mCropWindowChangeListener != null) {
- mCropWindowChangeListener.onCropWindowChanged(inProgress);
- }
- } catch (Exception e) {
- Log.e("AIC", "Exception in crop window changed", e);
- }
- }
- //endregion
+ // region: Inner class: CropWindowChangeListener
- //region: Inner class: CropWindowChangeListener
+ /** Interface definition for a callback to be invoked when crop window rectangle is changing. */
+ public interface CropWindowChangeListener {
/**
- * Interface definition for a callback to be invoked when crop window rectangle is changing.
+ * Called after a change in crop window rectangle.
+ *
+ * @param inProgress is the crop window change operation is still in progress by user touch
*/
- public interface CropWindowChangeListener {
-
- /**
- * Called after a change in crop window rectangle.
- *
- * @param inProgress is the crop window change operation is still in progress by user touch
- */
- void onCropWindowChanged(boolean inProgress);
- }
- //endregion
+ void onCropWindowChanged(boolean inProgress);
+ }
+ // endregion
- //region: Inner class: ScaleListener
+ // region: Inner class: ScaleListener
- /**
- * Handle scaling the rectangle based on two finger input
- */
- private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
-
- @Override
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public boolean onScale(ScaleGestureDetector detector) {
- RectF rect = mCropWindowHandler.getRect();
-
- float x = detector.getFocusX();
- float y = detector.getFocusY();
- float dY = detector.getCurrentSpanY() / 2;
- float dX = detector.getCurrentSpanX() / 2;
-
- float newTop = y - dY;
- float newLeft = x - dX;
- float newRight = x + dX;
- float newBottom = y + dY;
-
- if (newLeft < newRight &&
- newTop <= newBottom &&
- newLeft >= 0 &&
- newRight <= mCropWindowHandler.getMaxCropWidth() &&
- newTop >= 0 &&
- newBottom <= mCropWindowHandler.getMaxCropHeight()) {
-
- rect.set(newLeft, newTop, newRight, newBottom);
- mCropWindowHandler.setRect(rect);
- invalidate();
- }
-
- return true;
- }
+ /** Handle scaling the rectangle based on two finger input */
+ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public boolean onScale(ScaleGestureDetector detector) {
+ RectF rect = mCropWindowHandler.getRect();
+
+ float x = detector.getFocusX();
+ float y = detector.getFocusY();
+ float dY = detector.getCurrentSpanY() / 2;
+ float dX = detector.getCurrentSpanX() / 2;
+
+ float newTop = y - dY;
+ float newLeft = x - dX;
+ float newRight = x + dX;
+ float newBottom = y + dY;
+
+ if (newLeft < newRight
+ && newTop <= newBottom
+ && newLeft >= 0
+ && newRight <= mCropWindowHandler.getMaxCropWidth()
+ && newTop >= 0
+ && newBottom <= mCropWindowHandler.getMaxCropHeight()) {
+
+ rect.set(newLeft, newTop, newRight, newBottom);
+ mCropWindowHandler.setRect(rect);
+ invalidate();
+ }
+
+ return true;
}
- //endregion
-}
\ No newline at end of file
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java
index 7e66c527..55953606 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java
@@ -14,382 +14,358 @@
import android.graphics.RectF;
-/**
- * Handler from crop window stuff, moving and knowing possition.
- */
+/** Handler from crop window stuff, moving and knowing possition. */
final class CropWindowHandler {
- //region: Fields and Consts
-
- /**
- * The 4 edges of the crop window defining its coordinates and size
- */
- private final RectF mEdges = new RectF();
-
- /**
- * Rectangle used to return the edges rectangle without ability to change it and without creating new all the time.
- */
- private final RectF mGetEdges = new RectF();
-
- /**
- * Minimum width in pixels that the crop window can get.
- */
- private float mMinCropWindowWidth;
-
- /**
- * Minimum height in pixels that the crop window can get.
- */
- private float mMinCropWindowHeight;
-
- /**
- * Maximum width in pixels that the crop window can CURRENTLY get.
- */
- private float mMaxCropWindowWidth;
-
- /**
- * Maximum height in pixels that the crop window can CURRENTLY get.
- */
- private float mMaxCropWindowHeight;
-
- /**
- * Minimum width in pixels that the result of cropping an image can get,
- * affects crop window width adjusted by width scale factor.
- */
- private float mMinCropResultWidth;
-
- /**
- * Minimum height in pixels that the result of cropping an image can get,
- * affects crop window height adjusted by height scale factor.
- */
- private float mMinCropResultHeight;
-
- /**
- * Maximum width in pixels that the result of cropping an image can get,
- * affects crop window width adjusted by width scale factor.
- */
- private float mMaxCropResultWidth;
-
- /**
- * Maximum height in pixels that the result of cropping an image can get,
- * affects crop window height adjusted by height scale factor.
- */
- private float mMaxCropResultHeight;
-
- /**
- * The width scale factor of shown image and actual image
- */
- private float mScaleFactorWidth = 1;
-
- /**
- * The height scale factor of shown image and actual image
- */
- private float mScaleFactorHeight = 1;
- //endregion
-
- /**
- * Get the left/top/right/bottom coordinates of the crop window.
- */
- public RectF getRect() {
- mGetEdges.set(mEdges);
- return mGetEdges;
+ // region: Fields and Consts
+
+ /** The 4 edges of the crop window defining its coordinates and size */
+ private final RectF mEdges = new RectF();
+
+ /**
+ * Rectangle used to return the edges rectangle without ability to change it and without creating
+ * new all the time.
+ */
+ private final RectF mGetEdges = new RectF();
+
+ /** Minimum width in pixels that the crop window can get. */
+ private float mMinCropWindowWidth;
+
+ /** Minimum height in pixels that the crop window can get. */
+ private float mMinCropWindowHeight;
+
+ /** Maximum width in pixels that the crop window can CURRENTLY get. */
+ private float mMaxCropWindowWidth;
+
+ /** Maximum height in pixels that the crop window can CURRENTLY get. */
+ private float mMaxCropWindowHeight;
+
+ /**
+ * Minimum width in pixels that the result of cropping an image can get, affects crop window width
+ * adjusted by width scale factor.
+ */
+ private float mMinCropResultWidth;
+
+ /**
+ * Minimum height in pixels that the result of cropping an image can get, affects crop window
+ * height adjusted by height scale factor.
+ */
+ private float mMinCropResultHeight;
+
+ /**
+ * Maximum width in pixels that the result of cropping an image can get, affects crop window width
+ * adjusted by width scale factor.
+ */
+ private float mMaxCropResultWidth;
+
+ /**
+ * Maximum height in pixels that the result of cropping an image can get, affects crop window
+ * height adjusted by height scale factor.
+ */
+ private float mMaxCropResultHeight;
+
+ /** The width scale factor of shown image and actual image */
+ private float mScaleFactorWidth = 1;
+
+ /** The height scale factor of shown image and actual image */
+ private float mScaleFactorHeight = 1;
+ // endregion
+
+ /** Get the left/top/right/bottom coordinates of the crop window. */
+ public RectF getRect() {
+ mGetEdges.set(mEdges);
+ return mGetEdges;
+ }
+
+ /** Minimum width in pixels that the crop window can get. */
+ public float getMinCropWidth() {
+ return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth);
+ }
+
+ /** Minimum height in pixels that the crop window can get. */
+ public float getMinCropHeight() {
+ return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight);
+ }
+
+ /** Maximum width in pixels that the crop window can get. */
+ public float getMaxCropWidth() {
+ return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
+ }
+
+ /** Maximum height in pixels that the crop window can get. */
+ public float getMaxCropHeight() {
+ return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
+ }
+
+ /** get the scale factor (on width) of the showen image to original image. */
+ public float getScaleFactorWidth() {
+ return mScaleFactorWidth;
+ }
+
+ /** get the scale factor (on height) of the showen image to original image. */
+ public float getScaleFactorHeight() {
+ return mScaleFactorHeight;
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mMinCropResultWidth = minCropResultWidth;
+ mMinCropResultHeight = minCropResultHeight;
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mMaxCropResultWidth = maxCropResultWidth;
+ mMaxCropResultHeight = maxCropResultHeight;
+ }
+
+ /**
+ * set the max width/height and scale factor of the showen image to original image to scale the
+ * limits appropriately.
+ */
+ public void setCropWindowLimits(
+ float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
+ mMaxCropWindowWidth = maxWidth;
+ mMaxCropWindowHeight = maxHeight;
+ mScaleFactorWidth = scaleFactorWidth;
+ mScaleFactorHeight = scaleFactorHeight;
+ }
+
+ /** Set the variables to be used during crop window handling. */
+ public void setInitialAttributeValues(CropImageOptions options) {
+ mMinCropWindowWidth = options.minCropWindowWidth;
+ mMinCropWindowHeight = options.minCropWindowHeight;
+ mMinCropResultWidth = options.minCropResultWidth;
+ mMinCropResultHeight = options.minCropResultHeight;
+ mMaxCropResultWidth = options.maxCropResultWidth;
+ mMaxCropResultHeight = options.maxCropResultHeight;
+ }
+
+ /** Set the left/top/right/bottom coordinates of the crop window. */
+ public void setRect(RectF rect) {
+ mEdges.set(rect);
+ }
+
+ /**
+ * Indicates whether the crop window is small enough that the guidelines should be shown. Public
+ * because this function is also used to determine if the center handle should be focused.
+ *
+ * @return boolean Whether the guidelines should be shown or not
+ */
+ public boolean showGuidelines() {
+ return !(mEdges.width() < 100 || mEdges.height() < 100);
+ }
+
+ /**
+ * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
+ * box, and the touch radius.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param targetRadius the target radius in pixels
+ * @return the Handle that was pressed; null if no Handle was pressed
+ */
+ public CropWindowMoveHandler getMoveHandler(
+ float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
+ CropWindowMoveHandler.Type type =
+ cropShape == CropImageView.CropShape.OVAL
+ ? getOvalPressedMoveType(x, y)
+ : getRectanglePressedMoveType(x, y, targetRadius);
+ return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
+ }
+
+ // region: Private methods
+
+ /**
+ * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
+ * box, and the touch radius.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param targetRadius the target radius in pixels
+ * @return the Handle that was pressed; null if no Handle was pressed
+ */
+ private CropWindowMoveHandler.Type getRectanglePressedMoveType(
+ float x, float y, float targetRadius) {
+ CropWindowMoveHandler.Type moveType = null;
+
+ // Note: corner-handles take precedence, then side-handles, then center.
+ if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.TOP_LEFT;
+ } else if (CropWindowHandler.isInCornerTargetZone(
+ x, y, mEdges.right, mEdges.top, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
+ } else if (CropWindowHandler.isInCornerTargetZone(
+ x, y, mEdges.left, mEdges.bottom, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
+ } else if (CropWindowHandler.isInCornerTargetZone(
+ x, y, mEdges.right, mEdges.bottom, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
+ } else if (CropWindowHandler.isInCenterTargetZone(
+ x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
+ && focusCenter()) {
+ moveType = CropWindowMoveHandler.Type.CENTER;
+ } else if (CropWindowHandler.isInHorizontalTargetZone(
+ x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.TOP;
+ } else if (CropWindowHandler.isInHorizontalTargetZone(
+ x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.BOTTOM;
+ } else if (CropWindowHandler.isInVerticalTargetZone(
+ x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.LEFT;
+ } else if (CropWindowHandler.isInVerticalTargetZone(
+ x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
+ moveType = CropWindowMoveHandler.Type.RIGHT;
+ } else if (CropWindowHandler.isInCenterTargetZone(
+ x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
+ && !focusCenter()) {
+ moveType = CropWindowMoveHandler.Type.CENTER;
}
- /**
- * Minimum width in pixels that the crop window can get.
- */
- public float getMinCropWidth() {
- return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth);
+ return moveType;
+ }
+
+ /**
+ * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
+ * box/oval, and the touch radius.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @return the Handle that was pressed; null if no Handle was pressed
+ */
+ private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {
+
+ /*
+ Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
+ this is not perfect, it's a good quick-to-ship approach.
+
+ TL T T T T TR
+ L C C C C R
+ L C C C C R
+ L C C C C R
+ L C C C C R
+ BL B B B B BR
+ */
+
+ float cellLength = mEdges.width() / 6;
+ float leftCenter = mEdges.left + cellLength;
+ float rightCenter = mEdges.left + (5 * cellLength);
+
+ float cellHeight = mEdges.height() / 6;
+ float topCenter = mEdges.top + cellHeight;
+ float bottomCenter = mEdges.top + 5 * cellHeight;
+
+ CropWindowMoveHandler.Type moveType;
+ if (x < leftCenter) {
+ if (y < topCenter) {
+ moveType = CropWindowMoveHandler.Type.TOP_LEFT;
+ } else if (y < bottomCenter) {
+ moveType = CropWindowMoveHandler.Type.LEFT;
+ } else {
+ moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
+ }
+ } else if (x < rightCenter) {
+ if (y < topCenter) {
+ moveType = CropWindowMoveHandler.Type.TOP;
+ } else if (y < bottomCenter) {
+ moveType = CropWindowMoveHandler.Type.CENTER;
+ } else {
+ moveType = CropWindowMoveHandler.Type.BOTTOM;
+ }
+ } else {
+ if (y < topCenter) {
+ moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
+ } else if (y < bottomCenter) {
+ moveType = CropWindowMoveHandler.Type.RIGHT;
+ } else {
+ moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
+ }
}
- /**
- * Minimum height in pixels that the crop window can get.
- */
- public float getMinCropHeight() {
- return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight);
- }
-
- /**
- * Maximum width in pixels that the crop window can get.
- */
- public float getMaxCropWidth() {
- return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
- }
-
- /**
- * Maximum height in pixels that the crop window can get.
- */
- public float getMaxCropHeight() {
- return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
- }
-
- /**
- * get the scale factor (on width) of the showen image to original image.
- */
- public float getScaleFactorWidth() {
- return mScaleFactorWidth;
- }
-
- /**
- * get the scale factor (on height) of the showen image to original image.
- */
- public float getScaleFactorHeight() {
- return mScaleFactorHeight;
- }
-
- /**
- * the min size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
- mMinCropResultWidth = minCropResultWidth;
- mMinCropResultHeight = minCropResultHeight;
- }
-
- /**
- * the max size the resulting cropping image is allowed to be, affects the cropping window limits
- * (in pixels).
- */
- public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
- mMaxCropResultWidth = maxCropResultWidth;
- mMaxCropResultHeight = maxCropResultHeight;
- }
-
- /**
- * set the max width/height and scale factor of the showen image to original image to scale the limits
- * appropriately.
- */
- public void setCropWindowLimits(float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
- mMaxCropWindowWidth = maxWidth;
- mMaxCropWindowHeight = maxHeight;
- mScaleFactorWidth = scaleFactorWidth;
- mScaleFactorHeight = scaleFactorHeight;
- }
-
- /**
- * Set the variables to be used during crop window handling.
- */
- public void setInitialAttributeValues(CropImageOptions options) {
- mMinCropWindowWidth = options.minCropWindowWidth;
- mMinCropWindowHeight = options.minCropWindowHeight;
- mMinCropResultWidth = options.minCropResultWidth;
- mMinCropResultHeight = options.minCropResultHeight;
- mMaxCropResultWidth = options.maxCropResultWidth;
- mMaxCropResultHeight = options.maxCropResultHeight;
- }
-
- /**
- * Set the left/top/right/bottom coordinates of the crop window.
- */
- public void setRect(RectF rect) {
- mEdges.set(rect);
- }
-
- /**
- * Indicates whether the crop window is small enough that the guidelines
- * should be shown. Public because this function is also used to determine
- * if the center handle should be focused.
- *
- * @return boolean Whether the guidelines should be shown or not
- */
- public boolean showGuidelines() {
- return !(mEdges.width() < 100 || mEdges.height() < 100);
- }
-
- /**
- * Determines which, if any, of the handles are pressed given the touch
- * coordinates, the bounding box, and the touch radius.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param targetRadius the target radius in pixels
- * @return the Handle that was pressed; null if no Handle was pressed
- */
- public CropWindowMoveHandler getMoveHandler(float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
- CropWindowMoveHandler.Type type = cropShape == CropImageView.CropShape.OVAL
- ? getOvalPressedMoveType(x, y)
- : getRectanglePressedMoveType(x, y, targetRadius);
- return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
- }
-
- //region: Private methods
-
- /**
- * Determines which, if any, of the handles are pressed given the touch
- * coordinates, the bounding box, and the touch radius.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param targetRadius the target radius in pixels
- * @return the Handle that was pressed; null if no Handle was pressed
- */
- private CropWindowMoveHandler.Type getRectanglePressedMoveType(float x, float y, float targetRadius) {
- CropWindowMoveHandler.Type moveType = null;
-
- // Note: corner-handles take precedence, then side-handles, then center.
- if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.TOP_LEFT;
- } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.right, mEdges.top, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
- } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.bottom, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
- } else if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.right, mEdges.bottom, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
- } else if (CropWindowHandler.isInCenterTargetZone(x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom) && focusCenter()) {
- moveType = CropWindowMoveHandler.Type.CENTER;
- } else if (CropWindowHandler.isInHorizontalTargetZone(x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.TOP;
- } else if (CropWindowHandler.isInHorizontalTargetZone(x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.BOTTOM;
- } else if (CropWindowHandler.isInVerticalTargetZone(x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.LEFT;
- } else if (CropWindowHandler.isInVerticalTargetZone(x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
- moveType = CropWindowMoveHandler.Type.RIGHT;
- } else if (CropWindowHandler.isInCenterTargetZone(x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom) && !focusCenter()) {
- moveType = CropWindowMoveHandler.Type.CENTER;
- }
-
- return moveType;
- }
-
- /**
- * Determines which, if any, of the handles are pressed given the touch
- * coordinates, the bounding box/oval, and the touch radius.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @return the Handle that was pressed; null if no Handle was pressed
- */
- private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {
-
- /*
- Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
- this is not perfect, it's a good quick-to-ship approach.
-
- TL T T T T TR
- L C C C C R
- L C C C C R
- L C C C C R
- L C C C C R
- BL B B B B BR
- */
-
- float cellLength = mEdges.width() / 6;
- float leftCenter = mEdges.left + cellLength;
- float rightCenter = mEdges.left + (5 * cellLength);
-
- float cellHeight = mEdges.height() / 6;
- float topCenter = mEdges.top + cellHeight;
- float bottomCenter = mEdges.top + 5 * cellHeight;
-
- CropWindowMoveHandler.Type moveType;
- if (x < leftCenter) {
- if (y < topCenter) {
- moveType = CropWindowMoveHandler.Type.TOP_LEFT;
- } else if (y < bottomCenter) {
- moveType = CropWindowMoveHandler.Type.LEFT;
- } else {
- moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
- }
- } else if (x < rightCenter) {
- if (y < topCenter) {
- moveType = CropWindowMoveHandler.Type.TOP;
- } else if (y < bottomCenter) {
- moveType = CropWindowMoveHandler.Type.CENTER;
- } else {
- moveType = CropWindowMoveHandler.Type.BOTTOM;
- }
- } else {
- if (y < topCenter) {
- moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
- } else if (y < bottomCenter) {
- moveType = CropWindowMoveHandler.Type.RIGHT;
- } else {
- moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
- }
- }
-
- return moveType;
- }
-
- /**
- * Determines if the specified coordinate is in the target touch zone for a
- * corner handle.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param handleX the x-coordinate of the corner handle
- * @param handleY the y-coordinate of the corner handle
- * @param targetRadius the target radius in pixels
- * @return true if the touch point is in the target touch zone; false
- * otherwise
- */
- private static boolean isInCornerTargetZone(float x, float y, float handleX, float handleY, float targetRadius) {
- return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius;
- }
-
- /**
- * Determines if the specified coordinate is in the target touch zone for a
- * horizontal bar handle.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param handleXStart the left x-coordinate of the horizontal bar handle
- * @param handleXEnd the right x-coordinate of the horizontal bar handle
- * @param handleY the y-coordinate of the horizontal bar handle
- * @param targetRadius the target radius in pixels
- * @return true if the touch point is in the target touch zone; false
- * otherwise
- */
- private static boolean isInHorizontalTargetZone(float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
- return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius;
- }
-
- /**
- * Determines if the specified coordinate is in the target touch zone for a
- * vertical bar handle.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param handleX the x-coordinate of the vertical bar handle
- * @param handleYStart the top y-coordinate of the vertical bar handle
- * @param handleYEnd the bottom y-coordinate of the vertical bar handle
- * @param targetRadius the target radius in pixels
- * @return true if the touch point is in the target touch zone; false
- * otherwise
- */
- private static boolean isInVerticalTargetZone(float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
- return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd;
- }
-
- /**
- * Determines if the specified coordinate falls anywhere inside the given
- * bounds.
- *
- * @param x the x-coordinate of the touch point
- * @param y the y-coordinate of the touch point
- * @param left the x-coordinate of the left bound
- * @param top the y-coordinate of the top bound
- * @param right the x-coordinate of the right bound
- * @param bottom the y-coordinate of the bottom bound
- * @return true if the touch point is inside the bounding rectangle; false
- * otherwise
- */
- private static boolean isInCenterTargetZone(float x, float y, float left, float top, float right, float bottom) {
- return x > left && x < right && y > top && y < bottom;
- }
-
- /**
- * Determines if the cropper should focus on the center handle or the side
- * handles. If it is a small image, focus on the center handle so the user
- * can move it. If it is a large image, focus on the side handles so user
- * can grab them. Corresponds to the appearance of the
- * RuleOfThirdsGuidelines.
- *
- * @return true if it is small enough such that it should focus on the
- * center; less than show_guidelines limit
- */
- private boolean focusCenter() {
- return !showGuidelines();
- }
- //endregion
-}
\ No newline at end of file
+ return moveType;
+ }
+
+ /**
+ * Determines if the specified coordinate is in the target touch zone for a corner handle.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param handleX the x-coordinate of the corner handle
+ * @param handleY the y-coordinate of the corner handle
+ * @param targetRadius the target radius in pixels
+ * @return true if the touch point is in the target touch zone; false otherwise
+ */
+ private static boolean isInCornerTargetZone(
+ float x, float y, float handleX, float handleY, float targetRadius) {
+ return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius;
+ }
+
+ /**
+ * Determines if the specified coordinate is in the target touch zone for a horizontal bar handle.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param handleXStart the left x-coordinate of the horizontal bar handle
+ * @param handleXEnd the right x-coordinate of the horizontal bar handle
+ * @param handleY the y-coordinate of the horizontal bar handle
+ * @param targetRadius the target radius in pixels
+ * @return true if the touch point is in the target touch zone; false otherwise
+ */
+ private static boolean isInHorizontalTargetZone(
+ float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
+ return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius;
+ }
+
+ /**
+ * Determines if the specified coordinate is in the target touch zone for a vertical bar handle.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param handleX the x-coordinate of the vertical bar handle
+ * @param handleYStart the top y-coordinate of the vertical bar handle
+ * @param handleYEnd the bottom y-coordinate of the vertical bar handle
+ * @param targetRadius the target radius in pixels
+ * @return true if the touch point is in the target touch zone; false otherwise
+ */
+ private static boolean isInVerticalTargetZone(
+ float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
+ return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd;
+ }
+
+ /**
+ * Determines if the specified coordinate falls anywhere inside the given bounds.
+ *
+ * @param x the x-coordinate of the touch point
+ * @param y the y-coordinate of the touch point
+ * @param left the x-coordinate of the left bound
+ * @param top the y-coordinate of the top bound
+ * @param right the x-coordinate of the right bound
+ * @param bottom the y-coordinate of the bottom bound
+ * @return true if the touch point is inside the bounding rectangle; false otherwise
+ */
+ private static boolean isInCenterTargetZone(
+ float x, float y, float left, float top, float right, float bottom) {
+ return x > left && x < right && y > top && y < bottom;
+ }
+
+ /**
+ * Determines if the cropper should focus on the center handle or the side handles. If it is a
+ * small image, focus on the center handle so the user can move it. If it is a large image, focus
+ * on the side handles so user can grab them. Corresponds to the appearance of the
+ * RuleOfThirdsGuidelines.
+ *
+ * @return true if it is small enough such that it should focus on the center; less than
+ * show_guidelines limit
+ */
+ private boolean focusCenter() {
+ return !showGuidelines();
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java
index 0c34061b..f14b77bb 100644
--- a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java
@@ -17,693 +17,750 @@
import android.graphics.RectF;
/**
- * Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
+ * Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
+ *
*/
final class CropWindowMoveHandler {
- //region: Fields and Consts
-
- /**
- * Matrix used for rectangle rotation handling
- */
- private static final Matrix MATRIX = new Matrix();
-
- /**
- * Minimum width in pixels that the crop window can get.
- */
- private final float mMinCropWidth;
-
- /**
- * Minimum width in pixels that the crop window can get.
- */
- private final float mMinCropHeight;
-
- /**
- * Maximum height in pixels that the crop window can get.
- */
- private final float mMaxCropWidth;
-
- /**
- * Maximum height in pixels that the crop window can get.
- */
- private final float mMaxCropHeight;
-
- /**
- * The type of crop window move that is handled.
- */
- private final Type mType;
-
- /**
- * Holds the x and y offset between the exact touch location and the exact handle location that is activated.
- * There may be an offset because we allow for some leeway (specified by mHandleRadius) in activating a handle.
- * However, we want to maintain these offset values while the handle is being dragged so that the handle
- * doesn't jump.
- */
- private final PointF mTouchOffset = new PointF();
- //endregion
-
- /**
- * @param edgeMoveType the type of move this handler is executing
- * @param horizontalEdge the primary edge associated with this handle; may be null
- * @param verticalEdge the secondary edge associated with this handle; may be null
- * @param cropWindowHandler main crop window handle to get and update the crop window edges
- * @param touchX the location of the initial toch possition to measure move distance
- * @param touchY the location of the initial toch possition to measure move distance
- */
- public CropWindowMoveHandler(Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
- mType = type;
- mMinCropWidth = cropWindowHandler.getMinCropWidth();
- mMinCropHeight = cropWindowHandler.getMinCropHeight();
- mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
- mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
- calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
- }
-
- /**
- * Updates the crop window by change in the toch location.
- * Move type handled by this instance, as initialized in creation, affects how the change in toch location
- * changes the crop window position and size.
- * After the crop window position/size is changed by toch move it may result in values that vialate contraints:
- * outside the bounds of the shown bitmap, smaller/larger than min/max size or missmatch in aspect ratio.
- * So a series of fixes is executed on "secondary" edges to adjust it by the "primary" edge movement.
- * Primary is the edge directly affected by move type, secondary is the other edge.
- * The crop window is changed by directly setting the Edge coordinates.
- *
- * @param x the new x-coordinate of this handle
- * @param y the new y-coordinate of this handle
- * @param bounds the bounding rectangle of the image
- * @param viewWidth The bounding image view width used to know the crop overlay is at view edges.
- * @param viewHeight The bounding image view height used to know the crop overlay is at view edges.
- * @param parentView the parent View containing the image
- * @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the image
- * @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used
- * @param aspectRatio the aspect ratio to maintain
- */
- public void move(RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin, boolean fixedAspectRatio, float aspectRatio) {
-
- // Adjust the coordinates for the finger position's offset (i.e. the
- // distance from the initial touch to the precise handle location).
- // We want to maintain the initial touch's distance to the pressed
- // handle so that the crop window size does not "jump".
- float adjX = x + mTouchOffset.x;
- float adjY = y + mTouchOffset.y;
-
- if (mType == Type.CENTER) {
- moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
- } else {
- if (fixedAspectRatio) {
- moveSizeWithFixedAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio);
- } else {
- moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
- }
- }
+ // region: Fields and Consts
+
+ /** Matrix used for rectangle rotation handling */
+ private static final Matrix MATRIX = new Matrix();
+
+ /** Minimum width in pixels that the crop window can get. */
+ private final float mMinCropWidth;
+
+ /** Minimum width in pixels that the crop window can get. */
+ private final float mMinCropHeight;
+
+ /** Maximum height in pixels that the crop window can get. */
+ private final float mMaxCropWidth;
+
+ /** Maximum height in pixels that the crop window can get. */
+ private final float mMaxCropHeight;
+
+ /** The type of crop window move that is handled. */
+ private final Type mType;
+
+ /**
+ * Holds the x and y offset between the exact touch location and the exact handle location that is
+ * activated. There may be an offset because we allow for some leeway (specified by mHandleRadius)
+ * in activating a handle. However, we want to maintain these offset values while the handle is
+ * being dragged so that the handle doesn't jump.
+ */
+ private final PointF mTouchOffset = new PointF();
+ // endregion
+
+ /**
+ * @param edgeMoveType the type of move this handler is executing
+ * @param horizontalEdge the primary edge associated with this handle; may be null
+ * @param verticalEdge the secondary edge associated with this handle; may be null
+ * @param cropWindowHandler main crop window handle to get and update the crop window edges
+ * @param touchX the location of the initial toch possition to measure move distance
+ * @param touchY the location of the initial toch possition to measure move distance
+ */
+ public CropWindowMoveHandler(
+ Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
+ mType = type;
+ mMinCropWidth = cropWindowHandler.getMinCropWidth();
+ mMinCropHeight = cropWindowHandler.getMinCropHeight();
+ mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
+ mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
+ calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
+ }
+
+ /**
+ * Updates the crop window by change in the toch location.
+ * Move type handled by this instance, as initialized in creation, affects how the change in toch
+ * location changes the crop window position and size.
+ * After the crop window position/size is changed by toch move it may result in values that
+ * vialate contraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or
+ * missmatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it
+ * by the "primary" edge movement.
+ * Primary is the edge directly affected by move type, secondary is the other edge.
+ * The crop window is changed by directly setting the Edge coordinates.
+ *
+ * @param x the new x-coordinate of this handle
+ * @param y the new y-coordinate of this handle
+ * @param bounds the bounding rectangle of the image
+ * @param viewWidth The bounding image view width used to know the crop overlay is at view edges.
+ * @param viewHeight The bounding image view height used to know the crop overlay is at view
+ * edges.
+ * @param parentView the parent View containing the image
+ * @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the
+ * image
+ * @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used
+ * @param aspectRatio the aspect ratio to maintain
+ */
+ public void move(
+ RectF rect,
+ float x,
+ float y,
+ RectF bounds,
+ int viewWidth,
+ int viewHeight,
+ float snapMargin,
+ boolean fixedAspectRatio,
+ float aspectRatio) {
+
+ // Adjust the coordinates for the finger position's offset (i.e. the
+ // distance from the initial touch to the precise handle location).
+ // We want to maintain the initial touch's distance to the pressed
+ // handle so that the crop window size does not "jump".
+ float adjX = x + mTouchOffset.x;
+ float adjY = y + mTouchOffset.y;
+
+ if (mType == Type.CENTER) {
+ moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
+ } else {
+ if (fixedAspectRatio) {
+ moveSizeWithFixedAspectRatio(
+ rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio);
+ } else {
+ moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
+ }
}
-
- //region: Private methods
-
- /**
- * Calculates the offset of the touch point from the precise location of the specified handle.
- * Save these values in a member variable since we want to maintain this offset as we drag the handle.
- */
- private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
-
- float touchOffsetX = 0;
- float touchOffsetY = 0;
-
- // Calculate the offset from the appropriate handle.
- switch (mType) {
- case TOP_LEFT:
- touchOffsetX = rect.left - touchX;
- touchOffsetY = rect.top - touchY;
- break;
- case TOP_RIGHT:
- touchOffsetX = rect.right - touchX;
- touchOffsetY = rect.top - touchY;
- break;
- case BOTTOM_LEFT:
- touchOffsetX = rect.left - touchX;
- touchOffsetY = rect.bottom - touchY;
- break;
- case BOTTOM_RIGHT:
- touchOffsetX = rect.right - touchX;
- touchOffsetY = rect.bottom - touchY;
- break;
- case LEFT:
- touchOffsetX = rect.left - touchX;
- touchOffsetY = 0;
- break;
- case TOP:
- touchOffsetX = 0;
- touchOffsetY = rect.top - touchY;
- break;
- case RIGHT:
- touchOffsetX = rect.right - touchX;
- touchOffsetY = 0;
- break;
- case BOTTOM:
- touchOffsetX = 0;
- touchOffsetY = rect.bottom - touchY;
- break;
- case CENTER:
- touchOffsetX = rect.centerX() - touchX;
- touchOffsetY = rect.centerY() - touchY;
- break;
- default:
- break;
- }
-
- mTouchOffset.x = touchOffsetX;
- mTouchOffset.y = touchOffsetY;
+ }
+
+ // region: Private methods
+
+ /**
+ * Calculates the offset of the touch point from the precise location of the specified handle.
+ * Save these values in a member variable since we want to maintain this offset as we drag the
+ * handle.
+ */
+ private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
+
+ float touchOffsetX = 0;
+ float touchOffsetY = 0;
+
+ // Calculate the offset from the appropriate handle.
+ switch (mType) {
+ case TOP_LEFT:
+ touchOffsetX = rect.left - touchX;
+ touchOffsetY = rect.top - touchY;
+ break;
+ case TOP_RIGHT:
+ touchOffsetX = rect.right - touchX;
+ touchOffsetY = rect.top - touchY;
+ break;
+ case BOTTOM_LEFT:
+ touchOffsetX = rect.left - touchX;
+ touchOffsetY = rect.bottom - touchY;
+ break;
+ case BOTTOM_RIGHT:
+ touchOffsetX = rect.right - touchX;
+ touchOffsetY = rect.bottom - touchY;
+ break;
+ case LEFT:
+ touchOffsetX = rect.left - touchX;
+ touchOffsetY = 0;
+ break;
+ case TOP:
+ touchOffsetX = 0;
+ touchOffsetY = rect.top - touchY;
+ break;
+ case RIGHT:
+ touchOffsetX = rect.right - touchX;
+ touchOffsetY = 0;
+ break;
+ case BOTTOM:
+ touchOffsetX = 0;
+ touchOffsetY = rect.bottom - touchY;
+ break;
+ case CENTER:
+ touchOffsetX = rect.centerX() - touchX;
+ touchOffsetY = rect.centerY() - touchY;
+ break;
+ default:
+ break;
}
- /**
- * Center move only changes the position of the crop window without changing the size.
- */
- private void moveCenter(RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
- float dx = x - rect.centerX();
- float dy = y - rect.centerY();
- if (rect.left + dx < 0 || rect.right + dx > viewWidth || rect.left + dx < bounds.left || rect.right + dx > bounds.right) {
- dx /= 1.05f;
- mTouchOffset.x -= dx / 2;
- }
- if (rect.top + dy < 0 || rect.bottom + dy > viewHeight || rect.top + dy < bounds.top || rect.bottom + dy > bounds.bottom) {
- dy /= 1.05f;
- mTouchOffset.y -= dy / 2;
- }
- rect.offset(dx, dy);
- snapEdgesToBounds(rect, bounds, snapRadius);
- }
-
- /**
- * Change the size of the crop window on the required edge (or edges for corner size move) without
- * affecting "secondary" edges.
- * Only the primary edge(s) are fixed to stay within limits.
- */
- private void moveSizeWithFreeAspectRatio(RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
- switch (mType) {
- case TOP_LEFT:
- adjustTop(rect, y, bounds, snapMargin, 0, false, false);
- adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
- break;
- case TOP_RIGHT:
- adjustTop(rect, y, bounds, snapMargin, 0, false, false);
- adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
- break;
- case BOTTOM_LEFT:
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
- adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
- break;
- case BOTTOM_RIGHT:
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
- adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
- break;
- case LEFT:
- adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
- break;
- case TOP:
- adjustTop(rect, y, bounds, snapMargin, 0, false, false);
- break;
- case RIGHT:
- adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
- break;
- case BOTTOM:
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
- break;
- default:
- break;
- }
+ mTouchOffset.x = touchOffsetX;
+ mTouchOffset.y = touchOffsetY;
+ }
+
+ /** Center move only changes the position of the crop window without changing the size. */
+ private void moveCenter(
+ RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
+ float dx = x - rect.centerX();
+ float dy = y - rect.centerY();
+ if (rect.left + dx < 0
+ || rect.right + dx > viewWidth
+ || rect.left + dx < bounds.left
+ || rect.right + dx > bounds.right) {
+ dx /= 1.05f;
+ mTouchOffset.x -= dx / 2;
}
-
- /**
- * Change the size of the crop window on the required "primary" edge WITH affect to relevant "secondary"
- * edge via aspect ratio.
- * Example: change in the left edge (primary) will affect top and bottom edges (secondary) to preserve the
- * given aspect ratio.
- */
- private void moveSizeWithFixedAspectRatio(RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin, float aspectRatio) {
- switch (mType) {
- case TOP_LEFT:
- if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) {
- adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false);
- adjustLeftByAspectRatio(rect, aspectRatio);
- } else {
- adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false);
- adjustTopByAspectRatio(rect, aspectRatio);
- }
- break;
- case TOP_RIGHT:
- if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) {
- adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true);
- adjustRightByAspectRatio(rect, aspectRatio);
- } else {
- adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false);
- adjustTopByAspectRatio(rect, aspectRatio);
- }
- break;
- case BOTTOM_LEFT:
- if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) {
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false);
- adjustLeftByAspectRatio(rect, aspectRatio);
- } else {
- adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true);
- adjustBottomByAspectRatio(rect, aspectRatio);
- }
- break;
- case BOTTOM_RIGHT:
- if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) {
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true);
- adjustRightByAspectRatio(rect, aspectRatio);
- } else {
- adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true);
- adjustBottomByAspectRatio(rect, aspectRatio);
- }
- break;
- case LEFT:
- adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true);
- adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
- break;
- case TOP:
- adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true);
- adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
- break;
- case RIGHT:
- adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true);
- adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
- break;
- case BOTTOM:
- adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true);
- adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
- break;
- default:
- break;
- }
+ if (rect.top + dy < 0
+ || rect.bottom + dy > viewHeight
+ || rect.top + dy < bounds.top
+ || rect.bottom + dy > bounds.bottom) {
+ dy /= 1.05f;
+ mTouchOffset.y -= dy / 2;
}
-
- /**
- * Check if edges have gone out of bounds (including snap margin), and fix if needed.
- */
- private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
- if (edges.left < bounds.left + margin) {
- edges.offset(bounds.left - edges.left, 0);
- }
- if (edges.top < bounds.top + margin) {
- edges.offset(0, bounds.top - edges.top);
- }
- if (edges.right > bounds.right - margin) {
- edges.offset(bounds.right - edges.right, 0);
- }
- if (edges.bottom > bounds.bottom - margin) {
- edges.offset(0, bounds.bottom - edges.bottom);
- }
+ rect.offset(dx, dy);
+ snapEdgesToBounds(rect, bounds, snapRadius);
+ }
+
+ /**
+ * Change the size of the crop window on the required edge (or edges for corner size move) without
+ * affecting "secondary" edges.
+ * Only the primary edge(s) are fixed to stay within limits.
+ */
+ private void moveSizeWithFreeAspectRatio(
+ RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
+ switch (mType) {
+ case TOP_LEFT:
+ adjustTop(rect, y, bounds, snapMargin, 0, false, false);
+ adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
+ break;
+ case TOP_RIGHT:
+ adjustTop(rect, y, bounds, snapMargin, 0, false, false);
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
+ break;
+ case BOTTOM_LEFT:
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
+ adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
+ break;
+ case BOTTOM_RIGHT:
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
+ break;
+ case LEFT:
+ adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
+ break;
+ case TOP:
+ adjustTop(rect, y, bounds, snapMargin, 0, false, false);
+ break;
+ case RIGHT:
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
+ break;
+ case BOTTOM:
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
+ break;
+ default:
+ break;
}
-
- /**
- * Get the resulting x-position of the left edge of the crop window given
- * the handle's position and the image's bounding box and snap radius.
- *
- * @param left the position that the left edge is dragged to
- * @param bounds the bounding box of the image that is being cropped
- * @param snapMargin the snap distance to the image edge (in pixels)
- */
- private void adjustLeft(RectF rect, float left, RectF bounds, float snapMargin, float aspectRatio, boolean topMoves, boolean bottomMoves) {
-
- float newLeft = left;
-
- if (newLeft < 0) {
- newLeft /= 1.05f;
- mTouchOffset.x -= newLeft / 1.1f;
- }
-
- if (newLeft < bounds.left) {
- mTouchOffset.x -= (newLeft - bounds.left) / 2f;
- }
-
- if (newLeft - bounds.left < snapMargin) {
- newLeft = bounds.left;
- }
-
- // Checks if the window is too small horizontally
- if (rect.right - newLeft < mMinCropWidth) {
- newLeft = rect.right - mMinCropWidth;
- }
-
- // Checks if the window is too large horizontally
- if (rect.right - newLeft > mMaxCropWidth) {
- newLeft = rect.right - mMaxCropWidth;
- }
-
- if (newLeft - bounds.left < snapMargin) {
- newLeft = bounds.left;
- }
-
- // check vertical bounds if aspect ratio is in play
- if (aspectRatio > 0) {
- float newHeight = (rect.right - newLeft) / aspectRatio;
-
- // Checks if the window is too small vertically
- if (newHeight < mMinCropHeight) {
- newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio);
- newHeight = (rect.right - newLeft) / aspectRatio;
- }
-
- // Checks if the window is too large vertically
- if (newHeight > mMaxCropHeight) {
- newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio);
- newHeight = (rect.right - newLeft) / aspectRatio;
- }
-
- // if top AND bottom edge moves by aspect ratio check that it is within full height bounds
- if (topMoves && bottomMoves) {
- newLeft = Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio));
- } else {
- // if top edge moves by aspect ratio check that it is within bounds
- if (topMoves && rect.bottom - newHeight < bounds.top) {
- newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio);
- newHeight = (rect.right - newLeft) / aspectRatio;
- }
-
- // if bottom edge moves by aspect ratio check that it is within bounds
- if (bottomMoves && rect.top + newHeight > bounds.bottom) {
- newLeft = Math.max(newLeft, Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio));
- }
- }
- }
-
- rect.left = newLeft;
+ }
+
+ /**
+ * Change the size of the crop window on the required "primary" edge WITH affect to relevant
+ * "secondary" edge via aspect ratio.
+ * Example: change in the left edge (primary) will affect top and bottom edges (secondary) to
+ * preserve the given aspect ratio.
+ */
+ private void moveSizeWithFixedAspectRatio(
+ RectF rect,
+ float x,
+ float y,
+ RectF bounds,
+ int viewWidth,
+ int viewHeight,
+ float snapMargin,
+ float aspectRatio) {
+ switch (mType) {
+ case TOP_LEFT:
+ if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) {
+ adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false);
+ adjustLeftByAspectRatio(rect, aspectRatio);
+ } else {
+ adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false);
+ adjustTopByAspectRatio(rect, aspectRatio);
+ }
+ break;
+ case TOP_RIGHT:
+ if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) {
+ adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true);
+ adjustRightByAspectRatio(rect, aspectRatio);
+ } else {
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false);
+ adjustTopByAspectRatio(rect, aspectRatio);
+ }
+ break;
+ case BOTTOM_LEFT:
+ if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) {
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false);
+ adjustLeftByAspectRatio(rect, aspectRatio);
+ } else {
+ adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true);
+ adjustBottomByAspectRatio(rect, aspectRatio);
+ }
+ break;
+ case BOTTOM_RIGHT:
+ if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) {
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true);
+ adjustRightByAspectRatio(rect, aspectRatio);
+ } else {
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true);
+ adjustBottomByAspectRatio(rect, aspectRatio);
+ }
+ break;
+ case LEFT:
+ adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true);
+ adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
+ break;
+ case TOP:
+ adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true);
+ adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
+ break;
+ case RIGHT:
+ adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true);
+ adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
+ break;
+ case BOTTOM:
+ adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true);
+ adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
+ break;
+ default:
+ break;
}
+ }
- /**
- * Get the resulting x-position of the right edge of the crop window given
- * the handle's position and the image's bounding box and snap radius.
- *
- * @param right the position that the right edge is dragged to
- * @param bounds the bounding box of the image that is being cropped
- * @param viewWidth
- * @param snapMargin the snap distance to the image edge (in pixels)
- */
- private void adjustRight(RectF rect, float right, RectF bounds, int viewWidth, float snapMargin, float aspectRatio, boolean topMoves, boolean bottomMoves) {
-
- float newRight = right;
-
- if (newRight > viewWidth) {
- newRight = viewWidth + (newRight - viewWidth) / 1.05f;
- mTouchOffset.x -= (newRight - viewWidth) / 1.1f;
- }
-
- if (newRight > bounds.right) {
- mTouchOffset.x -= (newRight - bounds.right) / 2f;
- }
-
- // If close to the edge
- if (bounds.right - newRight < snapMargin) {
- newRight = bounds.right;
- }
-
- // Checks if the window is too small horizontally
- if (newRight - rect.left < mMinCropWidth) {
- newRight = rect.left + mMinCropWidth;
- }
-
- // Checks if the window is too large horizontally
- if (newRight - rect.left > mMaxCropWidth) {
- newRight = rect.left + mMaxCropWidth;
- }
-
- // If close to the edge
- if (bounds.right - newRight < snapMargin) {
- newRight = bounds.right;
- }
-
- // check vertical bounds if aspect ratio is in play
- if (aspectRatio > 0) {
- float newHeight = (newRight - rect.left) / aspectRatio;
-
- // Checks if the window is too small vertically
- if (newHeight < mMinCropHeight) {
- newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio);
- newHeight = (newRight - rect.left) / aspectRatio;
- }
-
- // Checks if the window is too large vertically
- if (newHeight > mMaxCropHeight) {
- newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio);
- newHeight = (newRight - rect.left) / aspectRatio;
- }
-
- // if top AND bottom edge moves by aspect ratio check that it is within full height bounds
- if (topMoves && bottomMoves) {
- newRight = Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio));
- } else {
- // if top edge moves by aspect ratio check that it is within bounds
- if (topMoves && rect.bottom - newHeight < bounds.top) {
- newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio);
- newHeight = (newRight - rect.left) / aspectRatio;
- }
-
- // if bottom edge moves by aspect ratio check that it is within bounds
- if (bottomMoves && rect.top + newHeight > bounds.bottom) {
- newRight = Math.min(newRight, Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio));
- }
- }
- }
+ /** Check if edges have gone out of bounds (including snap margin), and fix if needed. */
+ private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
+ if (edges.left < bounds.left + margin) {
+ edges.offset(bounds.left - edges.left, 0);
+ }
+ if (edges.top < bounds.top + margin) {
+ edges.offset(0, bounds.top - edges.top);
+ }
+ if (edges.right > bounds.right - margin) {
+ edges.offset(bounds.right - edges.right, 0);
+ }
+ if (edges.bottom > bounds.bottom - margin) {
+ edges.offset(0, bounds.bottom - edges.bottom);
+ }
+ }
+
+ /**
+ * Get the resulting x-position of the left edge of the crop window given the handle's position
+ * and the image's bounding box and snap radius.
+ *
+ * @param left the position that the left edge is dragged to
+ * @param bounds the bounding box of the image that is being cropped
+ * @param snapMargin the snap distance to the image edge (in pixels)
+ */
+ private void adjustLeft(
+ RectF rect,
+ float left,
+ RectF bounds,
+ float snapMargin,
+ float aspectRatio,
+ boolean topMoves,
+ boolean bottomMoves) {
+
+ float newLeft = left;
+
+ if (newLeft < 0) {
+ newLeft /= 1.05f;
+ mTouchOffset.x -= newLeft / 1.1f;
+ }
- rect.right = newRight;
+ if (newLeft < bounds.left) {
+ mTouchOffset.x -= (newLeft - bounds.left) / 2f;
}
- /**
- * Get the resulting y-position of the top edge of the crop window given the
- * handle's position and the image's bounding box and snap radius.
- *
- * @param top the x-position that the top edge is dragged to
- * @param bounds the bounding box of the image that is being cropped
- * @param snapMargin the snap distance to the image edge (in pixels)
- */
- private void adjustTop(RectF rect, float top, RectF bounds, float snapMargin, float aspectRatio, boolean leftMoves, boolean rightMoves) {
+ if (newLeft - bounds.left < snapMargin) {
+ newLeft = bounds.left;
+ }
- float newTop = top;
+ // Checks if the window is too small horizontally
+ if (rect.right - newLeft < mMinCropWidth) {
+ newLeft = rect.right - mMinCropWidth;
+ }
- if (newTop < 0) {
- newTop /= 1.05f;
- mTouchOffset.y -= newTop / 1.1f;
- }
+ // Checks if the window is too large horizontally
+ if (rect.right - newLeft > mMaxCropWidth) {
+ newLeft = rect.right - mMaxCropWidth;
+ }
- if (newTop < bounds.top) {
- mTouchOffset.y -= (newTop - bounds.top) / 2f;
- }
+ if (newLeft - bounds.left < snapMargin) {
+ newLeft = bounds.left;
+ }
- if (newTop - bounds.top < snapMargin) {
- newTop = bounds.top;
- }
+ // check vertical bounds if aspect ratio is in play
+ if (aspectRatio > 0) {
+ float newHeight = (rect.right - newLeft) / aspectRatio;
+
+ // Checks if the window is too small vertically
+ if (newHeight < mMinCropHeight) {
+ newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio);
+ newHeight = (rect.right - newLeft) / aspectRatio;
+ }
+
+ // Checks if the window is too large vertically
+ if (newHeight > mMaxCropHeight) {
+ newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio);
+ newHeight = (rect.right - newLeft) / aspectRatio;
+ }
+
+ // if top AND bottom edge moves by aspect ratio check that it is within full height bounds
+ if (topMoves && bottomMoves) {
+ newLeft =
+ Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio));
+ } else {
+ // if top edge moves by aspect ratio check that it is within bounds
+ if (topMoves && rect.bottom - newHeight < bounds.top) {
+ newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio);
+ newHeight = (rect.right - newLeft) / aspectRatio;
+ }
+
+ // if bottom edge moves by aspect ratio check that it is within bounds
+ if (bottomMoves && rect.top + newHeight > bounds.bottom) {
+ newLeft =
+ Math.max(
+ newLeft,
+ Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio));
+ }
+ }
+ }
- // Checks if the window is too small vertically
- if (rect.bottom - newTop < mMinCropHeight) {
- newTop = rect.bottom - mMinCropHeight;
- }
+ rect.left = newLeft;
+ }
+
+ /**
+ * Get the resulting x-position of the right edge of the crop window given the handle's position
+ * and the image's bounding box and snap radius.
+ *
+ * @param right the position that the right edge is dragged to
+ * @param bounds the bounding box of the image that is being cropped
+ * @param viewWidth
+ * @param snapMargin the snap distance to the image edge (in pixels)
+ */
+ private void adjustRight(
+ RectF rect,
+ float right,
+ RectF bounds,
+ int viewWidth,
+ float snapMargin,
+ float aspectRatio,
+ boolean topMoves,
+ boolean bottomMoves) {
+
+ float newRight = right;
+
+ if (newRight > viewWidth) {
+ newRight = viewWidth + (newRight - viewWidth) / 1.05f;
+ mTouchOffset.x -= (newRight - viewWidth) / 1.1f;
+ }
- // Checks if the window is too large vertically
- if (rect.bottom - newTop > mMaxCropHeight) {
- newTop = rect.bottom - mMaxCropHeight;
- }
+ if (newRight > bounds.right) {
+ mTouchOffset.x -= (newRight - bounds.right) / 2f;
+ }
- if (newTop - bounds.top < snapMargin) {
- newTop = bounds.top;
- }
+ // If close to the edge
+ if (bounds.right - newRight < snapMargin) {
+ newRight = bounds.right;
+ }
- // check horizontal bounds if aspect ratio is in play
- if (aspectRatio > 0) {
- float newWidth = (rect.bottom - newTop) * aspectRatio;
-
- // Checks if the crop window is too small horizontally due to aspect ratio adjustment
- if (newWidth < mMinCropWidth) {
- newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio));
- newWidth = (rect.bottom - newTop) * aspectRatio;
- }
-
- // Checks if the crop window is too large horizontally due to aspect ratio adjustment
- if (newWidth > mMaxCropWidth) {
- newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio));
- newWidth = (rect.bottom - newTop) * aspectRatio;
- }
-
- // if left AND right edge moves by aspect ratio check that it is within full width bounds
- if (leftMoves && rightMoves) {
- newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio));
- } else {
- // if left edge moves by aspect ratio check that it is within bounds
- if (leftMoves && rect.right - newWidth < bounds.left) {
- newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio);
- newWidth = (rect.bottom - newTop) * aspectRatio;
- }
-
- // if right edge moves by aspect ratio check that it is within bounds
- if (rightMoves && rect.left + newWidth > bounds.right) {
- newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio));
- }
- }
- }
+ // Checks if the window is too small horizontally
+ if (newRight - rect.left < mMinCropWidth) {
+ newRight = rect.left + mMinCropWidth;
+ }
- rect.top = newTop;
+ // Checks if the window is too large horizontally
+ if (newRight - rect.left > mMaxCropWidth) {
+ newRight = rect.left + mMaxCropWidth;
}
- /**
- * Get the resulting y-position of the bottom edge of the crop window given
- * the handle's position and the image's bounding box and snap radius.
- *
- * @param bottom the position that the bottom edge is dragged to
- * @param bounds the bounding box of the image that is being cropped
- * @param viewHeight
- * @param snapMargin the snap distance to the image edge (in pixels)
- */
- private void adjustBottom(RectF rect, float bottom, RectF bounds, int viewHeight, float snapMargin, float aspectRatio, boolean leftMoves, boolean rightMoves) {
+ // If close to the edge
+ if (bounds.right - newRight < snapMargin) {
+ newRight = bounds.right;
+ }
- float newBottom = bottom;
+ // check vertical bounds if aspect ratio is in play
+ if (aspectRatio > 0) {
+ float newHeight = (newRight - rect.left) / aspectRatio;
+
+ // Checks if the window is too small vertically
+ if (newHeight < mMinCropHeight) {
+ newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio);
+ newHeight = (newRight - rect.left) / aspectRatio;
+ }
+
+ // Checks if the window is too large vertically
+ if (newHeight > mMaxCropHeight) {
+ newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio);
+ newHeight = (newRight - rect.left) / aspectRatio;
+ }
+
+ // if top AND bottom edge moves by aspect ratio check that it is within full height bounds
+ if (topMoves && bottomMoves) {
+ newRight =
+ Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio));
+ } else {
+ // if top edge moves by aspect ratio check that it is within bounds
+ if (topMoves && rect.bottom - newHeight < bounds.top) {
+ newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio);
+ newHeight = (newRight - rect.left) / aspectRatio;
+ }
+
+ // if bottom edge moves by aspect ratio check that it is within bounds
+ if (bottomMoves && rect.top + newHeight > bounds.bottom) {
+ newRight =
+ Math.min(
+ newRight,
+ Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio));
+ }
+ }
+ }
- if (newBottom > viewHeight) {
- newBottom = viewHeight + (newBottom - viewHeight) / 1.05f;
- mTouchOffset.y -= (newBottom - viewHeight) / 1.1f;
- }
+ rect.right = newRight;
+ }
+
+ /**
+ * Get the resulting y-position of the top edge of the crop window given the handle's position and
+ * the image's bounding box and snap radius.
+ *
+ * @param top the x-position that the top edge is dragged to
+ * @param bounds the bounding box of the image that is being cropped
+ * @param snapMargin the snap distance to the image edge (in pixels)
+ */
+ private void adjustTop(
+ RectF rect,
+ float top,
+ RectF bounds,
+ float snapMargin,
+ float aspectRatio,
+ boolean leftMoves,
+ boolean rightMoves) {
+
+ float newTop = top;
+
+ if (newTop < 0) {
+ newTop /= 1.05f;
+ mTouchOffset.y -= newTop / 1.1f;
+ }
- if (newBottom > bounds.bottom) {
- mTouchOffset.y -= (newBottom - bounds.bottom) / 2f;
- }
+ if (newTop < bounds.top) {
+ mTouchOffset.y -= (newTop - bounds.top) / 2f;
+ }
- if (bounds.bottom - newBottom < snapMargin) {
- newBottom = bounds.bottom;
- }
+ if (newTop - bounds.top < snapMargin) {
+ newTop = bounds.top;
+ }
- // Checks if the window is too small vertically
- if (newBottom - rect.top < mMinCropHeight) {
- newBottom = rect.top + mMinCropHeight;
- }
+ // Checks if the window is too small vertically
+ if (rect.bottom - newTop < mMinCropHeight) {
+ newTop = rect.bottom - mMinCropHeight;
+ }
- // Checks if the window is too small vertically
- if (newBottom - rect.top > mMaxCropHeight) {
- newBottom = rect.top + mMaxCropHeight;
- }
+ // Checks if the window is too large vertically
+ if (rect.bottom - newTop > mMaxCropHeight) {
+ newTop = rect.bottom - mMaxCropHeight;
+ }
- if (bounds.bottom - newBottom < snapMargin) {
- newBottom = bounds.bottom;
- }
+ if (newTop - bounds.top < snapMargin) {
+ newTop = bounds.top;
+ }
- // check horizontal bounds if aspect ratio is in play
- if (aspectRatio > 0) {
- float newWidth = (newBottom - rect.top) * aspectRatio;
-
- // Checks if the window is too small horizontally
- if (newWidth < mMinCropWidth) {
- newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio);
- newWidth = (newBottom - rect.top) * aspectRatio;
- }
-
- // Checks if the window is too large horizontally
- if (newWidth > mMaxCropWidth) {
- newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio);
- newWidth = (newBottom - rect.top) * aspectRatio;
- }
-
- // if left AND right edge moves by aspect ratio check that it is within full width bounds
- if (leftMoves && rightMoves) {
- newBottom = Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio));
- } else {
- // if left edge moves by aspect ratio check that it is within bounds
- if (leftMoves && rect.right - newWidth < bounds.left) {
- newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio);
- newWidth = (newBottom - rect.top) * aspectRatio;
- }
-
- // if right edge moves by aspect ratio check that it is within bounds
- if (rightMoves && rect.left + newWidth > bounds.right) {
- newBottom = Math.min(newBottom, Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio));
- }
- }
- }
+ // check horizontal bounds if aspect ratio is in play
+ if (aspectRatio > 0) {
+ float newWidth = (rect.bottom - newTop) * aspectRatio;
+
+ // Checks if the crop window is too small horizontally due to aspect ratio adjustment
+ if (newWidth < mMinCropWidth) {
+ newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio));
+ newWidth = (rect.bottom - newTop) * aspectRatio;
+ }
+
+ // Checks if the crop window is too large horizontally due to aspect ratio adjustment
+ if (newWidth > mMaxCropWidth) {
+ newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio));
+ newWidth = (rect.bottom - newTop) * aspectRatio;
+ }
+
+ // if left AND right edge moves by aspect ratio check that it is within full width bounds
+ if (leftMoves && rightMoves) {
+ newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio));
+ } else {
+ // if left edge moves by aspect ratio check that it is within bounds
+ if (leftMoves && rect.right - newWidth < bounds.left) {
+ newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio);
+ newWidth = (rect.bottom - newTop) * aspectRatio;
+ }
+
+ // if right edge moves by aspect ratio check that it is within bounds
+ if (rightMoves && rect.left + newWidth > bounds.right) {
+ newTop =
+ Math.max(
+ newTop,
+ Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio));
+ }
+ }
+ }
- rect.bottom = newBottom;
+ rect.top = newTop;
+ }
+
+ /**
+ * Get the resulting y-position of the bottom edge of the crop window given the handle's position
+ * and the image's bounding box and snap radius.
+ *
+ * @param bottom the position that the bottom edge is dragged to
+ * @param bounds the bounding box of the image that is being cropped
+ * @param viewHeight
+ * @param snapMargin the snap distance to the image edge (in pixels)
+ */
+ private void adjustBottom(
+ RectF rect,
+ float bottom,
+ RectF bounds,
+ int viewHeight,
+ float snapMargin,
+ float aspectRatio,
+ boolean leftMoves,
+ boolean rightMoves) {
+
+ float newBottom = bottom;
+
+ if (newBottom > viewHeight) {
+ newBottom = viewHeight + (newBottom - viewHeight) / 1.05f;
+ mTouchOffset.y -= (newBottom - viewHeight) / 1.1f;
}
- /**
- * Adjust left edge by current crop window height and the given aspect ratio,
- * the right edge remains in possition while the left adjusts to keep aspect ratio to the height.
- */
- private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) {
- rect.left = rect.right - rect.height() * aspectRatio;
+ if (newBottom > bounds.bottom) {
+ mTouchOffset.y -= (newBottom - bounds.bottom) / 2f;
}
- /**
- * Adjust top edge by current crop window width and the given aspect ratio,
- * the bottom edge remains in possition while the top adjusts to keep aspect ratio to the width.
- */
- private void adjustTopByAspectRatio(RectF rect, float aspectRatio) {
- rect.top = rect.bottom - rect.width() / aspectRatio;
+ if (bounds.bottom - newBottom < snapMargin) {
+ newBottom = bounds.bottom;
}
- /**
- * Adjust right edge by current crop window height and the given aspect ratio,
- * the left edge remains in possition while the left adjusts to keep aspect ratio to the height.
- */
- private void adjustRightByAspectRatio(RectF rect, float aspectRatio) {
- rect.right = rect.left + rect.height() * aspectRatio;
+ // Checks if the window is too small vertically
+ if (newBottom - rect.top < mMinCropHeight) {
+ newBottom = rect.top + mMinCropHeight;
}
- /**
- * Adjust bottom edge by current crop window width and the given aspect ratio,
- * the top edge remains in possition while the top adjusts to keep aspect ratio to the width.
- */
- private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) {
- rect.bottom = rect.top + rect.width() / aspectRatio;
+ // Checks if the window is too small vertically
+ if (newBottom - rect.top > mMaxCropHeight) {
+ newBottom = rect.top + mMaxCropHeight;
}
- /**
- * Adjust left and right edges by current crop window height and the given aspect ratio,
- * both right and left edges adjusts equally relative to center to keep aspect ratio to the height.
- */
- private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
- rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0);
- if (rect.left < bounds.left) {
- rect.offset(bounds.left - rect.left, 0);
- }
- if (rect.right > bounds.right) {
- rect.offset(bounds.right - rect.right, 0);
- }
+ if (bounds.bottom - newBottom < snapMargin) {
+ newBottom = bounds.bottom;
}
- /**
- * Adjust top and bottom edges by current crop window width and the given aspect ratio,
- * both top and bottom edges adjusts equally relative to center to keep aspect ratio to the width.
- */
- private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
- rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2);
- if (rect.top < bounds.top) {
- rect.offset(0, bounds.top - rect.top);
- }
- if (rect.bottom > bounds.bottom) {
- rect.offset(0, bounds.bottom - rect.bottom);
- }
+ // check horizontal bounds if aspect ratio is in play
+ if (aspectRatio > 0) {
+ float newWidth = (newBottom - rect.top) * aspectRatio;
+
+ // Checks if the window is too small horizontally
+ if (newWidth < mMinCropWidth) {
+ newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio);
+ newWidth = (newBottom - rect.top) * aspectRatio;
+ }
+
+ // Checks if the window is too large horizontally
+ if (newWidth > mMaxCropWidth) {
+ newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio);
+ newWidth = (newBottom - rect.top) * aspectRatio;
+ }
+
+ // if left AND right edge moves by aspect ratio check that it is within full width bounds
+ if (leftMoves && rightMoves) {
+ newBottom =
+ Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio));
+ } else {
+ // if left edge moves by aspect ratio check that it is within bounds
+ if (leftMoves && rect.right - newWidth < bounds.left) {
+ newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio);
+ newWidth = (newBottom - rect.top) * aspectRatio;
+ }
+
+ // if right edge moves by aspect ratio check that it is within bounds
+ if (rightMoves && rect.left + newWidth > bounds.right) {
+ newBottom =
+ Math.min(
+ newBottom,
+ Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio));
+ }
+ }
}
- /**
- * Calculates the aspect ratio given a rectangle.
- */
- private static float calculateAspectRatio(float left, float top, float right, float bottom) {
- return (right - left) / (bottom - top);
- }
- //endregion
-
- //region: Inner class: Type
-
- /**
- * The type of crop window move that is handled.
- */
- public enum Type {
- TOP_LEFT,
- TOP_RIGHT,
- BOTTOM_LEFT,
- BOTTOM_RIGHT,
- LEFT,
- TOP,
- RIGHT,
- BOTTOM,
- CENTER
- }
- //endregion
-}
\ No newline at end of file
+ rect.bottom = newBottom;
+ }
+
+ /**
+ * Adjust left edge by current crop window height and the given aspect ratio, the right edge
+ * remains in possition while the left adjusts to keep aspect ratio to the height.
+ */
+ private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) {
+ rect.left = rect.right - rect.height() * aspectRatio;
+ }
+
+ /**
+ * Adjust top edge by current crop window width and the given aspect ratio, the bottom edge
+ * remains in possition while the top adjusts to keep aspect ratio to the width.
+ */
+ private void adjustTopByAspectRatio(RectF rect, float aspectRatio) {
+ rect.top = rect.bottom - rect.width() / aspectRatio;
+ }
+
+ /**
+ * Adjust right edge by current crop window height and the given aspect ratio, the left edge
+ * remains in possition while the left adjusts to keep aspect ratio to the height.
+ */
+ private void adjustRightByAspectRatio(RectF rect, float aspectRatio) {
+ rect.right = rect.left + rect.height() * aspectRatio;
+ }
+
+ /**
+ * Adjust bottom edge by current crop window width and the given aspect ratio, the top edge
+ * remains in possition while the top adjusts to keep aspect ratio to the width.
+ */
+ private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) {
+ rect.bottom = rect.top + rect.width() / aspectRatio;
+ }
+
+ /**
+ * Adjust left and right edges by current crop window height and the given aspect ratio, both
+ * right and left edges adjusts equally relative to center to keep aspect ratio to the height.
+ */
+ private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
+ rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0);
+ if (rect.left < bounds.left) {
+ rect.offset(bounds.left - rect.left, 0);
+ }
+ if (rect.right > bounds.right) {
+ rect.offset(bounds.right - rect.right, 0);
+ }
+ }
+
+ /**
+ * Adjust top and bottom edges by current crop window width and the given aspect ratio, both top
+ * and bottom edges adjusts equally relative to center to keep aspect ratio to the width.
+ */
+ private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
+ rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2);
+ if (rect.top < bounds.top) {
+ rect.offset(0, bounds.top - rect.top);
+ }
+ if (rect.bottom > bounds.bottom) {
+ rect.offset(0, bounds.bottom - rect.bottom);
+ }
+ }
+
+ /** Calculates the aspect ratio given a rectangle. */
+ private static float calculateAspectRatio(float left, float top, float right, float bottom) {
+ return (right - left) / (bottom - top);
+ }
+ // endregion
+
+ // region: Inner class: Type
+
+ /** The type of crop window move that is handled. */
+ public enum Type {
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT,
+ LEFT,
+ TOP,
+ RIGHT,
+ BOTTOM,
+ CENTER
+ }
+ // endregion
+}
diff --git a/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..133395df
Binary files /dev/null and b/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..79910ffe
Binary files /dev/null and b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..3629e38d
Binary files /dev/null and b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..4200cb86
Binary files /dev/null and b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/menu/crop_image_menu.xml b/cropper/src/main/res/menu/crop_image_menu.xml
index 5ef0ba25..a95ca970 100644
--- a/cropper/src/main/res/menu/crop_image_menu.xml
+++ b/cropper/src/main/res/menu/crop_image_menu.xml
@@ -13,6 +13,20 @@
android:icon="@drawable/crop_image_menu_rotate_right"
android:title="@string/crop_image_menu_rotate_right"
app:showAsAction="ifRoom"/>
+
+
+
+
+
+
+ أدر عكس اتجاه عقارب الساعة
+ أدر
+ قُصّ
+ اقلب
+ اقلب أفقيًا
+ اقلب رأسيًا
+
+ اختر مصدرًا
+
+ إلغاء؛ الأذونات المطلوبة غير ممنوحة
+
+
diff --git a/cropper/src/main/res/values-cs/strings.xml b/cropper/src/main/res/values-cs/strings.xml
new file mode 100644
index 00000000..a8cef3c9
--- /dev/null
+++ b/cropper/src/main/res/values-cs/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Otočit proti směru hodinových ručiček
+ Otočit
+ Oříznout
+ Překlopit
+ Překlopit vodorovně
+ Překlopit svisle
+
+ Vybrat zdroj
+
+ Probíhá storno, požadovaná povolení nejsou udělena
+
+
diff --git a/cropper/src/main/res/values-de/strings.xml b/cropper/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..1ef0f3d7
--- /dev/null
+++ b/cropper/src/main/res/values-de/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ gegen den Uhrzeigersinn drehen
+ drehen
+ zuschneiden
+ spiegeln
+ horizontal spiegeln
+ vertikal spiegeln
+
+ Quelle wählen
+
+ Vorgang wird abgebrochen, benötigte Berechtigungen wurden nicht erteilt.
+
+
diff --git a/cropper/src/main/res/values-es-rGT/strings.xml b/cropper/src/main/res/values-es-rGT/strings.xml
new file mode 100644
index 00000000..c9b0864f
--- /dev/null
+++ b/cropper/src/main/res/values-es-rGT/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Girar a la izquierda
+ Girar a la derecha
+ Cortar
+ Dar la vuelta
+ Voltear horizontalmente
+ Voltear verticalmente
+ Seleccionar fuente
+ Cancelando, los permisos requeridos no se otorgaron
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-es/strings.xml b/cropper/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..a14c240a
--- /dev/null
+++ b/cropper/src/main/res/values-es/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Rotar a la izquierda
+ Rotar a la derecha
+ Cortar
+ Dar la vuelta
+ Voltear horizontalmente
+ Voltear verticalmente
+ Seleccionar fuente
+ Cancelando, los permisos requeridos no han sido otorgados
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-fa/strings.xml b/cropper/src/main/res/values-fa/strings.xml
new file mode 100644
index 00000000..b6745743
--- /dev/null
+++ b/cropper/src/main/res/values-fa/strings.xml
@@ -0,0 +1,11 @@
+
+
+ چرخش در جهت عقربه های ساعت
+ چرخش
+ بریدن (کراپ)
+ آیینه کردن
+ آیینه کردن به صورت افقی
+ آیینه کردن به صورت عمودی
+ منبع را انتخاب کنید
+ لغو، مجوزهای مورد نیاز ارائه نشده
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-fr/strings.xml b/cropper/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..b0ec3bd0
--- /dev/null
+++ b/cropper/src/main/res/values-fr/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Pivoter à gauche
+ Pivoter à droite
+ Redimensionner
+ Retourner
+ Retourner horizontalement
+ Retourner verticalement
+ Sélectionner la source
+ Annulation, il manque des permissions requises
+
diff --git a/cropper/src/main/res/values-he/strings.xml b/cropper/src/main/res/values-he/strings.xml
new file mode 100644
index 00000000..8a781d36
--- /dev/null
+++ b/cropper/src/main/res/values-he/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ סובב נגד כיוון השעון
+ סובב
+ חתוך
+ הפוך
+ הפוך אופקית
+ הפוך אנכית
+
+ בחר מקור
+
+ ההרשאות הנדרשות חסרות, מבטל
+
+
diff --git a/cropper/src/main/res/values-hi/strings.xml b/cropper/src/main/res/values-hi/strings.xml
new file mode 100644
index 00000000..8549a125
--- /dev/null
+++ b/cropper/src/main/res/values-hi/strings.xml
@@ -0,0 +1,11 @@
+
+
+ घड़ी की सुई के विपरीत दिशा में घुमाइए
+ घुमाएँ
+ फ़सल
+ फ्लिप
+ क्षैतिज फ्लिप
+ लंबवत फ्लिप करें
+ सोर्स चुनें
+ रद्द करना, आवश्यक अनुमतियां नहीं दी गई हैं
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-id/strings.xml b/cropper/src/main/res/values-id/strings.xml
new file mode 100644
index 00000000..5d0570ea
--- /dev/null
+++ b/cropper/src/main/res/values-id/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar berlawanan arah jarum jam
+ Putar
+ Potong
+ Balik
+ Balik secara horizontal
+ Balik secara vertikal
+ Pilih sumber
+ Membatalkan, tidak mendapatkan izin yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-in/strings.xml b/cropper/src/main/res/values-in/strings.xml
new file mode 100644
index 00000000..5d0570ea
--- /dev/null
+++ b/cropper/src/main/res/values-in/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar berlawanan arah jarum jam
+ Putar
+ Potong
+ Balik
+ Balik secara horizontal
+ Balik secara vertikal
+ Pilih sumber
+ Membatalkan, tidak mendapatkan izin yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-it/strings.xml b/cropper/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..fa266660
--- /dev/null
+++ b/cropper/src/main/res/values-it/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Ruota in senso antiorario
+ Ruota
+ Ritaglia
+ Capovolgi
+ Capovolgi orizzontalmente
+ Capovolgi verticalmente
+
+ Seleziona origine
+
+ Annullamento in corso, autorizzazione richieste non concesse
+
+
diff --git a/cropper/src/main/res/values-ja/strings.xml b/cropper/src/main/res/values-ja/strings.xml
new file mode 100644
index 00000000..4ab0ca56
--- /dev/null
+++ b/cropper/src/main/res/values-ja/strings.xml
@@ -0,0 +1,11 @@
+
+
+ 左回転
+ 右回転
+ 切り取り
+ 反転
+ 左右反転
+ 上下反転
+ 画像を選択
+ 必要な権限がありません、キャンセルしています。
+
diff --git a/cropper/src/main/res/values-ko/strings.xml b/cropper/src/main/res/values-ko/strings.xml
new file mode 100644
index 00000000..a33967bd
--- /dev/null
+++ b/cropper/src/main/res/values-ko/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 반시계 회전
+ 회전
+ 자르기
+ 반전
+ 좌우반전
+ 상하반전
+
+ 이미지 선택
+
+ 필수 권한이 없어서 취소합니다.
+
+
diff --git a/cropper/src/main/res/values-ms/strings.xml b/cropper/src/main/res/values-ms/strings.xml
new file mode 100644
index 00000000..000ac2eb
--- /dev/null
+++ b/cropper/src/main/res/values-ms/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar arah berlawanan jam
+ Putar
+ Potong
+ Flip
+ Flip melintang
+ Flip menegak
+ Pilih sumber
+ Membatal, tidak mendapat kebenaran yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-nb/strings.xml b/cropper/src/main/res/values-nb/strings.xml
new file mode 100644
index 00000000..c177d252
--- /dev/null
+++ b/cropper/src/main/res/values-nb/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Roter teller med urviseren
+ Roter
+ Beskjær
+ Vend
+ Vend vannrett
+ Vend loddrett
+
+ Velg kilde
+
+ Avbryter, nødvendige tillatelser er ikke gitt
+
+
diff --git a/cropper/src/main/res/values-nl/strings.xml b/cropper/src/main/res/values-nl/strings.xml
new file mode 100644
index 00000000..6b25e03b
--- /dev/null
+++ b/cropper/src/main/res/values-nl/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Tegen de klok in draaien
+ Draaien
+ Bijsnijden
+ Spiegelen
+ Horizontaal spiegelen
+ Verticaal spiegelen
+
+ Bron selecteren
+
+ Wordt geannuleerd, vereiste machtigingen zijn niet toegekend
+
+
diff --git a/cropper/src/main/res/values-pl/strings.xml b/cropper/src/main/res/values-pl/strings.xml
new file mode 100644
index 00000000..9db22a14
--- /dev/null
+++ b/cropper/src/main/res/values-pl/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Obróć w lewo
+ Obróć
+ Przytnij
+ Odbij
+ Odbij poziomo
+ Odbij pionowo
+
+ Wybierz źródło
+
+ Przerywaniem, potrzebne uprawnienia nie zostały nadane
+
+
diff --git a/cropper/src/main/res/values-pt-rBR/strings.xml b/cropper/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..e60b8bfb
--- /dev/null
+++ b/cropper/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ Girar para a esquerda
+ Girar para a direita
+ Cortar
+ Espelhar
+ Espelhar na horizontal
+ Espelhar na vertifcal
+
+ Escolher foto a partir de
+
+
diff --git a/cropper/src/main/res/values-ru-rRU/strings.xml b/cropper/src/main/res/values-ru-rRU/strings.xml
new file mode 100644
index 00000000..cd8e62d9
--- /dev/null
+++ b/cropper/src/main/res/values-ru-rRU/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Повернуть налево
+ Повернуть направо
+ Обрезать
+ Отразить
+ Отразить по горизонтали
+ Отразить по вертикали
+ Выбрать источник
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-sv/strings.xml b/cropper/src/main/res/values-sv/strings.xml
new file mode 100644
index 00000000..ce8beb96
--- /dev/null
+++ b/cropper/src/main/res/values-sv/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Rotera vänster
+ Rotera höger
+ Beskär
+ Vänd
+ Vänd horisontellt
+ Vänd vertikalt
+
+ Välj bild
+
+ Avbryter, nödvändiga behörigheter beviljas inte
+
+
diff --git a/cropper/src/main/res/values-tr/strings.xml b/cropper/src/main/res/values-tr/strings.xml
new file mode 100644
index 00000000..ba0f6f4e
--- /dev/null
+++ b/cropper/src/main/res/values-tr/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Saat yönünde döndür
+ döndürmek
+ ekin
+ fiske
+ Yatay olarak çevir
+ Dikey olarak çevir
+ Kaynağı seçin
+ İptal ediliyor, gerekli izinler verilmiyor
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-ur/strings.xml b/cropper/src/main/res/values-ur/strings.xml
new file mode 100644
index 00000000..b62a923f
--- /dev/null
+++ b/cropper/src/main/res/values-ur/strings.xml
@@ -0,0 +1,12 @@
+
+
+ گھڑی وار گھڑی گھومیں
+ گھمائیں
+ فصل
+ پلٹائیں
+ افقی پلٹائیں
+ عمودی طور پر پلٹائیں
+ ذریعہ منتخب کریں
+ منسوخ کرنا، ضروری اجازت نہیں دی جاتی ہیں
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-vi/strings.xml b/cropper/src/main/res/values-vi/strings.xml
new file mode 100644
index 00000000..d6301f41
--- /dev/null
+++ b/cropper/src/main/res/values-vi/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Xoay theo chiều kim đồng hồ
+ Xoay
+ Cắt
+ Lật
+ Lật theo chiều ngang
+ Lật theo chiều dọc
+
+ Chọn nguồn
+
+ Đang hủy, các quyền đã yêu cầu không được cấp
+
+
diff --git a/cropper/src/main/res/values-zh-rCN/strings.xml b/cropper/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..7ceb799a
--- /dev/null
+++ b/cropper/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆时针旋转
+ 旋转
+ 裁切
+ 翻转
+ 水平翻转
+ 垂直翻转
+
+ 选择来源
+
+ 取消中,未授予所需权限
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-zh-rTW/strings.xml b/cropper/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..269b9653
--- /dev/null
+++ b/cropper/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆時針旋轉
+ 旋轉
+ 裁切
+ 翻轉
+ 水平翻轉
+ 垂直翻轉
+
+ 選擇來源
+
+ 取消中,未授予所需權限
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-zh/strings.xml b/cropper/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000..b197f48f
--- /dev/null
+++ b/cropper/src/main/res/values-zh/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆时针旋转
+ 旋转
+ 裁剪
+ 翻转
+ 水平翻转
+ 垂直翻转
+
+ 选择来源
+
+ 正在取消,该操作未获得所需权限。
+
+
diff --git a/cropper/src/main/res/values/attrs.xml b/cropper/src/main/res/values/attrs.xml
index d1622b03..41579fb7 100644
--- a/cropper/src/main/res/values/attrs.xml
+++ b/cropper/src/main/res/values/attrs.xml
@@ -34,6 +34,7 @@
+
@@ -42,6 +43,8 @@
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values/ids.xml b/cropper/src/main/res/values/ids.xml
deleted file mode 100644
index 26fc5d40..00000000
--- a/cropper/src/main/res/values/ids.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/cropper/src/main/res/values/strings.xml b/cropper/src/main/res/values/strings.xml
index be74a6ff..6dc1fd22 100644
--- a/cropper/src/main/res/values/strings.xml
+++ b/cropper/src/main/res/values/strings.xml
@@ -5,6 +5,12 @@
Rotate counter clockwiseRotateCrop
+ Flip
+ Flip horizontally
+ Flip vertically
+
Select source
+ Cancelling, required permissions are not granted
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..8103ea76
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,12 @@
+#
+# Copyright (c) 2018. DNA Software. All rights reserved.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+android.enableJetifier=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e6dd0a25..a9559919 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Aug 17 18:17:00 IDT 2016
+#Fri Apr 06 15:20:13 IDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
diff --git a/quick-start/build.gradle b/quick-start/build.gradle
index f7474c6d..0f037fe7 100644
--- a/quick-start/build.gradle
+++ b/quick-start/build.gradle
@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 24
- buildToolsVersion '24.0.1'
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
defaultConfig {
minSdkVersion 14
- targetSdkVersion 24
+ targetSdkVersion rootProject.compileSdkVersion
versionCode 1
versionName '1.0'
}
@@ -16,6 +16,6 @@ android {
}
dependencies {
- compile project(':cropper')
- compile 'com.android.support:appcompat-v7:24.1.1'
+ api project(':cropper')
+ api "androidx.appcompat:appcompat:$androidXLibraryVersion"
}
diff --git a/quick-start/src/main/AndroidManifest.xml b/quick-start/src/main/AndroidManifest.xml
index c8fbab2b..2f109e5b 100644
--- a/quick-start/src/main/AndroidManifest.xml
+++ b/quick-start/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
xmlns:android="http://schemas.android.com/apk/res/android">
+ = 23 we need to check specifically that we have permissions to read external storage.
- if (CropImage.isReadExternalStoragePermissionsRequired(this, imageUri)) {
- // request permissions and handle the result in onRequestPermissionsResult()
- mCropImageUri = imageUri;
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
- } else {
- // no permissions required or already grunted, can start crop image activity
- startCropImageActivity(imageUri);
- }
- }
-
- // handle result of CropImageActivity
- if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
- CropImage.ActivityResult result = CropImage.getActivityResult(data);
- if (resultCode == RESULT_OK) {
- ((ImageView) findViewById(R.id.quick_start_cropped_image)).setImageURI(result.getUri());
- Toast.makeText(this, "Cropping successful, Sample: " + result.getSampleSize(), Toast.LENGTH_LONG).show();
- } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
- Toast.makeText(this, "Cropping failed: " + result.getError(), Toast.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
- if (mCropImageUri != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // required permissions granted, start crop image activity
- startCropImageActivity(mCropImageUri);
- } else {
- Toast.makeText(this, "Cancelling, required permissions are not granted", Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Start crop image activity for the given image.
- */
- private void startCropImageActivity(Uri imageUri) {
- CropImage.activity(imageUri)
- .setGuidelines(CropImageView.Guidelines.ON)
- .setMultiTouchEnabled(true)
- .start(this);
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+
+ /** Start pick image activity with chooser. */
+ public void onSelectImageClick(View view) {
+ CropImage.activity()
+ .setGuidelines(CropImageView.Guidelines.ON)
+ .setActivityTitle("My Crop")
+ .setCropShape(CropImageView.CropShape.OVAL)
+ .setCropMenuCropButtonTitle("Done")
+ .setRequestedSize(400, 400)
+ .setCropMenuCropButtonIcon(R.drawable.ic_launcher)
+ .start(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ // handle result of CropImageActivity
+ if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+ CropImage.ActivityResult result = CropImage.getActivityResult(data);
+ if (resultCode == RESULT_OK) {
+ ((ImageView) findViewById(R.id.quick_start_cropped_image)).setImageURI(result.getUri());
+ Toast.makeText(
+ this, "Cropping successful, Sample: " + result.getSampleSize(), Toast.LENGTH_LONG)
+ .show();
+ } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
+ Toast.makeText(this, "Cropping failed: " + result.getError(), Toast.LENGTH_LONG).show();
+ }
}
+ }
}
diff --git a/sample/build.gradle b/sample/build.gradle
index f7474c6d..0f037fe7 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 24
- buildToolsVersion '24.0.1'
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
defaultConfig {
minSdkVersion 14
- targetSdkVersion 24
+ targetSdkVersion rootProject.compileSdkVersion
versionCode 1
versionName '1.0'
}
@@ -16,6 +16,6 @@ android {
}
dependencies {
- compile project(':cropper')
- compile 'com.android.support:appcompat-v7:24.1.1'
+ api project(':cropper')
+ api "androidx.appcompat:appcompat:$androidXLibraryVersion"
}
diff --git a/sample/project.properties b/sample/project.properties
index 5f3ae42b..2e6e4744 100644
--- a/sample/project.properties
+++ b/sample/project.properties
@@ -9,7 +9,6 @@
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
-
# Project target.
target=android-17
android.library.reference.1=../cropper
diff --git a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropDemoPreset.java b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropDemoPreset.java
index 8b3e5cbf..d40481c2 100644
--- a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropDemoPreset.java
+++ b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropDemoPreset.java
@@ -12,14 +12,11 @@
package com.theartofdev.edmodo.cropper.sample;
-/**
- * Created by Arthu on 24-03-16.
- */
enum CropDemoPreset {
- RECT,
- CIRCULAR,
- CUSTOMIZED_OVERLAY,
- MIN_MAX_OVERRIDE,
- SCALE_CENTER_INSIDE,
- CUSTOM
+ RECT,
+ CIRCULAR,
+ CUSTOMIZED_OVERLAY,
+ MIN_MAX_OVERRIDE,
+ SCALE_CENTER_INSIDE,
+ CUSTOM
}
diff --git a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropImageViewOptions.java b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropImageViewOptions.java
index ae27807a..97a71cd9 100644
--- a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropImageViewOptions.java
+++ b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropImageViewOptions.java
@@ -16,28 +16,30 @@
import com.theartofdev.edmodo.cropper.CropImageView;
-/**
- * The crop image view options that can be changed live.
- */
+/** The crop image view options that can be changed live. */
final class CropImageViewOptions {
- public CropImageView.ScaleType scaleType = CropImageView.ScaleType.CENTER_INSIDE;
+ public CropImageView.ScaleType scaleType = CropImageView.ScaleType.CENTER_INSIDE;
- public CropImageView.CropShape cropShape = CropImageView.CropShape.RECTANGLE;
+ public CropImageView.CropShape cropShape = CropImageView.CropShape.RECTANGLE;
- public CropImageView.Guidelines guidelines = CropImageView.Guidelines.ON_TOUCH;
+ public CropImageView.Guidelines guidelines = CropImageView.Guidelines.ON_TOUCH;
- public Pair aspectRatio = new Pair<>(1, 1);
+ public Pair aspectRatio = new Pair<>(1, 1);
- public boolean autoZoomEnabled;
+ public boolean autoZoomEnabled;
- public int maxZoomLevel;
+ public int maxZoomLevel;
- public boolean fixAspectRatio;
+ public boolean fixAspectRatio;
- public boolean multitouch;
+ public boolean multitouch;
- public boolean showCropOverlay;
+ public boolean showCropOverlay;
- public boolean showProgressBar;
+ public boolean showProgressBar;
+
+ public boolean flipHorizontally;
+
+ public boolean flipVertically;
}
diff --git a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropResultActivity.java b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropResultActivity.java
index 09b36c8b..b1235477 100644
--- a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropResultActivity.java
+++ b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/CropResultActivity.java
@@ -27,58 +27,67 @@
public final class CropResultActivity extends Activity {
- /**
- * The image to show in the activity.
- */
- static Bitmap mImage;
+ /** The image to show in the activity. */
+ static Bitmap mImage;
- private ImageView imageView;
+ private ImageView imageView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.activity_crop_result);
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.activity_crop_result);
- imageView = ((ImageView) findViewById(R.id.resultImageView));
- imageView.setBackgroundResource(R.drawable.backdrop);
+ imageView = ((ImageView) findViewById(R.id.resultImageView));
+ imageView.setBackgroundResource(R.drawable.backdrop);
- Intent intent = getIntent();
- if (mImage != null) {
- imageView.setImageBitmap(mImage);
- int sampleSize = intent.getIntExtra("SAMPLE_SIZE", 1);
- double ratio = ((int) (10 * mImage.getWidth() / (double) mImage.getHeight())) / 10d;
- int byteCount = 0;
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
- byteCount = mImage.getByteCount() / 1024;
- }
- String desc = "(" + mImage.getWidth() + ", " + mImage.getHeight() + "), Sample: " + sampleSize + ", Ratio: " + ratio + ", Bytes: " + byteCount + "K";
- ((TextView) findViewById(R.id.resultImageText)).setText(desc);
- } else {
- Uri imageUri = intent.getParcelableExtra("URI");
- if (imageUri != null) {
- imageView.setImageURI(imageUri);
- } else {
- Toast.makeText(this, "No image is set to show", Toast.LENGTH_LONG).show();
- }
- }
+ Intent intent = getIntent();
+ if (mImage != null) {
+ imageView.setImageBitmap(mImage);
+ int sampleSize = intent.getIntExtra("SAMPLE_SIZE", 1);
+ double ratio = ((int) (10 * mImage.getWidth() / (double) mImage.getHeight())) / 10d;
+ int byteCount = 0;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
+ byteCount = mImage.getByteCount() / 1024;
+ }
+ String desc =
+ "("
+ + mImage.getWidth()
+ + ", "
+ + mImage.getHeight()
+ + "), Sample: "
+ + sampleSize
+ + ", Ratio: "
+ + ratio
+ + ", Bytes: "
+ + byteCount
+ + "K";
+ ((TextView) findViewById(R.id.resultImageText)).setText(desc);
+ } else {
+ Uri imageUri = intent.getParcelableExtra("URI");
+ if (imageUri != null) {
+ imageView.setImageURI(imageUri);
+ } else {
+ Toast.makeText(this, "No image is set to show", Toast.LENGTH_LONG).show();
+ }
}
+ }
- @Override
- public void onBackPressed() {
- releaseBitmap();
- super.onBackPressed();
- }
+ @Override
+ public void onBackPressed() {
+ releaseBitmap();
+ super.onBackPressed();
+ }
- public void onImageViewClicked(View view) {
- releaseBitmap();
- finish();
- }
+ public void onImageViewClicked(View view) {
+ releaseBitmap();
+ finish();
+ }
- private void releaseBitmap() {
- if (mImage != null) {
- mImage.recycle();
- mImage = null;
- }
+ private void releaseBitmap() {
+ if (mImage != null) {
+ mImage.recycle();
+ mImage = null;
}
+ }
}
diff --git a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainActivity.java b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainActivity.java
index 7fc7b2f3..44d3f34d 100644
--- a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainActivity.java
+++ b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainActivity.java
@@ -18,10 +18,10 @@
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.ActionBarDrawerToggle;
+import androidx.appcompat.app.AppCompatActivity;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuInflater;
@@ -36,245 +36,306 @@
public class MainActivity extends AppCompatActivity {
- //region: Fields and Consts
+ // region: Fields and Consts
- DrawerLayout mDrawerLayout;
+ DrawerLayout mDrawerLayout;
- private ActionBarDrawerToggle mDrawerToggle;
+ private ActionBarDrawerToggle mDrawerToggle;
- private MainFragment mCurrentFragment;
+ private MainFragment mCurrentFragment;
- private Uri mCropImageUri;
+ private Uri mCropImageUri;
- private CropImageViewOptions mCropImageViewOptions = new CropImageViewOptions();
- //endregion
+ private CropImageViewOptions mCropImageViewOptions = new CropImageViewOptions();
+ // endregion
- public void setCurrentFragment(MainFragment fragment) {
- mCurrentFragment = fragment;
- }
+ public void setCurrentFragment(MainFragment fragment) {
+ mCurrentFragment = fragment;
+ }
- public void setCurrentOptions(CropImageViewOptions options) {
- mCropImageViewOptions = options;
- updateDrawerTogglesByOptions(options);
- }
+ public void setCurrentOptions(CropImageViewOptions options) {
+ mCropImageViewOptions = options;
+ updateDrawerTogglesByOptions(options);
+ }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setHomeButtonEnabled(true);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
- mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
- mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.main_drawer_open, R.string.main_drawer_close);
- mDrawerToggle.setDrawerIndicatorEnabled(true);
- mDrawerLayout.setDrawerListener(mDrawerToggle);
+ mDrawerToggle =
+ new ActionBarDrawerToggle(
+ this, mDrawerLayout, R.string.main_drawer_open, R.string.main_drawer_close);
+ mDrawerToggle.setDrawerIndicatorEnabled(true);
+ mDrawerLayout.setDrawerListener(mDrawerToggle);
- if (savedInstanceState == null) {
- setMainFragmentByPreset(CropDemoPreset.RECT);
- }
+ if (savedInstanceState == null) {
+ setMainFragmentByPreset(CropDemoPreset.RECT);
}
+ }
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- mDrawerToggle.syncState();
- mCurrentFragment.updateCurrentCropViewOptions();
- }
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mDrawerToggle.syncState();
+ mCurrentFragment.updateCurrentCropViewOptions();
+ }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main, menu);
- return true;
- }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return true;
+ }
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (mDrawerToggle.onOptionsItemSelected(item)) {
- return true;
- }
- if (mCurrentFragment != null && mCurrentFragment.onOptionsItemSelected(item)) {
- return true;
- }
- return super.onOptionsItemSelected(item);
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mDrawerToggle.onOptionsItemSelected(item)) {
+ return true;
}
+ if (mCurrentFragment != null && mCurrentFragment.onOptionsItemSelected(item)) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
- @Override
- @SuppressLint("NewApi")
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
+ @Override
+ @SuppressLint("NewApi")
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE && resultCode == AppCompatActivity.RESULT_OK) {
- Uri imageUri = CropImage.getPickImageResultUri(this, data);
+ if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE
+ && resultCode == AppCompatActivity.RESULT_OK) {
+ Uri imageUri = CropImage.getPickImageResultUri(this, data);
- // For API >= 23 we need to check specifically that we have permissions to read external storage,
- // but we don't know if we need to for the URI so the simplest is to try open the stream and see if we get error.
- boolean requirePermissions = false;
- if (CropImage.isReadExternalStoragePermissionsRequired(this, imageUri)) {
+ // For API >= 23 we need to check specifically that we have permissions to read external
+ // storage,
+ // but we don't know if we need to for the URI so the simplest is to try open the stream and
+ // see if we get error.
+ boolean requirePermissions = false;
+ if (CropImage.isReadExternalStoragePermissionsRequired(this, imageUri)) {
- // request permissions and handle the result in onRequestPermissionsResult()
- requirePermissions = true;
- mCropImageUri = imageUri;
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
- } else {
+ // request permissions and handle the result in onRequestPermissionsResult()
+ requirePermissions = true;
+ mCropImageUri = imageUri;
+ requestPermissions(
+ new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+ CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
+ } else {
- mCurrentFragment.setImageUri(imageUri);
- }
- }
+ mCurrentFragment.setImageUri(imageUri);
+ }
}
+ }
- @Override
- public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
- if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- CropImage.startPickImageActivity(this);
- } else {
- Toast.makeText(this, "Cancelling, required permissions are not granted", Toast.LENGTH_LONG).show();
- }
- }
- if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
- if (mCropImageUri != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- mCurrentFragment.setImageUri(mCropImageUri);
- } else {
- Toast.makeText(this, "Cancelling, required permissions are not granted", Toast.LENGTH_LONG).show();
- }
- }
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String permissions[], int[] grantResults) {
+ if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ CropImage.startPickImageActivity(this);
+ } else {
+ Toast.makeText(this, "Cancelling, required permissions are not granted", Toast.LENGTH_LONG)
+ .show();
+ }
}
-
- @SuppressLint("NewApi")
- public void onDrawerOptionClicked(View view) {
- switch (view.getId()) {
- case R.id.drawer_option_load:
- if (CropImage.isExplicitCameraPermissionRequired(this)) {
- requestPermissions(new String[]{Manifest.permission.CAMERA}, CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
- } else {
- CropImage.startPickImageActivity(this);
- }
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_oval:
- setMainFragmentByPreset(CropDemoPreset.CIRCULAR);
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_rect:
- setMainFragmentByPreset(CropDemoPreset.RECT);
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_customized_overlay:
- setMainFragmentByPreset(CropDemoPreset.CUSTOMIZED_OVERLAY);
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_min_max_override:
- setMainFragmentByPreset(CropDemoPreset.MIN_MAX_OVERRIDE);
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_scale_center:
- setMainFragmentByPreset(CropDemoPreset.SCALE_CENTER_INSIDE);
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_toggle_scale:
- mCropImageViewOptions.scaleType = mCropImageViewOptions.scaleType == CropImageView.ScaleType.FIT_CENTER
- ? CropImageView.ScaleType.CENTER_INSIDE : mCropImageViewOptions.scaleType == CropImageView.ScaleType.CENTER_INSIDE
- ? CropImageView.ScaleType.CENTER : mCropImageViewOptions.scaleType == CropImageView.ScaleType.CENTER
- ? CropImageView.ScaleType.CENTER_CROP : CropImageView.ScaleType.FIT_CENTER;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_shape:
- mCropImageViewOptions.cropShape = mCropImageViewOptions.cropShape == CropImageView.CropShape.RECTANGLE
- ? CropImageView.CropShape.OVAL : CropImageView.CropShape.RECTANGLE;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_guidelines:
- mCropImageViewOptions.guidelines = mCropImageViewOptions.guidelines == CropImageView.Guidelines.OFF
- ? CropImageView.Guidelines.ON : mCropImageViewOptions.guidelines == CropImageView.Guidelines.ON
- ? CropImageView.Guidelines.ON_TOUCH : CropImageView.Guidelines.OFF;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_aspect_ratio:
- if (!mCropImageViewOptions.fixAspectRatio) {
- mCropImageViewOptions.fixAspectRatio = true;
- mCropImageViewOptions.aspectRatio = new Pair<>(1, 1);
- } else {
- if (mCropImageViewOptions.aspectRatio.first == 1 && mCropImageViewOptions.aspectRatio.second == 1) {
- mCropImageViewOptions.aspectRatio = new Pair<>(4, 3);
- } else if (mCropImageViewOptions.aspectRatio.first == 4 && mCropImageViewOptions.aspectRatio.second == 3) {
- mCropImageViewOptions.aspectRatio = new Pair<>(16, 9);
- } else if (mCropImageViewOptions.aspectRatio.first == 16 && mCropImageViewOptions.aspectRatio.second == 9) {
- mCropImageViewOptions.aspectRatio = new Pair<>(9, 16);
- } else {
- mCropImageViewOptions.fixAspectRatio = false;
- }
- }
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_auto_zoom:
- mCropImageViewOptions.autoZoomEnabled = !mCropImageViewOptions.autoZoomEnabled;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_max_zoom:
- mCropImageViewOptions.maxZoomLevel = mCropImageViewOptions.maxZoomLevel == 4 ? 8
- : mCropImageViewOptions.maxZoomLevel == 8 ? 2 : 4;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_set_initial_crop_rect:
- mCurrentFragment.setInitialCropRect();
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_reset_crop_rect:
- mCurrentFragment.resetCropRect();
- mDrawerLayout.closeDrawers();
- break;
- case R.id.drawer_option_toggle_multitouch:
- mCropImageViewOptions.multitouch = !mCropImageViewOptions.multitouch;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_show_overlay:
- mCropImageViewOptions.showCropOverlay = !mCropImageViewOptions.showCropOverlay;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- case R.id.drawer_option_toggle_show_progress_bar:
- mCropImageViewOptions.showProgressBar = !mCropImageViewOptions.showProgressBar;
- mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
- updateDrawerTogglesByOptions(mCropImageViewOptions);
- break;
- default:
- Toast.makeText(this, "Unknown drawer option clicked", Toast.LENGTH_LONG).show();
- }
+ if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
+ if (mCropImageUri != null
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ mCurrentFragment.setImageUri(mCropImageUri);
+ } else {
+ Toast.makeText(this, "Cancelling, required permissions are not granted", Toast.LENGTH_LONG)
+ .show();
+ }
}
+ }
- private void setMainFragmentByPreset(CropDemoPreset demoPreset) {
- FragmentManager fragmentManager = getSupportFragmentManager();
- fragmentManager.beginTransaction()
- .replace(R.id.container, MainFragment.newInstance(demoPreset))
- .commit();
+ @SuppressLint("NewApi")
+ public void onDrawerOptionClicked(View view) {
+ switch (view.getId()) {
+ case R.id.drawer_option_load:
+ if (CropImage.isExplicitCameraPermissionRequired(this)) {
+ requestPermissions(
+ new String[] {Manifest.permission.CAMERA},
+ CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
+ } else {
+ CropImage.startPickImageActivity(this);
+ }
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_oval:
+ setMainFragmentByPreset(CropDemoPreset.CIRCULAR);
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_rect:
+ setMainFragmentByPreset(CropDemoPreset.RECT);
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_customized_overlay:
+ setMainFragmentByPreset(CropDemoPreset.CUSTOMIZED_OVERLAY);
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_min_max_override:
+ setMainFragmentByPreset(CropDemoPreset.MIN_MAX_OVERRIDE);
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_scale_center:
+ setMainFragmentByPreset(CropDemoPreset.SCALE_CENTER_INSIDE);
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_toggle_scale:
+ mCropImageViewOptions.scaleType =
+ mCropImageViewOptions.scaleType == CropImageView.ScaleType.FIT_CENTER
+ ? CropImageView.ScaleType.CENTER_INSIDE
+ : mCropImageViewOptions.scaleType == CropImageView.ScaleType.CENTER_INSIDE
+ ? CropImageView.ScaleType.CENTER
+ : mCropImageViewOptions.scaleType == CropImageView.ScaleType.CENTER
+ ? CropImageView.ScaleType.CENTER_CROP
+ : CropImageView.ScaleType.FIT_CENTER;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_shape:
+ mCropImageViewOptions.cropShape =
+ mCropImageViewOptions.cropShape == CropImageView.CropShape.RECTANGLE
+ ? CropImageView.CropShape.OVAL
+ : CropImageView.CropShape.RECTANGLE;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_guidelines:
+ mCropImageViewOptions.guidelines =
+ mCropImageViewOptions.guidelines == CropImageView.Guidelines.OFF
+ ? CropImageView.Guidelines.ON
+ : mCropImageViewOptions.guidelines == CropImageView.Guidelines.ON
+ ? CropImageView.Guidelines.ON_TOUCH
+ : CropImageView.Guidelines.OFF;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_aspect_ratio:
+ if (!mCropImageViewOptions.fixAspectRatio) {
+ mCropImageViewOptions.fixAspectRatio = true;
+ mCropImageViewOptions.aspectRatio = new Pair<>(1, 1);
+ } else {
+ if (mCropImageViewOptions.aspectRatio.first == 1
+ && mCropImageViewOptions.aspectRatio.second == 1) {
+ mCropImageViewOptions.aspectRatio = new Pair<>(4, 3);
+ } else if (mCropImageViewOptions.aspectRatio.first == 4
+ && mCropImageViewOptions.aspectRatio.second == 3) {
+ mCropImageViewOptions.aspectRatio = new Pair<>(16, 9);
+ } else if (mCropImageViewOptions.aspectRatio.first == 16
+ && mCropImageViewOptions.aspectRatio.second == 9) {
+ mCropImageViewOptions.aspectRatio = new Pair<>(9, 16);
+ } else {
+ mCropImageViewOptions.fixAspectRatio = false;
+ }
+ }
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_auto_zoom:
+ mCropImageViewOptions.autoZoomEnabled = !mCropImageViewOptions.autoZoomEnabled;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_max_zoom:
+ mCropImageViewOptions.maxZoomLevel =
+ mCropImageViewOptions.maxZoomLevel == 4
+ ? 8
+ : mCropImageViewOptions.maxZoomLevel == 8 ? 2 : 4;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_set_initial_crop_rect:
+ mCurrentFragment.setInitialCropRect();
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_reset_crop_rect:
+ mCurrentFragment.resetCropRect();
+ mDrawerLayout.closeDrawers();
+ break;
+ case R.id.drawer_option_toggle_multitouch:
+ mCropImageViewOptions.multitouch = !mCropImageViewOptions.multitouch;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_show_overlay:
+ mCropImageViewOptions.showCropOverlay = !mCropImageViewOptions.showCropOverlay;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ case R.id.drawer_option_toggle_show_progress_bar:
+ mCropImageViewOptions.showProgressBar = !mCropImageViewOptions.showProgressBar;
+ mCurrentFragment.setCropImageViewOptions(mCropImageViewOptions);
+ updateDrawerTogglesByOptions(mCropImageViewOptions);
+ break;
+ default:
+ Toast.makeText(this, "Unknown drawer option clicked", Toast.LENGTH_LONG).show();
}
+ }
- private void updateDrawerTogglesByOptions(CropImageViewOptions options) {
- ((TextView) findViewById(R.id.drawer_option_toggle_scale)).setText(getResources().getString(R.string.drawer_option_toggle_scale, options.scaleType.name()));
- ((TextView) findViewById(R.id.drawer_option_toggle_shape)).setText(getResources().getString(R.string.drawer_option_toggle_shape, options.cropShape.name()));
- ((TextView) findViewById(R.id.drawer_option_toggle_guidelines)).setText(getResources().getString(R.string.drawer_option_toggle_guidelines, options.guidelines.name()));
- ((TextView) findViewById(R.id.drawer_option_toggle_multitouch)).setText(getResources().getString(R.string.drawer_option_toggle_multitouch, Boolean.toString(options.multitouch)));
- ((TextView) findViewById(R.id.drawer_option_toggle_show_overlay)).setText(getResources().getString(R.string.drawer_option_toggle_show_overlay, Boolean.toString(options.showCropOverlay)));
- ((TextView) findViewById(R.id.drawer_option_toggle_show_progress_bar)).setText(getResources().getString(R.string.drawer_option_toggle_show_progress_bar, Boolean.toString(options.showProgressBar)));
+ private void setMainFragmentByPreset(CropDemoPreset demoPreset) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager
+ .beginTransaction()
+ .replace(R.id.container, MainFragment.newInstance(demoPreset))
+ .commit();
+ }
- String aspectRatio = "FREE";
- if (options.fixAspectRatio) {
- aspectRatio = options.aspectRatio.first + ":" + options.aspectRatio.second;
- }
- ((TextView) findViewById(R.id.drawer_option_toggle_aspect_ratio)).setText(getResources().getString(R.string.drawer_option_toggle_aspect_ratio, aspectRatio));
+ private void updateDrawerTogglesByOptions(CropImageViewOptions options) {
+ ((TextView) findViewById(R.id.drawer_option_toggle_scale))
+ .setText(
+ getResources()
+ .getString(R.string.drawer_option_toggle_scale, options.scaleType.name()));
+ ((TextView) findViewById(R.id.drawer_option_toggle_shape))
+ .setText(
+ getResources()
+ .getString(R.string.drawer_option_toggle_shape, options.cropShape.name()));
+ ((TextView) findViewById(R.id.drawer_option_toggle_guidelines))
+ .setText(
+ getResources()
+ .getString(R.string.drawer_option_toggle_guidelines, options.guidelines.name()));
+ ((TextView) findViewById(R.id.drawer_option_toggle_multitouch))
+ .setText(
+ getResources()
+ .getString(
+ R.string.drawer_option_toggle_multitouch,
+ Boolean.toString(options.multitouch)));
+ ((TextView) findViewById(R.id.drawer_option_toggle_show_overlay))
+ .setText(
+ getResources()
+ .getString(
+ R.string.drawer_option_toggle_show_overlay,
+ Boolean.toString(options.showCropOverlay)));
+ ((TextView) findViewById(R.id.drawer_option_toggle_show_progress_bar))
+ .setText(
+ getResources()
+ .getString(
+ R.string.drawer_option_toggle_show_progress_bar,
+ Boolean.toString(options.showProgressBar)));
- ((TextView) findViewById(R.id.drawer_option_toggle_auto_zoom)).setText(getResources().getString(R.string.drawer_option_toggle_auto_zoom, options.autoZoomEnabled ? "Enabled" : "Disabled"));
- ((TextView) findViewById(R.id.drawer_option_toggle_max_zoom)).setText(getResources().getString(R.string.drawer_option_toggle_max_zoom, options.maxZoomLevel));
+ String aspectRatio = "FREE";
+ if (options.fixAspectRatio) {
+ aspectRatio = options.aspectRatio.first + ":" + options.aspectRatio.second;
}
+ ((TextView) findViewById(R.id.drawer_option_toggle_aspect_ratio))
+ .setText(getResources().getString(R.string.drawer_option_toggle_aspect_ratio, aspectRatio));
+
+ ((TextView) findViewById(R.id.drawer_option_toggle_auto_zoom))
+ .setText(
+ getResources()
+ .getString(
+ R.string.drawer_option_toggle_auto_zoom,
+ options.autoZoomEnabled ? "Enabled" : "Disabled"));
+ ((TextView) findViewById(R.id.drawer_option_toggle_max_zoom))
+ .setText(
+ getResources().getString(R.string.drawer_option_toggle_max_zoom, options.maxZoomLevel));
+ }
}
diff --git a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainFragment.java b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainFragment.java
index 731fe2b1..25dfcec2 100644
--- a/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainFragment.java
+++ b/sample/src/main/java/com/theartofdev/edmodo/cropper/sample/MainFragment.java
@@ -17,7 +17,7 @@
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.app.Fragment;
+import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -29,197 +29,203 @@
import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView;
-/**
- * The fragment that will show the Image Cropping UI by requested preset.
- */
+/** The fragment that will show the Image Cropping UI by requested preset. */
public final class MainFragment extends Fragment
- implements CropImageView.OnSetImageUriCompleteListener, CropImageView.OnCropImageCompleteListener {
-
- //region: Fields and Consts
-
- private CropDemoPreset mDemoPreset;
-
- private CropImageView mCropImageView;
- //endregion
-
- /**
- * Returns a new instance of this fragment for the given section number.
- */
- public static MainFragment newInstance(CropDemoPreset demoPreset) {
- MainFragment fragment = new MainFragment();
- Bundle args = new Bundle();
- args.putString("DEMO_PRESET", demoPreset.name());
- fragment.setArguments(args);
- return fragment;
+ implements CropImageView.OnSetImageUriCompleteListener,
+ CropImageView.OnCropImageCompleteListener {
+
+ // region: Fields and Consts
+
+ private CropDemoPreset mDemoPreset;
+
+ private CropImageView mCropImageView;
+ // endregion
+
+ /** Returns a new instance of this fragment for the given section number. */
+ public static MainFragment newInstance(CropDemoPreset demoPreset) {
+ MainFragment fragment = new MainFragment();
+ Bundle args = new Bundle();
+ args.putString("DEMO_PRESET", demoPreset.name());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ /** Set the image to show for cropping. */
+ public void setImageUri(Uri imageUri) {
+ mCropImageView.setImageUriAsync(imageUri);
+ // CropImage.activity(imageUri)
+ // .start(getContext(), this);
+ }
+
+ /** Set the options of the crop image view to the given values. */
+ public void setCropImageViewOptions(CropImageViewOptions options) {
+ mCropImageView.setScaleType(options.scaleType);
+ mCropImageView.setCropShape(options.cropShape);
+ mCropImageView.setGuidelines(options.guidelines);
+ mCropImageView.setAspectRatio(options.aspectRatio.first, options.aspectRatio.second);
+ mCropImageView.setFixedAspectRatio(options.fixAspectRatio);
+ mCropImageView.setMultiTouchEnabled(options.multitouch);
+ mCropImageView.setShowCropOverlay(options.showCropOverlay);
+ mCropImageView.setShowProgressBar(options.showProgressBar);
+ mCropImageView.setAutoZoomEnabled(options.autoZoomEnabled);
+ mCropImageView.setMaxZoom(options.maxZoomLevel);
+ mCropImageView.setFlippedHorizontally(options.flipHorizontally);
+ mCropImageView.setFlippedVertically(options.flipVertically);
+ }
+
+ /** Set the initial rectangle to use. */
+ public void setInitialCropRect() {
+ mCropImageView.setCropRect(new Rect(100, 300, 500, 1200));
+ }
+
+ /** Reset crop window to initial rectangle. */
+ public void resetCropRect() {
+ mCropImageView.resetCropRect();
+ }
+
+ public void updateCurrentCropViewOptions() {
+ CropImageViewOptions options = new CropImageViewOptions();
+ options.scaleType = mCropImageView.getScaleType();
+ options.cropShape = mCropImageView.getCropShape();
+ options.guidelines = mCropImageView.getGuidelines();
+ options.aspectRatio = mCropImageView.getAspectRatio();
+ options.fixAspectRatio = mCropImageView.isFixAspectRatio();
+ options.showCropOverlay = mCropImageView.isShowCropOverlay();
+ options.showProgressBar = mCropImageView.isShowProgressBar();
+ options.autoZoomEnabled = mCropImageView.isAutoZoomEnabled();
+ options.maxZoomLevel = mCropImageView.getMaxZoom();
+ options.flipHorizontally = mCropImageView.isFlippedHorizontally();
+ options.flipVertically = mCropImageView.isFlippedVertically();
+ ((MainActivity) getActivity()).setCurrentOptions(options);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView;
+ switch (mDemoPreset) {
+ case RECT:
+ rootView = inflater.inflate(R.layout.fragment_main_rect, container, false);
+ break;
+ case CIRCULAR:
+ rootView = inflater.inflate(R.layout.fragment_main_oval, container, false);
+ break;
+ case CUSTOMIZED_OVERLAY:
+ rootView = inflater.inflate(R.layout.fragment_main_customized, container, false);
+ break;
+ case MIN_MAX_OVERRIDE:
+ rootView = inflater.inflate(R.layout.fragment_main_min_max, container, false);
+ break;
+ case SCALE_CENTER_INSIDE:
+ rootView = inflater.inflate(R.layout.fragment_main_scale_center, container, false);
+ break;
+ case CUSTOM:
+ rootView = inflater.inflate(R.layout.fragment_main_rect, container, false);
+ break;
+ default:
+ throw new IllegalStateException("Unknown preset: " + mDemoPreset);
}
+ return rootView;
+ }
- /**
- * Set the image to show for cropping.
- */
- public void setImageUri(Uri imageUri) {
- mCropImageView.setImageUriAsync(imageUri);
- // CropImage.activity(imageUri)
- // .start(getContext(), this);
- }
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
- /**
- * Set the options of the crop image view to the given values.
- */
- public void setCropImageViewOptions(CropImageViewOptions options) {
- mCropImageView.setScaleType(options.scaleType);
- mCropImageView.setCropShape(options.cropShape);
- mCropImageView.setGuidelines(options.guidelines);
- mCropImageView.setAspectRatio(options.aspectRatio.first, options.aspectRatio.second);
- mCropImageView.setFixedAspectRatio(options.fixAspectRatio);
- mCropImageView.setMultiTouchEnabled(options.multitouch);
- mCropImageView.setShowCropOverlay(options.showCropOverlay);
- mCropImageView.setShowProgressBar(options.showProgressBar);
- mCropImageView.setAutoZoomEnabled(options.autoZoomEnabled);
- mCropImageView.setMaxZoom(options.maxZoomLevel);
- }
+ mCropImageView = view.findViewById(R.id.cropImageView);
+ mCropImageView.setOnSetImageUriCompleteListener(this);
+ mCropImageView.setOnCropImageCompleteListener(this);
- /**
- * Set the initial rectangle to use.
- */
- public void setInitialCropRect() {
- mCropImageView.setCropRect(new Rect(100, 300, 500, 1200));
- }
+ updateCurrentCropViewOptions();
- /**
- * Reset crop window to initial rectangle.
- */
- public void resetCropRect() {
- mCropImageView.resetCropRect();
+ if (savedInstanceState == null) {
+ if (mDemoPreset == CropDemoPreset.SCALE_CENTER_INSIDE) {
+ mCropImageView.setImageResource(R.drawable.cat_small);
+ } else {
+ mCropImageView.setImageResource(R.drawable.cat);
+ }
}
-
- public void updateCurrentCropViewOptions() {
- CropImageViewOptions options = new CropImageViewOptions();
- options.scaleType = mCropImageView.getScaleType();
- options.cropShape = mCropImageView.getCropShape();
- options.guidelines = mCropImageView.getGuidelines();
- options.aspectRatio = mCropImageView.getAspectRatio();
- options.fixAspectRatio = mCropImageView.isFixAspectRatio();
- options.showCropOverlay = mCropImageView.isShowCropOverlay();
- options.showProgressBar = mCropImageView.isShowProgressBar();
- options.autoZoomEnabled = mCropImageView.isAutoZoomEnabled();
- options.maxZoomLevel = mCropImageView.getMaxZoom();
- ((MainActivity) getActivity()).setCurrentOptions(options);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.main_action_crop) {
+ mCropImageView.getCroppedImageAsync();
+ return true;
+ } else if (item.getItemId() == R.id.main_action_rotate) {
+ mCropImageView.rotateImage(90);
+ return true;
+ } else if (item.getItemId() == R.id.main_action_flip_horizontally) {
+ mCropImageView.flipImageHorizontally();
+ return true;
+ } else if (item.getItemId() == R.id.main_action_flip_vertically) {
+ mCropImageView.flipImageVertically();
+ return true;
}
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View rootView;
- switch (mDemoPreset) {
- case RECT:
- rootView = inflater.inflate(R.layout.fragment_main_rect, container, false);
- break;
- case CIRCULAR:
- rootView = inflater.inflate(R.layout.fragment_main_oval, container, false);
- break;
- case CUSTOMIZED_OVERLAY:
- rootView = inflater.inflate(R.layout.fragment_main_customized, container, false);
- break;
- case MIN_MAX_OVERRIDE:
- rootView = inflater.inflate(R.layout.fragment_main_min_max, container, false);
- break;
- case SCALE_CENTER_INSIDE:
- rootView = inflater.inflate(R.layout.fragment_main_scale_center, container, false);
- break;
- case CUSTOM:
- rootView = inflater.inflate(R.layout.fragment_main_rect, container, false);
- break;
- default:
- throw new IllegalStateException("Unknown preset: " + mDemoPreset);
- }
- return rootView;
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mDemoPreset = CropDemoPreset.valueOf(getArguments().getString("DEMO_PRESET"));
+ ((MainActivity) activity).setCurrentFragment(this);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (mCropImageView != null) {
+ mCropImageView.setOnSetImageUriCompleteListener(null);
+ mCropImageView.setOnCropImageCompleteListener(null);
}
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- mCropImageView = (CropImageView) view.findViewById(R.id.cropImageView);
- mCropImageView.setOnSetImageUriCompleteListener(this);
- mCropImageView.setOnCropImageCompleteListener(this);
-
- updateCurrentCropViewOptions();
-
- if (savedInstanceState == null) {
- if (mDemoPreset == CropDemoPreset.SCALE_CENTER_INSIDE) {
- mCropImageView.setImageResource(R.drawable.cat_small);
- } else {
- mCropImageView.setImageResource(R.drawable.cat);
- }
- }
+ }
+
+ @Override
+ public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
+ if (error == null) {
+ Toast.makeText(getActivity(), "Image load successful", Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e("AIC", "Failed to load image by URI", error);
+ Toast.makeText(getActivity(), "Image load failed: " + error.getMessage(), Toast.LENGTH_LONG)
+ .show();
}
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.main_action_crop) {
- mCropImageView.getCroppedImageAsync();
- return true;
- } else if (item.getItemId() == R.id.main_action_rotate) {
- mCropImageView.rotateImage(90);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- mDemoPreset = CropDemoPreset.valueOf(getArguments().getString("DEMO_PRESET"));
- ((MainActivity) activity).setCurrentFragment(this);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- if (mCropImageView != null) {
- mCropImageView.setOnSetImageUriCompleteListener(null);
- mCropImageView.setOnCropImageCompleteListener(null);
- }
- }
-
- @Override
- public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
- if (error == null) {
- Toast.makeText(getActivity(), "Image load successful", Toast.LENGTH_SHORT).show();
- } else {
- Log.e("AIC", "Failed to load image by URI", error);
- Toast.makeText(getActivity(), "Image load failed: " + error.getMessage(), Toast.LENGTH_LONG).show();
- }
- }
-
- @Override
- public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
- handleCropResult(result);
+ }
+
+ @Override
+ public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
+ handleCropResult(result);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+ CropImage.ActivityResult result = CropImage.getActivityResult(data);
+ handleCropResult(result);
}
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
- CropImage.ActivityResult result = CropImage.getActivityResult(data);
- handleCropResult(result);
- }
- }
-
- private void handleCropResult(CropImageView.CropResult result) {
- if (result.getError() == null) {
- Intent intent = new Intent(getActivity(), CropResultActivity.class);
- intent.putExtra("SAMPLE_SIZE", result.getSampleSize());
- if (result.getUri() != null) {
- intent.putExtra("URI", result.getUri());
- } else {
- CropResultActivity.mImage = mCropImageView.getCropShape() == CropImageView.CropShape.OVAL
- ? CropImage.toOvalBitmap(result.getBitmap())
- : result.getBitmap();
- }
- startActivity(intent);
- } else {
- Log.e("AIC", "Failed to crop image", result.getError());
- Toast.makeText(getActivity(), "Image crop failed: " + result.getError().getMessage(), Toast.LENGTH_LONG).show();
- }
+ }
+
+ private void handleCropResult(CropImageView.CropResult result) {
+ if (result.getError() == null) {
+ Intent intent = new Intent(getActivity(), CropResultActivity.class);
+ intent.putExtra("SAMPLE_SIZE", result.getSampleSize());
+ if (result.getUri() != null) {
+ intent.putExtra("URI", result.getUri());
+ } else {
+ CropResultActivity.mImage =
+ mCropImageView.getCropShape() == CropImageView.CropShape.OVAL
+ ? CropImage.toOvalBitmap(result.getBitmap())
+ : result.getBitmap();
+ }
+ startActivity(intent);
+ } else {
+ Log.e("AIC", "Failed to crop image", result.getError());
+ Toast.makeText(
+ getActivity(),
+ "Image crop failed: " + result.getError().getMessage(),
+ Toast.LENGTH_LONG)
+ .show();
}
+ }
}
diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml
index 6faa8a00..c714b8f7 100644
--- a/sample/src/main/res/layout/activity_main.xml
+++ b/sample/src/main/res/layout/activity_main.xml
@@ -1,5 +1,5 @@
-
-
+
diff --git a/sample/src/main/res/menu/main.xml b/sample/src/main/res/menu/main.xml
index 7698c09d..35e28a81 100644
--- a/sample/src/main/res/menu/main.xml
+++ b/sample/src/main/res/menu/main.xml
@@ -1,11 +1,25 @@
Rotate 90 degrees clockwise
+ Flip
+ Flip horizontally
+ Flip verticallyCrop View PresetsCircularRectangular
diff --git a/settings.gradle b/settings.gradle
index 18154fe6..dc87507d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
+include ':test'
include 'cropper'
include 'sample'
include 'quick-start'
\ No newline at end of file
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/test/build.gradle b/test/build.gradle
new file mode 100644
index 00000000..1d150522
--- /dev/null
+++ b/test/build.gradle
@@ -0,0 +1,22 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 28
+ versionCode 1
+ versionName '1.0'
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ api "androidx.appcompat:appcompat:$androidXLibraryVersion"
+ implementation project(":cropper")
+
+}
diff --git a/test/src/main/AndroidManifest.xml b/test/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..491b03c8
--- /dev/null
+++ b/test/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/src/main/java/com/theartofdev/edmodo/cropper/test/MainActivity.java b/test/src/main/java/com/theartofdev/edmodo/cropper/test/MainActivity.java
new file mode 100644
index 00000000..06fe2362
--- /dev/null
+++ b/test/src/main/java/com/theartofdev/edmodo/cropper/test/MainActivity.java
@@ -0,0 +1,43 @@
+package com.theartofdev.edmodo.cropper.test;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.example.test.R;
+import com.theartofdev.edmodo.cropper.CropImage;
+import com.theartofdev.edmodo.cropper.CropImageView;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+
+ /** Start pick image activity with chooser. */
+ public void onSelectImageClick(View view) {
+ CropImage.activity(null).setGuidelines(CropImageView.Guidelines.ON).start(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ // handle result of CropImageActivity
+ if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+ CropImage.ActivityResult result = CropImage.getActivityResult(data);
+ if (resultCode == RESULT_OK) {
+ ((ImageView) findViewById(R.id.quick_start_cropped_image)).setImageURI(result.getUri());
+ Toast.makeText(
+ this, "Cropping successful, Sample: " + result.getSampleSize(), Toast.LENGTH_LONG)
+ .show();
+ } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
+ Toast.makeText(this, "Cropping failed: " + result.getError(), Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+}
diff --git a/test/src/main/res/layout/activity_main.xml b/test/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..b122edb3
--- /dev/null
+++ b/test/src/main/res/layout/activity_main.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/test/src/main/res/mipmap-xhdpi/ic_launcher.png b/test/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..bfa42f0e
Binary files /dev/null and b/test/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/test/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/test/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..3af2608a
Binary files /dev/null and b/test/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/test/src/main/res/values/colors.xml b/test/src/main/res/values/colors.xml
new file mode 100644
index 00000000..3ab3e9cb
--- /dev/null
+++ b/test/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/test/src/main/res/values/strings.xml b/test/src/main/res/values/strings.xml
new file mode 100644
index 00000000..949a9380
--- /dev/null
+++ b/test/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Test
+
diff --git a/test/src/main/res/values/styles.xml b/test/src/main/res/values/styles.xml
new file mode 100644
index 00000000..95131607
--- /dev/null
+++ b/test/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+