diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b549f54..a9863af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - `Security` in case of vulnerabilities. ## [unreleased x.x.x] - +### Added +- `CropImageContract` and `PickImageContract` +- added dependency to `androidx.activity:activity-ktx:.2.3` + +### Changed +- `CropImageActivity.onActivityResult` no longer receives any result. Override `onPickImageResult` instead. + +### Deprecated +- deprecated old methods that depend on the deprecated `onActivityResult`. Use `CropImageContract` and `PickImageContract` instead. ## [3.1.3] - 10/06/21 ### Fixed diff --git a/README.md b/README.md index 8e5c49c3..8efaa66c 100644 --- a/README.md +++ b/README.md @@ -92,43 +92,33 @@ override fun onCreate(savedInstanceState: Bundle?) { ``` ### Start the default Activity -- Start `CropImageActivity` using builder pattern from your activity +- Register for activity result with `CropImageContract` ```kotlin class MainActivity { - private fun startCrop() { - // 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) - .start(this) - - // for fragment (DO NOT use `getActivity()`) - CropImage - .activity() - .start(requireContext(), this) - } + private val cropImage = registerForActivityResult(CropImageContract()) { result -> + if (result.isSuccessful) { + // use the returned uri + val uri = result.uriContent + } else { + // an error occured + } + } } ``` -- Override `onActivityResult` method in your activity to get crop result +- Launch the activity ```kotlin -class MainActivity { - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { - val result = CropImage.getActivityResult(data) - if (resultCode == Activity.RESULT_OK) { - val resultUri: Uri? = result?.uriContent - val resultFilePath: String? = result?.getUriFilePath(requireContext()) - } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - val error = result!!.error - } - } - } +private fun startCrop() { + // start picker to get image for cropping and then use the image in cropping activity + cropImage.launch(options()) + + // start cropping activity for pre-acquired image saved on the device and customize settings + cropImage.launch( + options(uri = imageUri) { + setGuidelines(Guidelines.ON) + setOutputCompressFormat(CompressFormat.PNG) + } + ) } ``` diff --git a/cropper/build.gradle b/cropper/build.gradle index 89ea9d05..656d5cfc 100644 --- a/cropper/build.gradle +++ b/cropper/build.gradle @@ -24,11 +24,21 @@ android { buildFeatures { viewBinding true } + kotlinOptions { + jvmTarget = "1.8" + } + testOptions { + unitTests { + includeAndroidResources = true + } + } } dependencies { api "androidx.appcompat:appcompat:$androidXAppCompatVersionCropper" + implementation "androidx.activity:activity-ktx:$androidXActivity" + implementation "androidx.exifinterface:exifinterface:$androidXExifVersion" implementation "androidx.core:core-ktx:$androidXCoreKtxVersion" @@ -37,4 +47,11 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + + testImplementation "junit:junit:$junitVersion" + testImplementation "androidx.test.ext:junit:$androidXJunitVersion" + testImplementation "androidx.test:core:$androidXTestVersion" + testImplementation "androidx.test:runner:$androidXTestVersion" + debugImplementation "androidx.fragment:fragment-testing:$androidXFragmentVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" } diff --git a/cropper/src/main/java/com/canhub/cropper/CropImage.kt b/cropper/src/main/java/com/canhub/cropper/CropImage.kt index 844cef90..7d606b32 100644 --- a/cropper/src/main/java/com/canhub/cropper/CropImage.kt +++ b/cropper/src/main/java/com/canhub/cropper/CropImage.kt @@ -28,6 +28,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi import androidx.core.content.FileProvider import androidx.fragment.app.Fragment +import com.canhub.cropper.CropImageOptions.Companion.DEGREES_360 import com.canhub.cropper.CropImageView.CropResult import com.canhub.cropper.CropImageView.CropShape import com.canhub.cropper.CropImageView.Guidelines @@ -129,6 +130,7 @@ object CropImage { * * @param activity the activity to be used to start activity from */ + @Deprecated("use the PickImageContract ActivityResultContract instead") fun startPickImageActivity(activity: Activity) { activity.startActivityForResult( getPickImageChooserIntent(activity), PICK_IMAGE_CHOOSER_REQUEST_CODE @@ -142,6 +144,7 @@ object CropImage { * @param context The Fragments context. Use getContext() * @param fragment The calling Fragment to start and return the image to */ + @Deprecated("use the PickImageContract ActivityResultContract instead") fun startPickImageActivity(context: Context, fragment: Fragment) { fragment.startActivityForResult( getPickImageChooserIntent(context), PICK_IMAGE_CHOOSER_REQUEST_CODE @@ -465,6 +468,7 @@ object CropImage { * @return builder for Crop Image Activity */ @JvmStatic + @Deprecated("use the CropImageContract ActivityResultContract instead") fun activity(): ActivityBuilder { return ActivityBuilder(null) } @@ -479,6 +483,7 @@ object CropImage { * @return builder for Crop Image Activity */ @JvmStatic + @Deprecated("use the CropImageContract ActivityResultContract instead") fun activity(uri: Uri?): ActivityBuilder { return ActivityBuilder(uri) } @@ -491,6 +496,7 @@ object CropImage { */ // TODO don't return null @JvmStatic + @Deprecated("use the CropImageContract ActivityResultContract instead") fun getActivityResult(data: Intent?): ActivityResult? = data?.getParcelableExtra(CROP_IMAGE_EXTRA_RESULT) as? ActivityResult? @@ -499,6 +505,7 @@ object CropImage { * * @param mSource The image to crop source Android uri. */ + @Deprecated("use the CropImageContract ActivityResultContract instead") class ActivityBuilder(private val mSource: Uri?) { /** @@ -958,7 +965,7 @@ object CropImage { * *Default: NONE - will read image exif data* */ fun setInitialRotation(initialRotation: Int): ActivityBuilder { - mOptions.initialRotation = (initialRotation + 360) % 360 + mOptions.initialRotation = (initialRotation + DEGREES_360) % DEGREES_360 return this } @@ -995,7 +1002,7 @@ object CropImage { * *Default: 90* */ fun setRotationDegrees(rotationDegrees: Int): ActivityBuilder { - mOptions.rotationDegrees = (rotationDegrees + 360) % 360 + mOptions.rotationDegrees = (rotationDegrees + DEGREES_360) % DEGREES_360 return this } @@ -1101,4 +1108,17 @@ object CropImage { } } } + + object CancelledResult : CropImageView.CropResult( + originalBitmap = null, + originalUri = null, + bitmap = null, + uriContent = null, + error = Exception("cropping has been cancelled by the user"), + cropPoints = floatArrayOf(), + cropRect = null, + wholeImageRect = null, + rotation = 0, + sampleSize = 0 + ) } diff --git a/cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt b/cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt index 40fc6b56..6bfd140e 100644 --- a/cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt +++ b/cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt @@ -1,7 +1,6 @@ package com.canhub.cropper import android.Manifest -import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -49,6 +48,8 @@ open class CropImageActivity : private var cropImageView: CropImageView? = null private lateinit var binding: CropImageActivityBinding + private val pickImage = registerForActivityResult(PickImageContract(), ::onPickImageResult) + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -68,7 +69,7 @@ open class CropImageActivity : CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE ) } else { - CropImage.startPickImageActivity(this) + pickImage.launch(true) } } else if ( cropImageUri?.let { @@ -163,30 +164,25 @@ open class CropImageActivity : setResultCancel() } - @SuppressLint("NewApi") - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - // handle result of pick image chooser - if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE) { - if (resultCode == RESULT_CANCELED) setResultCancel() - if (resultCode == RESULT_OK) { - cropImageUri = CropImage.getPickImageResultUriContent(this, data) - // For API >= 23 we need to check specifically that we have permissions to read external - // storage. - if (cropImageUri?.let { - CropImage.isReadExternalStoragePermissionsRequired(this, it) - } == true && - CommonVersionCheck.isAtLeastM23() - ) { - // request permissions and handle the result in onRequestPermissionsResult() - requestPermissions( - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE - ) - } else { - // no permissions required or already grunted, can start crop image activity - cropImageView?.setImageUriAsync(cropImageUri) - } + protected open fun onPickImageResult(resultUri: Uri?) { + if (resultUri == null) setResultCancel() + if (resultUri != null) { + cropImageUri = resultUri + // For API >= 23 we need to check specifically that we have permissions to read external + // storage. + if (cropImageUri?.let { + CropImage.isReadExternalStoragePermissionsRequired(this, it) + } == true && + CommonVersionCheck.isAtLeastM23() + ) { + // request permissions and handle the result in onRequestPermissionsResult() + requestPermissions( + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE + ) + } else { + // no permissions required or already granted, can start crop image activity + cropImageView?.setImageUriAsync(cropImageUri) } } } @@ -212,7 +208,7 @@ open class CropImageActivity : } else 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) + pickImage.launch(true) } else super.onRequestPermissionsResult(requestCode, permissions, grantResults) } diff --git a/cropper/src/main/java/com/canhub/cropper/CropImageContract.kt b/cropper/src/main/java/com/canhub/cropper/CropImageContract.kt new file mode 100644 index 00000000..13a2f166 --- /dev/null +++ b/cropper/src/main/java/com/canhub/cropper/CropImageContract.kt @@ -0,0 +1,40 @@ +package com.canhub.cropper + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.activity.result.contract.ActivityResultContract + +/** + * An ActivityResultContract to start an activity that allows the user to crop an image. + * The activity can be heavily customized by the input CropImageContractOptions. + * If you do not provide an uri in the input the user will be asked to pick an image before cropping. + */ + +class CropImageContract : + ActivityResultContract() { + + override fun createIntent(context: Context, input: CropImageContractOptions): Intent { + input.options.validate() + return Intent(context, CropImageActivity::class.java).apply { + val bundle = Bundle() + bundle.putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri) + bundle.putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.options) + putExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE, bundle) + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): CropImageView.CropResult { + val result = intent?.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_RESULT) as? CropImage.ActivityResult? + return if (result == null || resultCode == Activity.RESULT_CANCELED) { + CropImage.CancelledResult + } else { + result + } + } +} diff --git a/cropper/src/main/java/com/canhub/cropper/CropImageContractOptions.kt b/cropper/src/main/java/com/canhub/cropper/CropImageContractOptions.kt new file mode 100644 index 00000000..6e4541cb --- /dev/null +++ b/cropper/src/main/java/com/canhub/cropper/CropImageContractOptions.kt @@ -0,0 +1,463 @@ +package com.canhub.cropper + +import android.graphics.Bitmap +import android.graphics.Rect +import android.net.Uri +import androidx.annotation.DrawableRes +import com.canhub.cropper.CropImageOptions.Companion.DEGREES_360 +import com.canhub.cropper.CropImageView.CropShape +import com.canhub.cropper.CropImageView.Guidelines +import com.canhub.cropper.CropImageView.RequestSizeOptions + +/** + * Options to customize the activity opened by CropImageContract. + * Conveniently created by the options method. + */ + +data class CropImageContractOptions( + val uri: Uri?, + val options: CropImageOptions +) { + + /** + * The shape of the cropping window.

+ * To set square/circle crop shape set aspect ratio to 1:1.

+ * *Default: RECTANGLE* + * + * When setting RECTANGLE_VERTICAL_ONLY or RECTANGLE_HORIZONTAL_ONLY you may also want to + * use a free aspect ratio (to allow the crop window to change in the desired dimension + * whilst staying the same in the other dimension) and have the initial crop window cover + * the entire image (so that the crop window has no space to move in the other dimension). + * These can be done with + * [setFixAspectRatio] } (with argument `false`) and + * [setInitialCropWindowPaddingRatio] (with argument `0f). + */ + fun setCropShape(cropShape: CropShape): CropImageContractOptions { + options.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* + */ + fun setSnapRadius(snapRadius: Float): CropImageContractOptions { + options.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* + */ + fun setTouchRadius(touchRadius: Float): CropImageContractOptions { + options.touchRadius = touchRadius + return this + } + + /** + * whether the guidelines should be on, off, or only showing when resizing.

+ * *Default: ON_TOUCH* + */ + fun setGuidelines(guidelines: Guidelines): CropImageContractOptions { + options.guidelines = guidelines + return this + } + + /** + * The initial scale type of the image in the crop image view

+ * *Default: FIT_CENTER* + */ + fun setScaleType(scaleType: CropImageView.ScaleType): CropImageContractOptions { + options.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.* + */ + fun setShowCropOverlay(showCropOverlay: Boolean): CropImageContractOptions { + options.showCropOverlay = showCropOverlay + return this + } + + /** + * if auto-zoom functionality is enabled.

+ * default: true. + */ + fun setAutoZoomEnabled(autoZoomEnabled: Boolean): CropImageContractOptions { + options.autoZoomEnabled = autoZoomEnabled + return this + } + + /** + * if multi touch functionality is enabled.

+ * default: true. + */ + fun setMultiTouchEnabled(multiTouchEnabled: Boolean): CropImageContractOptions { + options.multiTouchEnabled = multiTouchEnabled + return this + } + + /** + * if the crop window can be moved by dragging the center.

+ * default: true + */ + fun setCenterMoveEnabled(centerMoveEnabled: Boolean): CropImageContractOptions { + options.centerMoveEnabled = centerMoveEnabled + return this + } + + /** + * The max zoom allowed during cropping.

+ * *Default: 4* + */ + fun setMaxZoom(maxZoom: Int): CropImageContractOptions { + options.maxZoom = maxZoom + return this + } + + /** + * The initial crop window padding from image borders in percentage of the cropping image + * dimensions.

+ * *Default: 0.1* + */ + fun setInitialCropWindowPaddingRatio(initialCropWindowPaddingRatio: Float): CropImageContractOptions { + options.initialCropWindowPaddingRatio = initialCropWindowPaddingRatio + return this + } + + /** + * whether the width to height aspect ratio should be maintained or free to change.

+ * *Default: false* + */ + fun setFixAspectRatio(fixAspectRatio: Boolean): CropImageContractOptions { + options.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 + */ + fun setAspectRatio(aspectRatioX: Int, aspectRatioY: Int): CropImageContractOptions { + options.aspectRatioX = aspectRatioX + options.aspectRatioY = aspectRatioY + options.fixAspectRatio = true + return this + } + + /** + * the thickness of the guidelines lines (in pixels).

+ * *Default: 3dp* + */ + fun setBorderLineThickness(borderLineThickness: Float): CropImageContractOptions { + options.borderLineThickness = borderLineThickness + return this + } + + /** + * the color of the guidelines lines.

+ * *Default: Color.argb(170, 255, 255, 255)* + */ + fun setBorderLineColor(borderLineColor: Int): CropImageContractOptions { + options.borderLineColor = borderLineColor + return this + } + + /** + * thickness of the corner line (in pixels).

+ * *Default: 2dp* + */ + fun setBorderCornerThickness(borderCornerThickness: Float): CropImageContractOptions { + options.borderCornerThickness = borderCornerThickness + return this + } + + /** + * the offset of corner line from crop window border (in pixels).

+ * *Default: 5dp* + */ + fun setBorderCornerOffset(borderCornerOffset: Float): CropImageContractOptions { + options.borderCornerOffset = borderCornerOffset + return this + } + + /** + * the length of the corner line away from the corner (in pixels).

+ * *Default: 14dp* + */ + fun setBorderCornerLength(borderCornerLength: Float): CropImageContractOptions { + options.borderCornerLength = borderCornerLength + return this + } + + /** + * the color of the corner line.

+ * *Default: WHITE* + */ + fun setBorderCornerColor(borderCornerColor: Int): CropImageContractOptions { + options.borderCornerColor = borderCornerColor + return this + } + + /** + * the thickness of the guidelines lines (in pixels).

+ * *Default: 1dp* + */ + fun setGuidelinesThickness(guidelinesThickness: Float): CropImageContractOptions { + options.guidelinesThickness = guidelinesThickness + return this + } + + /** + * the color of the guidelines lines.

+ * *Default: Color.argb(170, 255, 255, 255)* + */ + fun setGuidelinesColor(guidelinesColor: Int): CropImageContractOptions { + options.guidelinesColor = guidelinesColor + 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)* + */ + fun setBackgroundColor(backgroundColor: Int): CropImageContractOptions { + options.backgroundColor = backgroundColor + return this + } + + /** + * the min size the crop window is allowed to be (in pixels).

+ * *Default: 42dp, 42dp* + */ + fun setMinCropWindowSize( + minCropWindowWidth: Int, + minCropWindowHeight: Int + ): CropImageContractOptions { + options.minCropWindowWidth = minCropWindowWidth + options.minCropWindowHeight = minCropWindowHeight + return this + } + + /** + * the min size the resulting cropping image is allowed to be, affects the cropping window + * limits (in pixels).

+ * *Default: 40px, 40px* + */ + fun setMinCropResultSize( + minCropResultWidth: Int, + minCropResultHeight: Int + ): CropImageContractOptions { + options.minCropResultWidth = minCropResultWidth + options.minCropResultHeight = minCropResultHeight + return this + } + + /** + * the max size the resulting cropping image is allowed to be, affects the cropping window + * limits (in pixels).

+ * *Default: 99999, 99999* + */ + fun setMaxCropResultSize( + maxCropResultWidth: Int, + maxCropResultHeight: Int + ): CropImageContractOptions { + options.maxCropResultWidth = maxCropResultWidth + options.maxCropResultHeight = maxCropResultHeight + return this + } + + /** + * the title of the [CropImageActivity].

+ * *Default: ""* + */ + fun setActivityTitle(activityTitle: CharSequence): CropImageContractOptions { + options.activityTitle = activityTitle + return this + } + + /** + * the color to use for action bar items icons.

+ * *Default: NONE* + */ + fun setActivityMenuIconColor(activityMenuIconColor: Int): CropImageContractOptions { + options.activityMenuIconColor = activityMenuIconColor + return this + } + + /** + * the Android Uri to save the cropped image to.

+ * *Default: NONE, will create a temp file* + */ + fun setOutputUri(outputUri: Uri?): CropImageContractOptions { + options.outputUri = outputUri + return this + } + + /** + * the compression format to use when writting the image.

+ * *Default: JPEG* + */ + fun setOutputCompressFormat(outputCompressFormat: Bitmap.CompressFormat): CropImageContractOptions { + options.outputCompressFormat = outputCompressFormat + return this + } + + /** + * the quality (if applicable) to use when writting the image (0 - 100).

+ * *Default: 90* + */ + fun setOutputCompressQuality(outputCompressQuality: Int): CropImageContractOptions { + options.outputCompressQuality = outputCompressQuality + return this + } + + /** + * the size to resize the cropped image to.

+ * Uses [CropImageView.RequestSizeOptions.RESIZE_INSIDE] option.

+ * *Default: 0, 0 - not set, will not resize* + */ + fun setRequestedSize(reqWidth: Int, reqHeight: Int): CropImageContractOptions { + return setRequestedSize(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE) + } + + /** + * the size to resize the cropped image to.

+ * *Default: 0, 0 - not set, will not resize* + */ + fun setRequestedSize( + reqWidth: Int, + reqHeight: Int, + reqSizeOptions: RequestSizeOptions, + ): CropImageContractOptions { + options.outputRequestWidth = reqWidth + options.outputRequestHeight = reqHeight + options.outputRequestSizeOptions = reqSizeOptions + 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* + */ + fun setNoOutputImage(noOutputImage: Boolean): CropImageContractOptions { + options.noOutputImage = noOutputImage + return this + } + + /** + * the initial rectangle to set on the cropping image after loading.

+ * *Default: NONE - will initialize using initial crop window padding ratio* + */ + fun setInitialCropWindowRectangle(initialCropWindowRectangle: Rect?): CropImageContractOptions { + options.initialCropWindowRectangle = initialCropWindowRectangle + return this + } + + /** + * the initial rotation to set on the cropping image after loading (0-360 degrees clockwise). + *

+ * *Default: NONE - will read image exif data* + */ + fun setInitialRotation(initialRotation: Int): CropImageContractOptions { + options.initialRotation = (initialRotation + DEGREES_360) % DEGREES_360 + return this + } + + /** + * if to allow rotation during cropping.

+ * *Default: true* + */ + fun setAllowRotation(allowRotation: Boolean): CropImageContractOptions { + options.allowRotation = allowRotation + return this + } + + /** + * if to allow flipping during cropping.

+ * *Default: true* + */ + fun setAllowFlipping(allowFlipping: Boolean): CropImageContractOptions { + options.allowFlipping = allowFlipping + return this + } + + /** + * if to allow counter-clockwise rotation during cropping.

+ * Note: if rotation is disabled this option has no effect.

+ * *Default: false* + */ + fun setAllowCounterRotation(allowCounterRotation: Boolean): CropImageContractOptions { + options.allowCounterRotation = allowCounterRotation + return this + } + + /** + * The amount of degreees to rotate clockwise or counter-clockwise (0-360).

+ * *Default: 90* + */ + fun setRotationDegrees(rotationDegrees: Int): CropImageContractOptions { + options.rotationDegrees = (rotationDegrees + DEGREES_360) % DEGREES_360 + return this + } + + /** + * whether the image should be flipped horizontally.

+ * *Default: false* + */ + fun setFlipHorizontally(flipHorizontally: Boolean): CropImageContractOptions { + options.flipHorizontally = flipHorizontally + return this + } + + /** + * whether the image should be flipped vertically.

+ * *Default: false* + */ + fun setFlipVertically(flipVertically: Boolean): CropImageContractOptions { + options.flipVertically = flipVertically + return this + } + + /** + * optional, set crop menu crop button title.

+ * *Default: null, will use resource string: crop_image_menu_crop* + */ + fun setCropMenuCropButtonTitle(title: CharSequence?): CropImageContractOptions { + options.cropMenuCropButtonTitle = title + return this + } + + /** + * Image resource id to use for crop icon instead of text.

+ * *Default: 0* + */ + fun setCropMenuCropButtonIcon(@DrawableRes drawableResource: Int): CropImageContractOptions { + options.cropMenuCropButtonIcon = drawableResource + return this + } +} + +fun options( + uri: Uri? = null, + builder: CropImageContractOptions.() -> (Unit) = {} +): CropImageContractOptions { + val options = CropImageContractOptions(uri, CropImageOptions()) + options.run(builder) + return options +} diff --git a/cropper/src/main/java/com/canhub/cropper/CropImageOptions.kt b/cropper/src/main/java/com/canhub/cropper/CropImageOptions.kt index a43148b4..4fdd0e0a 100644 --- a/cropper/src/main/java/com/canhub/cropper/CropImageOptions.kt +++ b/cropper/src/main/java/com/canhub/cropper/CropImageOptions.kt @@ -436,10 +436,13 @@ open class CropImageOptions : Parcelable { require(maxCropResultHeight >= minCropResultHeight) { "Cannot set max crop result height to smaller value than min crop result height" } require(outputRequestWidth >= 0) { "Cannot set request width value to a number < 0 " } require(outputRequestHeight >= 0) { "Cannot set request height value to a number < 0 " } - require(!(rotationDegrees < 0 || rotationDegrees > 360)) { "Cannot set rotation degrees value to a number < 0 or > 360" } + require(!(rotationDegrees < 0 || rotationDegrees > DEGREES_360)) { "Cannot set rotation degrees value to a number < 0 or > 360" } } companion object { + + internal const val DEGREES_360 = 360 + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { diff --git a/cropper/src/main/java/com/canhub/cropper/PickImageContract.kt b/cropper/src/main/java/com/canhub/cropper/PickImageContract.kt new file mode 100644 index 00000000..b79260a6 --- /dev/null +++ b/cropper/src/main/java/com/canhub/cropper/PickImageContract.kt @@ -0,0 +1,48 @@ +package com.canhub.cropper + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import com.canhub.cropper.CropImage.getPickImageResultUriContent + +/** + * An ActivityResultContract to prompt the user to pick an image, receiving + * a Uri for that image that allows you to use + * android.content.ContentResolver#openInputStream(Uri) to access the raw data. + *

+ * Set the boolean input flag to true to include the camera in the options presented to the user. + *

+ * If you want to customize how the result is parsed, extend this class and override parseResult +*/ + +open class PickImageContract : ActivityResultContract() { + + protected var context: Context? = null + + override fun createIntent(context: Context, input: Boolean): Intent { + + this.context = context + + return CropImage.getPickImageChooserIntent( + context = context, + title = context.getString(R.string.pick_image_intent_chooser_title), + includeDocuments = false, + includeCamera = input + ) + } + + open override fun parseResult( + resultCode: Int, + intent: Intent? + ): Uri? { + if (intent != null) { + context?.let { + context = null + return getPickImageResultUriContent(it, intent) + } + } + context = null + return null + } +} diff --git a/cropper/src/test/java/com/canhub/cropper/ContractTestFragment.kt b/cropper/src/test/java/com/canhub/cropper/ContractTestFragment.kt new file mode 100644 index 00000000..09202fa7 --- /dev/null +++ b/cropper/src/test/java/com/canhub/cropper/ContractTestFragment.kt @@ -0,0 +1,34 @@ +package com.canhub.cropper + +import android.content.Intent +import android.net.Uri +import androidx.activity.result.ActivityResultRegistry +import androidx.fragment.app.Fragment + +class ContractTestFragment( + registry: ActivityResultRegistry +) : Fragment() { + + var cropResult: CropImageView.CropResult? = null + var pickResult: Uri? = null + + val cropImage = registerForActivityResult(CropImageContract(), registry) { result -> + this.cropResult = result + } + + val pickImage = registerForActivityResult(PickImageContract(), registry) { result -> + this.pickResult = result + } + + fun cropImage(input: CropImageContractOptions) { + cropImage.launch(input) + } + + fun cropImageIntent(input: CropImageContractOptions): Intent { + return cropImage.contract.createIntent(requireContext(), input) + } + + fun pickImageIntent(input: Boolean): Intent { + return pickImage.contract.createIntent(requireContext(), input) + } +} diff --git a/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt b/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt new file mode 100644 index 00000000..8f8541ae --- /dev/null +++ b/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt @@ -0,0 +1,205 @@ +package com.canhub.cropper + +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Rect +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat +import androidx.core.net.toUri +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CropImageContractTest { + + @Test(expected = IllegalArgumentException::class) + fun `when providing invalid options then cropping should crash`() { + + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) + } + } + + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + fragment.cropImageIntent(options { setMaxZoom(-10) }) + } + } + } + + @Test + fun `when cropping is cancelled by user then result should be cancelled`() { + + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) + } + } + + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + fragment.cropImage(options()) + assert(fragment.cropResult == CropImage.CancelledResult) + } + } + } + + @Test + fun `when cropping succeeds result should be successful`() { + + val result = CropImage.ActivityResult( + originalUri = "content://original".toUri(), + uriContent = "content://content".toUri(), + error = null, + cropPoints = floatArrayOf(), + cropRect = Rect(1, 2, 3, 4), + rotation = 45, + wholeImageRect = Rect(10, 20, 0, 0), + sampleSize = 0 + ) + + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + + val intent = Intent() + intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result) + + dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) + } + } + + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + fragment.cropImage(options()) + assert(fragment.cropResult == result) + } + } + } + + @Test + fun `when starting crop with all options then intent should contain these options`() { + + val options = options("file://testInput".toUri()) { + setCropShape(CropImageView.CropShape.OVAL) + setSnapRadius(1f) + setTouchRadius(2f) + setGuidelines(CropImageView.Guidelines.ON_TOUCH) + setScaleType(CropImageView.ScaleType.CENTER) + setShowCropOverlay(true) + setAutoZoomEnabled(false) + setMultiTouchEnabled(true) + setCenterMoveEnabled(false) + setMaxZoom(17) + setInitialCropWindowPaddingRatio(0.2f) + setFixAspectRatio(true) + setAspectRatio(3, 4) + setBorderLineThickness(3f) + setBorderLineColor(Color.GREEN) + setBorderCornerThickness(5f) + setBorderCornerOffset(6f) + setBorderCornerLength(7f) + setBorderCornerColor(Color.MAGENTA) + setGuidelinesThickness(8f) + setGuidelinesColor(Color.RED) + setBackgroundColor(Color.BLUE) + setMinCropWindowSize(5, 5) + setMinCropResultSize(10, 10) + setMaxCropResultSize(5000, 5000) + setActivityTitle("Test Activity Title") + setActivityMenuIconColor(Color.BLACK) + setOutputUri("file://testOutputUri".toUri()) + setOutputCompressFormat(Bitmap.CompressFormat.JPEG) + setOutputCompressQuality(85) + setRequestedSize(25, 30, CropImageView.RequestSizeOptions.NONE) + setNoOutputImage(false) + setInitialCropWindowRectangle(Rect(4, 5, 6, 7)) + setInitialRotation(13) + setAllowRotation(true) + setAllowFlipping(false) + setAllowCounterRotation(true) + setRotationDegrees(4) + setFlipHorizontally(true) + setFlipVertically(false) + setCropMenuCropButtonTitle("Test Button Title") + setCropMenuCropButtonIcon(R.drawable.ic_rotate_left_24) + } + + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) {} + } + + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + val cropImageIntent = fragment.cropImageIntent(options) + + assertEquals(CropImageActivity::class.java.name, cropImageIntent.component?.className) + + val bundle = cropImageIntent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE) + assertEquals("file://testInput".toUri(), bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE)) + assertEquals(options.options, bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS)) + } + } + } + + @Test + fun `when cropping fails result should be unsuccessful`() { + + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + val result = CropImage.ActivityResult( + originalUri = null, + uriContent = null, + error = Exception("Error!"), + cropPoints = floatArrayOf(), + cropRect = null, + rotation = 0, + wholeImageRect = null, + sampleSize = 0 + ) + val intent = Intent() + intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result) + + dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) + } + } + + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + fragment.cropImage(options()) + assertEquals(false, fragment.cropResult?.isSuccessful) + } + } + } +} diff --git a/cropper/src/test/java/com/canhub/cropper/PickImageContractTest.kt b/cropper/src/test/java/com/canhub/cropper/PickImageContractTest.kt new file mode 100644 index 00000000..513ad3e4 --- /dev/null +++ b/cropper/src/test/java/com/canhub/cropper/PickImageContractTest.kt @@ -0,0 +1,51 @@ +package com.canhub.cropper + +import android.app.Activity +import android.content.Intent +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat +import androidx.core.net.toUri +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PickImageContractTest { + + private val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { } + } + + @Test + fun `when starting image pick then intent action should be ACTION_CHOOSER`() { + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + val pickImageIntent = fragment.pickImageIntent(true) + + assertEquals(pickImageIntent.action, Intent.ACTION_CHOOSER) + } + } + } + + @Test + fun `when parsing image pick result correct uri should be returned`() { + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> + fragment.pickImageIntent(true) + val resultIntent = Intent().apply { + data = "content://testResult".toUri() + } + val result = fragment.pickImage.contract.parseResult(Activity.RESULT_OK, resultIntent) + assertEquals("content://testResult".toUri(), result) + } + } + } +} diff --git a/sample/build.gradle b/sample/build.gradle index c0f44595..687e3f6f 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -20,6 +20,7 @@ android { } kotlinOptions { freeCompilerArgs = ["-Xinline-classes"] + jvmTarget = "1.8" } buildFeatures { viewBinding true diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera/app/SCameraFragment.kt b/sample/src/main/java/com/canhub/cropper/sample/camera/app/SCameraFragment.kt index da29149a..22585fc1 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera/app/SCameraFragment.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/camera/app/SCameraFragment.kt @@ -10,7 +10,6 @@ import android.graphics.Color.WHITE import android.net.Uri import android.os.Bundle import android.os.Environment -import android.provider.MediaStore import android.util.Log import android.view.LayoutInflater import android.view.View @@ -18,9 +17,13 @@ import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.fragment.app.Fragment import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract import com.canhub.cropper.CropImageView +import com.canhub.cropper.PickImageContract +import com.canhub.cropper.options import com.canhub.cropper.sample.SCropResultActivity import com.canhub.cropper.sample.camera.domain.CameraEnumDomain import com.canhub.cropper.sample.camera.domain.SCameraContract @@ -37,16 +40,13 @@ internal class SCameraFragment : SCameraContract.View { companion object { - fun newInstance() = SCameraFragment() - const val CODE_PHOTO_CAMERA = 811917 const val DATE_FORMAT = "yyyyMMdd_HHmmss" const val FILE_NAMING_PREFIX = "JPEG_" const val FILE_NAMING_SUFFIX = "_" const val FILE_FORMAT = ".jpg" const val AUTHORITY_SUFFIX = ".fileprovider" - const val CUSTOM_REQUEST_CODE = 8119153 } private lateinit var binding: FragmentCameraBinding @@ -56,6 +56,32 @@ internal class SCameraFragment : ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> presenter.onPermissionResult(isGranted) } + private val pickImage = + registerForActivityResult(PickImageContract(), presenter::onPickImageResult) + + private val pickImageCustom = + registerForActivityResult( + object : PickImageContract() { + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + if (intent != null) { + context?.let { + context = null + return CropImage.getPickImageResultUriFilePath(it, intent).toUri() + } + } + context = null + return null + } + }, + presenter::onPickImageResultCustom + ) + + private val cropImage = + registerForActivityResult(CropImageContract(), presenter::onCropImageResult) + + private val takePicture = + registerForActivityResult(ActivityResultContracts.TakePicture(), presenter::onTakePictureResult) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -79,7 +105,7 @@ internal class SCameraFragment : presenter.startPickImageActivityClicked() } binding.startActivityForResult.setOnClickListener { - presenter.startActivityForResultClicked() + presenter.startPickImageActivityCustomClicked() } presenter.onCreate(activity, context) @@ -90,118 +116,114 @@ internal class SCameraFragment : CameraEnumDomain.START_WITH_URI -> startCameraWithUri() CameraEnumDomain.START_WITHOUT_URI -> startCameraWithoutUri() CameraEnumDomain.START_PICK_IMG -> startPickImage() - CameraEnumDomain.START_FOR_RESULT -> startForResult() + CameraEnumDomain.START_PICK_IMG_CUSTOM -> startPickImageCustom() } } - private fun startForResult() { - context?.let { - val intent = CropImage.getPickImageChooserIntent(it, "Selection Baby", true, false) - - this.startActivityForResult(intent, CUSTOM_REQUEST_CODE) - } + private fun startPickImageCustom() { + pickImageCustom.launch(false) } private fun startPickImage() { - context?.let { CropImage.startPickImageActivity(it, this) } + pickImage.launch(false) } private fun startCameraWithoutUri() { - context?.let { ctx -> - CropImage.activity() - .setScaleType(CropImageView.ScaleType.CENTER) - .setCropShape(CropImageView.CropShape.OVAL) - .setGuidelines(CropImageView.Guidelines.ON) - .setAspectRatio(4, 16) - .setMaxZoom(8) - .setAutoZoomEnabled(false) - .setMultiTouchEnabled(false) - .setCenterMoveEnabled(true) - .setShowCropOverlay(false) - .setAllowFlipping(false) - .setSnapRadius(10f) - .setTouchRadius(30f) - .setInitialCropWindowPaddingRatio(0.3f) - .setBorderLineThickness(5f) - .setBorderLineColor(R.color.black) - .setBorderCornerThickness(6f) - .setBorderCornerOffset(2f) - .setBorderCornerLength(20f) - .setBorderCornerColor(RED) - .setGuidelinesThickness(5f) - .setGuidelinesColor(RED) - .setBackgroundColor(Color.argb(119, 30, 60, 90)) - .setMinCropWindowSize(20, 20) - .setMinCropResultSize(16, 16) - .setMaxCropResultSize(999, 999) - .setActivityTitle("CUSTOM title") - .setActivityMenuIconColor(RED) - .setOutputUri(null) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setOutputCompressQuality(50) - .setRequestedSize(100, 100) - .setRequestedSize(100, 100, CropImageView.RequestSizeOptions.RESIZE_FIT) - .setInitialCropWindowRectangle(null) - .setInitialRotation(180) - .setAllowCounterRotation(true) - .setFlipHorizontally(true) - .setFlipVertically(true) - .setCropMenuCropButtonTitle("Custom name") - .setCropMenuCropButtonIcon(R.drawable.ic_gear_24) - .setAllowRotation(false) - .setNoOutputImage(false) - .setFixAspectRatio(true) - .start(ctx, this) - } + cropImage.launch( + options { + setScaleType(CropImageView.ScaleType.CENTER) + setCropShape(CropImageView.CropShape.OVAL) + setGuidelines(CropImageView.Guidelines.ON) + setAspectRatio(4, 16) + setMaxZoom(8) + setAutoZoomEnabled(false) + setMultiTouchEnabled(false) + setCenterMoveEnabled(true) + setShowCropOverlay(false) + setAllowFlipping(false) + setSnapRadius(10f) + setTouchRadius(30f) + setInitialCropWindowPaddingRatio(0.3f) + setBorderLineThickness(5f) + setBorderLineColor(R.color.black) + setBorderCornerThickness(6f) + setBorderCornerOffset(2f) + setBorderCornerLength(20f) + setBorderCornerColor(RED) + setGuidelinesThickness(5f) + setGuidelinesColor(RED) + setBackgroundColor(Color.argb(119, 30, 60, 90)) + setMinCropWindowSize(20, 20) + setMinCropResultSize(16, 16) + setMaxCropResultSize(999, 999) + setActivityTitle("CUSTOM title") + setActivityMenuIconColor(RED) + setOutputUri(null) + setOutputCompressFormat(Bitmap.CompressFormat.PNG) + setOutputCompressQuality(50) + setRequestedSize(100, 100) + setRequestedSize(100, 100, CropImageView.RequestSizeOptions.RESIZE_FIT) + setInitialCropWindowRectangle(null) + setInitialRotation(180) + setAllowCounterRotation(true) + setFlipHorizontally(true) + setFlipVertically(true) + setCropMenuCropButtonTitle("Custom name") + setCropMenuCropButtonIcon(R.drawable.ic_gear_24) + setAllowRotation(false) + setNoOutputImage(false) + setFixAspectRatio(true) + } + ) } private fun startCameraWithUri() { - context?.let { ctx -> - CropImage.activity(photoUri) - .setScaleType(CropImageView.ScaleType.FIT_CENTER) - .setCropShape(CropImageView.CropShape.RECTANGLE) - .setGuidelines(CropImageView.Guidelines.ON_TOUCH) - .setAspectRatio(1, 1) - .setMaxZoom(4) - .setAutoZoomEnabled(true) - .setMultiTouchEnabled(true) - .setCenterMoveEnabled(true) - .setShowCropOverlay(true) - .setAllowFlipping(true) - .setSnapRadius(3f) - .setTouchRadius(48f) - .setInitialCropWindowPaddingRatio(0.1f) - .setBorderLineThickness(3f) - .setBorderLineColor(Color.argb(170, 255, 255, 255)) - .setBorderCornerThickness(2f) - .setBorderCornerOffset(5f) - .setBorderCornerLength(14f) - .setBorderCornerColor(WHITE) - .setGuidelinesThickness(1f) - .setGuidelinesColor(R.color.white) - .setBackgroundColor(Color.argb(119, 0, 0, 0)) - .setMinCropWindowSize(24, 24) - .setMinCropResultSize(20, 20) - .setMaxCropResultSize(99999, 99999) - .setActivityTitle("") - .setActivityMenuIconColor(0) - .setOutputUri(null) - .setOutputCompressFormat(Bitmap.CompressFormat.JPEG) - .setOutputCompressQuality(90) - .setRequestedSize(0, 0) - .setRequestedSize(0, 0, CropImageView.RequestSizeOptions.RESIZE_INSIDE) - .setInitialCropWindowRectangle(null) - .setInitialRotation(90) - .setAllowCounterRotation(false) - .setFlipHorizontally(false) - .setFlipVertically(false) - .setCropMenuCropButtonTitle(null) - .setCropMenuCropButtonIcon(0) - .setAllowRotation(true) - .setNoOutputImage(false) - .setFixAspectRatio(false) - .start(ctx, this) - } + cropImage.launch( + options(photoUri) { + setScaleType(CropImageView.ScaleType.FIT_CENTER) + setCropShape(CropImageView.CropShape.RECTANGLE) + setGuidelines(CropImageView.Guidelines.ON_TOUCH) + setAspectRatio(1, 1) + setMaxZoom(4) + setAutoZoomEnabled(true) + setMultiTouchEnabled(true) + setCenterMoveEnabled(true) + setShowCropOverlay(true) + setAllowFlipping(true) + setSnapRadius(3f) + setTouchRadius(48f) + setInitialCropWindowPaddingRatio(0.1f) + setBorderLineThickness(3f) + setBorderLineColor(Color.argb(170, 255, 255, 255)) + setBorderCornerThickness(2f) + setBorderCornerOffset(5f) + setBorderCornerLength(14f) + setBorderCornerColor(WHITE) + setGuidelinesThickness(1f) + setGuidelinesColor(R.color.white) + setBackgroundColor(Color.argb(119, 0, 0, 0)) + setMinCropWindowSize(24, 24) + setMinCropResultSize(20, 20) + setMaxCropResultSize(99999, 99999) + setActivityTitle("") + setActivityMenuIconColor(0) + setOutputUri(null) + setOutputCompressFormat(Bitmap.CompressFormat.JPEG) + setOutputCompressQuality(90) + setRequestedSize(0, 0) + setRequestedSize(0, 0, CropImageView.RequestSizeOptions.RESIZE_INSIDE) + setInitialCropWindowRectangle(null) + setInitialRotation(90) + setAllowCounterRotation(false) + setFlipHorizontally(false) + setFlipVertically(false) + setCropMenuCropButtonTitle(null) + setCropMenuCropButtonIcon(0) + setAllowRotation(true) + setNoOutputImage(false) + setFixAspectRatio(false) + } + ) } override fun showErrorMessage(message: String) { @@ -209,16 +231,11 @@ internal class SCameraFragment : Toast.makeText(activity, "Crop failed: $message", Toast.LENGTH_SHORT).show() } - override fun dispatchTakePictureIntent() { - Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent -> - context?.let { ctx -> - if (takePictureIntent.resolveActivity(ctx.packageManager) != null) { - val authorities = "${ctx.applicationContext?.packageName}$AUTHORITY_SUFFIX" - photoUri = FileProvider.getUriForFile(ctx, authorities, createImageFile()) - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri) - startActivityForResult(takePictureIntent, CODE_PHOTO_CAMERA) - } - } + override fun startTakePicture() { + context?.let { ctx -> + val authorities = "${ctx.applicationContext?.packageName}$AUTHORITY_SUFFIX" + photoUri = FileProvider.getUriForFile(ctx, authorities, createImageFile()) + takePicture.launch(photoUri) } } @@ -241,11 +258,6 @@ internal class SCameraFragment : SCropResultActivity.start(this, null, Uri.parse(uri), null) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - presenter.onActivityResult(resultCode, requestCode, data) - } - private fun createImageFile(): File { val timeStamp = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(Date()) val storageDir: File? = activity?.getExternalFilesDir(Environment.DIRECTORY_PICTURES) diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraContract.kt b/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraContract.kt index 6a07b018..9258a7e9 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraContract.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraContract.kt @@ -1,15 +1,16 @@ package com.canhub.cropper.sample.camera.domain import android.content.Context -import android.content.Intent +import android.net.Uri import androidx.fragment.app.FragmentActivity +import com.canhub.cropper.CropImageView internal interface SCameraContract { interface View { fun startCropImage(option: CameraEnumDomain) fun showErrorMessage(message: String) - fun dispatchTakePictureIntent() + fun startTakePicture() fun cameraPermissionLaunch() fun showDialog() fun handleCropImageResult(uri: String) @@ -22,10 +23,13 @@ internal interface SCameraContract { fun onCreate(activity: FragmentActivity?, context: Context?) fun onOk() fun onCancel() - fun onActivityResult(resultCode: Int, requestCode: Int, data: Intent?) + fun onCropImageResult(result: CropImageView.CropResult) + fun onPickImageResult(resultUri: Uri?) + fun onPickImageResultCustom(resultUri: Uri?) + fun onTakePictureResult(success: Boolean) fun startWithUriClicked() fun startWithoutUriClicked() fun startPickImageActivityClicked() - fun startActivityForResultClicked() + fun startPickImageActivityCustomClicked() } } diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraDomain.kt b/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraDomain.kt index 2b2799b5..f282003b 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraDomain.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/camera/domain/SCameraDomain.kt @@ -1,5 +1,5 @@ package com.canhub.cropper.sample.camera.domain internal enum class CameraEnumDomain { - START_WITH_URI, START_WITHOUT_URI, START_PICK_IMG, START_FOR_RESULT + START_WITH_URI, START_WITHOUT_URI, START_PICK_IMG, START_PICK_IMG_CUSTOM } diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera/presenter/SCameraPresenter.kt b/sample/src/main/java/com/canhub/cropper/sample/camera/presenter/SCameraPresenter.kt index a8867088..6f60203d 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera/presenter/SCameraPresenter.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/camera/presenter/SCameraPresenter.kt @@ -2,17 +2,15 @@ package com.canhub.cropper.sample.camera.presenter import android.Manifest import android.app.Activity -import android.app.Activity.RESULT_OK import android.content.Context -import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import com.canhub.cropper.CropImage -import com.canhub.cropper.sample.camera.app.SCameraFragment -import com.canhub.cropper.sample.camera.app.SCameraFragment.Companion.CODE_PHOTO_CAMERA +import com.canhub.cropper.CropImageView import com.canhub.cropper.sample.camera.domain.CameraEnumDomain import com.canhub.cropper.sample.camera.domain.SCameraContract @@ -36,7 +34,7 @@ internal class SCameraPresenter : SCameraContract.Presenter { override fun onPermissionResult(granted: Boolean) { view?.apply { when { - granted -> dispatchTakePictureIntent() + granted -> startTakePicture() minVersion && request -> showDialog() else -> cameraPermissionLaunch() } @@ -64,7 +62,7 @@ internal class SCameraPresenter : SCameraContract.Presenter { override fun startWithUriClicked() { view?.apply { when { - hasSystemFeature && selfPermission -> dispatchTakePictureIntent() + hasSystemFeature && selfPermission -> startTakePicture() hasSystemFeature && minVersion && request -> showDialog() hasSystemFeature -> cameraPermissionLaunch() else -> showErrorMessage("onCreate no case apply") @@ -80,8 +78,8 @@ internal class SCameraPresenter : SCameraContract.Presenter { view?.startCropImage(CameraEnumDomain.START_PICK_IMG) } - override fun startActivityForResultClicked() { - view?.startCropImage(CameraEnumDomain.START_FOR_RESULT) + override fun startPickImageActivityCustomClicked() { + view?.startCropImage(CameraEnumDomain.START_PICK_IMG_CUSTOM) } override fun onOk() { @@ -92,41 +90,41 @@ internal class SCameraPresenter : SCameraContract.Presenter { view?.showErrorMessage("onCancel") } - override fun onActivityResult(resultCode: Int, requestCode: Int, data: Intent?) { - if (resultCode == RESULT_OK) { - when (requestCode) { - CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> { - val bitmap = context?.let { CropImage.getActivityResult(data)?.getBitmap(it) } - Log.v( - "File Path", - context - ?.let { CropImage.getActivityResult(data)?.getUriFilePath(it) } - .toString() - ) - - CropImage.getActivityResult(data)?.uriContent?.let { - view?.handleCropImageResult(it.toString().replace("file:", "")) - } ?: view?.showErrorMessage("CropImage getActivityResult return null") - } - SCameraFragment.CUSTOM_REQUEST_CODE -> { - context?.let { - Log.v("File Path", CropImage.getPickImageResultUriFilePath(it, data)) - CropImage.getPickImageResultUriFilePath(it, data) - val uri = CropImage.getPickImageResultUriContent(it, data) - view?.handleCropImageResult(uri.toString().replace("file:", "")) - } - } - CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE -> { - context?.let { ctx -> - Log.v("File Path", CropImage.getPickImageResultUriFilePath(ctx, data)) - val uri = CropImage.getPickImageResultUriContent(ctx, data) - - view?.handleCropImageResult(uri.toString()) - } - } - CODE_PHOTO_CAMERA -> view?.startCropImage(CameraEnumDomain.START_WITH_URI) - else -> view?.showErrorMessage("requestCode = $requestCode") - } - } else view?.showErrorMessage("resultCode = $resultCode") + override fun onCropImageResult(result: CropImageView.CropResult) { + if (result.isSuccessful) { + val bitmap = result.bitmap + Log.v("File Path", context?.let { result.getUriFilePath(it) }.toString()) + view?.handleCropImageResult(result.uriContent.toString().replace("file:", "")) + } else if (result is CropImage.CancelledResult) { + view?.showErrorMessage("cropping image was cancelled by the user") + } else { + view?.showErrorMessage("cropping image failed") + } + } + + override fun onPickImageResult(resultUri: Uri?) { + if (resultUri != null) { + Log.v("Uri", resultUri.toString()) + view?.handleCropImageResult(resultUri.toString()) + } else { + view?.showErrorMessage("picking image failed") + } + } + + override fun onPickImageResultCustom(resultUri: Uri?) { + if (resultUri != null) { + Log.v("File Path", resultUri.toString()) + view?.handleCropImageResult(resultUri.toString()) + } else { + view?.showErrorMessage("picking image failed") + } + } + + override fun onTakePictureResult(success: Boolean) { + if (success) { + view?.startCropImage(CameraEnumDomain.START_WITH_URI) + } else { + view?.showErrorMessage("taking picture failed") + } } } diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera_java/app/SCameraFragmentJava.java b/sample/src/main/java/com/canhub/cropper/sample/camera_java/app/SCameraFragmentJava.java index 3df70526..88b5797f 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera_java/app/SCameraFragmentJava.java +++ b/sample/src/main/java/com/canhub/cropper/sample/camera_java/app/SCameraFragmentJava.java @@ -2,7 +2,6 @@ import android.Manifest; import android.app.AlertDialog; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -10,7 +9,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import android.provider.MediaStore; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -25,7 +23,11 @@ import androidx.fragment.app.Fragment; import com.canhub.cropper.CropImage; +import com.canhub.cropper.CropImageContract; +import com.canhub.cropper.CropImageContractOptions; +import com.canhub.cropper.CropImageOptions; import com.canhub.cropper.CropImageView; +import com.canhub.cropper.PickImageContract; import com.canhub.cropper.sample.SCropResultActivity; import com.canhub.cropper.sample.camera_java.domain.CameraEnumDomainJava; import com.canhub.cropper.sample.camera_java.domain.SCameraContractJava; @@ -45,13 +47,11 @@ public class SCameraFragmentJava extends Fragment implements SCameraContractJava.View { - public static final int CODE_PHOTO_CAMERA = 811917; static final String DATE_FORMAT = "yyyyMMdd_HHmmss"; static final String FILE_NAMING_PREFIX = "JPEG_"; static final String FILE_NAMING_SUFFIX = "_"; static final String FILE_FORMAT = ".jpg"; static final String AUTHORITY_SUFFIX = ".fileprovider"; - public static final int CUSTOM_REQUEST_CODE = 8119153; private FragmentCameraBinding binding; private final SCameraContractJava.Presenter presenter = new SCameraPresenterJava(); @@ -59,6 +59,32 @@ public class SCameraFragmentJava extends Fragment implements SCameraContractJava private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), presenter::onPermissionResult); + private final ActivityResultLauncher pickImage = + registerForActivityResult(new PickImageContract(), presenter::onPickImageResult); + + private final ActivityResultLauncher pickImageCustom = + registerForActivityResult(new PickImageContract() { + + @Override + @Nullable + public Uri parseResult(int resultCode, @Nullable Intent intent) { + if (intent != null) { + Uri result = Uri.parse(CropImage.getPickImageResultUriFilePath(requireContext(), intent, false)); + setContext(null); + return result; + } + + setContext(null); + return null; + } + }, presenter::onPickImageResultCustom); + + private final ActivityResultLauncher cropImage = + registerForActivityResult(new CropImageContract(), presenter::onCropImageResult); + + private final ActivityResultLauncher takePicture = + registerForActivityResult(new ActivityResultContracts.TakePicture(), presenter::onTakePictureResult); + public static SCameraFragmentJava newInstance() { return new SCameraFragmentJava(); } @@ -82,7 +108,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.startPickImageActivity.setOnClickListener(v -> presenter.startPickImageActivityClicked()); - binding.startActivityForResult.setOnClickListener(v -> presenter.startActivityForResultClicked()); + binding.startActivityForResult.setOnClickListener(v -> presenter.startPickImageActivityCustomClicked()); presenter.onCreate(getActivity(), getContext()); } @@ -99,31 +125,24 @@ public void startCropImage(@NotNull CameraEnumDomainJava option) { case START_PICK_IMG: startPickImage(); break; - case START_FOR_RESULT: - startForResult(); + case START_PICK_IMG_CUSTOM: + startPickImageCustom(); break; default: break; } } - private void startForResult() { - assert (getContext() != null); - Intent intent = CropImage.getPickImageChooserIntent(getContext(),"Selection Baby", true, false); - startActivityForResult(intent, CUSTOM_REQUEST_CODE); - + private void startPickImageCustom() { + pickImageCustom.launch(false); } private void startPickImage() { - assert (getContext() != null); - CropImage.activity() - .start(getContext(), this); + pickImage.launch(false); } private void startCameraWithoutUri() { - assert (getContext() != null); - Context ctx = getContext(); - CropImage.activity() + CropImageContractOptions options = new CropImageContractOptions(null, new CropImageOptions()) .setScaleType(CropImageView.ScaleType.CENTER) .setCropShape(CropImageView.CropShape.OVAL) .setGuidelines(CropImageView.Guidelines.ON) @@ -165,14 +184,13 @@ private void startCameraWithoutUri() { .setCropMenuCropButtonIcon(R.drawable.ic_gear_24) .setAllowRotation(false) .setNoOutputImage(false) - .setFixAspectRatio(true) - .start(ctx, this); + .setFixAspectRatio(true); + + cropImage.launch(options); } private void startCameraWithUri() { - assert (getContext() != null); - Context ctx = getContext(); - CropImage.activity(photoUri) + CropImageContractOptions options = new CropImageContractOptions(photoUri, new CropImageOptions()) .setScaleType(CropImageView.ScaleType.FIT_CENTER) .setCropShape(CropImageView.CropShape.RECTANGLE) .setGuidelines(CropImageView.Guidelines.ON_TOUCH) @@ -214,8 +232,8 @@ private void startCameraWithUri() { .setCropMenuCropButtonIcon(0) .setAllowRotation(true) .setNoOutputImage(false) - .setFixAspectRatio(false) - .start(ctx, this); + .setFixAspectRatio(false); + cropImage.launch(options); } @Override @@ -225,19 +243,12 @@ public void showErrorMessage(@NotNull String message) { } @Override - public void dispatchTakePictureIntent() { - assert (getContext() != null); - Context ctx = getContext(); - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + public void startTakePicture() { try { - if (takePictureIntent.resolveActivity(ctx.getPackageManager()) != null) { - String authorities = getContext().getPackageName() + AUTHORITY_SUFFIX; - photoUri = FileProvider.getUriForFile(ctx, authorities, createImageFile()); - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); - startActivityForResult(takePictureIntent, CODE_PHOTO_CAMERA); - } - } catch (ActivityNotFoundException e) { - // display error state to the user + Context ctx = requireContext(); + String authorities = ctx.getPackageName() + AUTHORITY_SUFFIX; + photoUri = FileProvider.getUriForFile(ctx, authorities, createImageFile()); + takePicture.launch(photoUri); } catch (IOException e) { e.printStackTrace(); } @@ -257,7 +268,6 @@ public void showDialog() { alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> presenter.onCancel()); AlertDialog alertDialog = alertDialogBuilder.create(); alertDialog.show(); - } @Override @@ -265,18 +275,11 @@ public void handleCropImageResult(@NotNull String uri) { SCropResultActivity.Companion.start(this, null, Uri.parse(uri), null); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - presenter.onActivityResult(resultCode, requestCode, data); - } - private File createImageFile() throws IOException { - assert getActivity() != null; SimpleDateFormat timeStamp = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault()); - File storageDir = getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File storageDir = requireActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES); return File.createTempFile( - FILE_NAMING_PREFIX + FILE_NAMING_SUFFIX, + FILE_NAMING_PREFIX + timeStamp + FILE_NAMING_SUFFIX, FILE_FORMAT, storageDir ); diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/CameraEnumDomainJava.java b/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/CameraEnumDomainJava.java index fab991c5..bd83bd87 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/CameraEnumDomainJava.java +++ b/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/CameraEnumDomainJava.java @@ -1,5 +1,5 @@ package com.canhub.cropper.sample.camera_java.domain; public enum CameraEnumDomainJava { - START_WITH_URI, START_WITHOUT_URI, START_PICK_IMG, START_FOR_RESULT; + START_WITH_URI, START_WITHOUT_URI, START_PICK_IMG, START_PICK_IMG_CUSTOM } diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/SCameraContractJava.java b/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/SCameraContractJava.java index dc5b19fb..468ee43c 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/SCameraContractJava.java +++ b/sample/src/main/java/com/canhub/cropper/sample/camera_java/domain/SCameraContractJava.java @@ -1,10 +1,14 @@ package com.canhub.cropper.sample.camera_java.domain; import android.content.Context; -import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import com.canhub.cropper.CropImageView; + public interface SCameraContractJava { interface View { @@ -12,7 +16,7 @@ interface View { void showErrorMessage(String message); - void dispatchTakePictureIntent(); + void startTakePicture(); void cameraPermissionLaunch(); @@ -34,7 +38,13 @@ interface Presenter { void onCancel(); - void onActivityResult(int resultCode, int requestCode, Intent data); + void onCropImageResult(@NonNull CropImageView.CropResult result); + + void onPickImageResult(@Nullable Uri resultUri); + + void onPickImageResultCustom(@Nullable Uri resultUri); + + void onTakePictureResult(boolean success); void startWithUriClicked(); @@ -42,6 +52,6 @@ interface Presenter { void startPickImageActivityClicked(); - void startActivityForResultClicked(); + void startPickImageActivityCustomClicked(); } } diff --git a/sample/src/main/java/com/canhub/cropper/sample/camera_java/presenter/SCameraPresenterJava.java b/sample/src/main/java/com/canhub/cropper/sample/camera_java/presenter/SCameraPresenterJava.java index 4b720617..14d914e8 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/camera_java/presenter/SCameraPresenterJava.java +++ b/sample/src/main/java/com/canhub/cropper/sample/camera_java/presenter/SCameraPresenterJava.java @@ -2,32 +2,28 @@ import android.Manifest; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Bitmap; import android.net.Uri; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import com.canhub.cropper.CropImage; +import com.canhub.cropper.CropImageView; import com.canhub.cropper.common.CommonVersionCheck; -import com.canhub.cropper.sample.camera_java.app.SCameraFragmentJava; import com.canhub.cropper.sample.camera_java.domain.CameraEnumDomainJava; import com.canhub.cropper.sample.camera_java.domain.SCameraContractJava; -import static android.app.Activity.RESULT_OK; -import static com.canhub.cropper.sample.camera_java.app.SCameraFragmentJava.CODE_PHOTO_CAMERA; - public class SCameraPresenterJava implements SCameraContractJava.Presenter { private SCameraContractJava.View view = null; private boolean minVersion = CommonVersionCheck.INSTANCE.isAtLeastM23(); private boolean request = false; private boolean hasSystemFeature = false; private boolean selfPermission = false; - private Context context = null; @Override public void bind(SCameraContractJava.View view) { @@ -43,7 +39,7 @@ public void unbind() { public void onPermissionResult(boolean granted) { assert view != null; if (granted) { - view.dispatchTakePictureIntent(); + view.startTakePicture(); } else if (minVersion && request) { view.showDialog(); } else { @@ -58,7 +54,6 @@ public void onCreate(FragmentActivity activity, Context context) { view.showErrorMessage("onCreate activity and/or context are null"); return; } - this.context = context; request = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA); if (context.getPackageManager() != null) { @@ -73,7 +68,7 @@ public void onCreate(FragmentActivity activity, Context context) { public void startWithUriClicked() { assert view != null; if (hasSystemFeature && selfPermission) { - view.dispatchTakePictureIntent(); + view.startTakePicture(); } else if (hasSystemFeature && minVersion && request) { view.showDialog(); } else if (hasSystemFeature) { @@ -96,9 +91,9 @@ public void startPickImageActivityClicked() { } @Override - public void startActivityForResultClicked() { + public void startPickImageActivityCustomClicked() { assert view != null; - view.startCropImage(CameraEnumDomainJava.START_FOR_RESULT); + view.startCropImage(CameraEnumDomainJava.START_PICK_IMG_CUSTOM); } @Override @@ -114,57 +109,42 @@ public void onCancel() { } @Override - public void onActivityResult(int resultCode, int requestCode, Intent data) { - assert view != null; - if (resultCode == RESULT_OK) { - switch (requestCode) { - case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: { - assert (context != null); - Bitmap bitmap = CropImage.getActivityResult(data).getBitmap(context); - Log.v( - "File Path", - CropImage.getActivityResult(data).getUriFilePath(context, false) - ); - - Uri uriContent = CropImage.getActivityResult(data).getUriContent(); - if (uriContent != null && !CropImage.isReadExternalStoragePermissionsRequired(context, uriContent)) { - view.handleCropImageResult(uriContent.toString().replace("file:", "")); - } else { - view.showErrorMessage("CropImage getActivityResult return null"); - } - break; - } - case SCameraFragmentJava.CUSTOM_REQUEST_CODE: { - assert context != null; - Log.v("File Path", CropImage.getPickImageResultUriFilePath(context, data, false)); - CropImage.getPickImageResultUriFilePath(context, data, false); - Uri uri = CropImage.getPickImageResultUriContent(context, data); - if (view != null) { - view.handleCropImageResult(uri.toString().replace("file:", "")); - } - - break; - } - case CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE: { - assert context != null; - Log.v("File Path", CropImage.getPickImageResultUriFilePath(context, data, false)); - Uri uri = CropImage.getPickImageResultUriContent(context, data); - if (view != null) { - view.handleCropImageResult(uri.toString()); - } - break; - } - case CODE_PHOTO_CAMERA: { - view.startCropImage(CameraEnumDomainJava.START_WITH_URI); - break; - } - default: { - view.showErrorMessage("requestCode = " + requestCode); - break; - } - } + public void onCropImageResult(@NonNull CropImageView.CropResult result) { + if (result.isSuccessful()) { + view.handleCropImageResult(result.getUriContent().toString().replace("file:", "")); + } else if (result.equals(CropImage.CancelledResult.INSTANCE)) { + view.showErrorMessage("cropping image was cancelled by the user"); + } else { + view.showErrorMessage("cropping image failed"); + } + } + + @Override + public void onPickImageResult(@Nullable Uri resultUri) { + if (resultUri != null) { + Log.v("Uri", resultUri.toString()); + view.handleCropImageResult(resultUri.toString()); + } else { + view.showErrorMessage("picking image failed"); + } + } + + @Override + public void onPickImageResultCustom(@Nullable Uri resultUri) { + if (resultUri != null) { + Log.v("File Path", resultUri.toString()); + view.handleCropImageResult(resultUri.toString()); + } else { + view.showErrorMessage("picking image failed"); + } + } + + @Override + public void onTakePictureResult(boolean success) { + if (success) { + view.startCropImage(CameraEnumDomainJava.START_WITH_URI); } else { - view.showErrorMessage("resultCode = " + resultCode); + view.showErrorMessage("taking picture failed"); } } } \ No newline at end of file diff --git a/sample/src/main/java/com/canhub/cropper/sample/crop_image_view/app/SCropImageViewFragment.kt b/sample/src/main/java/com/canhub/cropper/sample/crop_image_view/app/SCropImageViewFragment.kt index 05327b55..a19d47a9 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/crop_image_view/app/SCropImageViewFragment.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/crop_image_view/app/SCropImageViewFragment.kt @@ -1,7 +1,6 @@ package com.canhub.cropper.sample.crop_image_view.app import android.Manifest -import android.content.Intent import android.content.pm.PackageManager import android.graphics.Rect import android.net.Uri @@ -14,13 +13,13 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.canhub.cropper.CropImage import com.canhub.cropper.CropImageView import com.canhub.cropper.CropImageView.CropResult import com.canhub.cropper.CropImageView.OnCropImageCompleteListener import com.canhub.cropper.CropImageView.OnSetImageUriCompleteListener +import com.canhub.cropper.PickImageContract import com.canhub.cropper.sample.SCropResultActivity import com.canhub.cropper.sample.crop_image_view.domain.SCropImageViewContract import com.canhub.cropper.sample.crop_image_view.presenter.SCropImageViewPresenter @@ -46,6 +45,19 @@ internal class SCropImageViewFragment : private var options: SOptionsDomain? = null private var cropImageUri: Uri? = null + private val openPicker = registerForActivityResult(PickImageContract()) { imageUri -> + if (imageUri != null && CropImage.isReadExternalStoragePermissionsRequired(requireContext(), imageUri) + ) { + cropImageUri = imageUri + requestPermissions( + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE + ) + } else { + binding.cropImageView.setImageUriAsync(imageUri) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -79,7 +91,7 @@ internal class SCropImageViewFragment : arrayOf(Manifest.permission.CAMERA), CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE ) - } else context?.let { context -> CropImage.startPickImageActivity(context, this) } + } else openPicker.launch(true) } } @@ -169,33 +181,6 @@ internal class SCropImageViewFragment : handleCropResult(result) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == AppCompatActivity.RESULT_OK) { - when (requestCode) { - CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> - handleCropResult(CropImage.getActivityResult(data)) - CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE -> { - val ctx = context - ctx?.let { Log.v("File Path", CropImage.getPickImageResultUriFilePath(it, data)) } - val imageUri = ctx?.let { CropImage.getPickImageResultUriContent(it, data) } - - if (imageUri != null && - CropImage.isReadExternalStoragePermissionsRequired(ctx, imageUri) - ) { - cropImageUri = imageUri - requestPermissions( - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE - ) - } else { - binding.cropImageView.setImageUriAsync(imageUri) - } - } - } - } - } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -203,7 +188,7 @@ internal class SCropImageViewFragment : ) { if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - activity?.let { CropImage.startPickImageActivity(it) } + openPicker.launch(true) } else { Toast .makeText(context, "Cancelling, permissions not granted", Toast.LENGTH_LONG) diff --git a/sample/src/main/java/com/canhub/cropper/sample/extend_activity/app/SExtendActivity.kt b/sample/src/main/java/com/canhub/cropper/sample/extend_activity/app/SExtendActivity.kt index 0284cb04..1e8622f6 100644 --- a/sample/src/main/java/com/canhub/cropper/sample/extend_activity/app/SExtendActivity.kt +++ b/sample/src/main/java/com/canhub/cropper/sample/extend_activity/app/SExtendActivity.kt @@ -18,7 +18,6 @@ import com.example.croppersample.databinding.ExtendedActivityBinding internal class SExtendActivity : CropImageActivity(), SExtendContract.View { companion object { - fun start(activity: Activity) { ActivityCompat.startActivity( activity, @@ -68,11 +67,11 @@ internal class SExtendActivity : CropImageActivity(), SExtendContract.View { binding.rotateText.text = getString(R.string.rotation_value, counter) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) + override fun onPickImageResult(resultUri: Uri?) { + super.onPickImageResult(resultUri) - if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE && resultCode == RESULT_OK) { - binding.cropImageView.setImageUriAsync(cropImageUri) + if (resultUri != null) { + binding.cropImageView.setImageUriAsync(resultUri) } } diff --git a/sample/src/main/res/layout/fragment_camera.xml b/sample/src/main/res/layout/fragment_camera.xml index 8f80a2fe..10fe67a8 100644 --- a/sample/src/main/res/layout/fragment_camera.xml +++ b/sample/src/main/res/layout/fragment_camera.xml @@ -28,31 +28,31 @@ android:layout_margin="@dimen/keyline_x4" android:text="@string/startWithoutUri_text" app:elevation="@dimen/elevation_fab_resting_or_snackbar" - app:layout_constraintBottom_toTopOf="@id/startPickImageActivity" + app:layout_constraintBottom_toTopOf="@id/startActivityForResult" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:pressedTranslationZ="@dimen/elevation_fab_pressed" />