├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── README.md
├── app
├── build.gradle
├── project.properties
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── com
│ │ └── aemerse
│ │ └── cropper
│ │ └── app
│ │ ├── SCropResultActivity.kt
│ │ ├── SMainActivity.kt
│ │ ├── crop_image
│ │ ├── app
│ │ │ └── SCropImageFragment.kt
│ │ ├── domain
│ │ │ └── SCropImageContract.kt
│ │ └── presenter
│ │ │ └── SCropImagePresenter.kt
│ │ ├── crop_image_view
│ │ ├── app
│ │ │ └── SCropImageViewFragment.kt
│ │ ├── domain
│ │ │ └── SCropImageViewContract.kt
│ │ └── presenter
│ │ │ └── SCropImageViewPresenter.kt
│ │ ├── extend_activity
│ │ ├── app
│ │ │ └── SExtendActivity.kt
│ │ ├── domain
│ │ │ └── SExtendContract.kt
│ │ └── presenter
│ │ │ └── SExtendPresenter.kt
│ │ └── options_dialog
│ │ ├── app
│ │ ├── SOptionsDialogBottomSheet.kt
│ │ └── SOptionsServiceLocator.kt
│ │ ├── domain
│ │ ├── SOptionsContract.kt
│ │ └── SOptionsDomain.kt
│ │ └── presenter
│ │ └── SOptionsPresenter.kt
│ └── res
│ ├── animator
│ └── toolbar_elevation.xml
│ ├── color
│ ├── chip_bg_states.xml
│ ├── chip_text_states.xml
│ ├── switch_thumb_selector.xml
│ └── switch_track_selector.xml
│ ├── drawable
│ ├── backdrop.xml
│ ├── bg_bottom_sheet_rounded.xml
│ ├── bg_draggable_view.xml
│ ├── bg_purple_gradient.xml
│ ├── cat.jpg
│ ├── cat_small.jpg
│ ├── checkerboard.xml
│ ├── checktile.png
│ ├── crop.png
│ ├── ic_arrow_back_24.xml
│ ├── ic_gear_24.xml
│ ├── ic_mage_search_24.xml
│ └── muted.xml
│ ├── layout
│ ├── activity_crop_result.xml
│ ├── activity_main.xml
│ ├── chip_crop_shape.xml
│ ├── chip_guidelines.xml
│ ├── chip_max_zoom.xml
│ ├── chip_ratio.xml
│ ├── chip_scale_type.xml
│ ├── extended_activity.xml
│ ├── fragment_camera.xml
│ ├── fragment_crop_image_view.xml
│ ├── fragment_options.xml
│ ├── switch_auto_zoom.xml
│ ├── switch_center_move.xml
│ ├── switch_crop_overlay.xml
│ ├── switch_flip_horizontal.xml
│ ├── switch_flip_vertical.xml
│ ├── switch_multi_touch.xml
│ └── switch_progress_bar.xml
│ ├── menu
│ └── main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ ├── styles.xml
│ └── themes.xml
├── build.gradle
├── cropper
├── build.gradle
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── aemerse
│ │ │ └── cropper
│ │ │ ├── BitmapCroppingWorkerJob.kt
│ │ │ ├── BitmapLoadingWorkerJob.kt
│ │ │ ├── BitmapUtils.kt
│ │ │ ├── CropFileProvider.kt
│ │ │ ├── CropImage.kt
│ │ │ ├── CropImageActivity.kt
│ │ │ ├── CropImageAnimation.kt
│ │ │ ├── CropImageContract.kt
│ │ │ ├── CropImageContractOptions.kt
│ │ │ ├── CropImageOptions.kt
│ │ │ ├── CropImageView.kt
│ │ │ ├── CropOverlayView.kt
│ │ │ ├── CropWindowHandler.kt
│ │ │ ├── CropWindowMoveHandler.kt
│ │ │ ├── common
│ │ │ ├── CommonValues.kt
│ │ │ └── CommonVersionCheck.kt
│ │ │ └── utils
│ │ │ ├── GetFilePathFromUri.kt
│ │ │ └── GetUriForFile.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_flip_24.xml
│ │ ├── ic_rotate_left_24.xml
│ │ └── ic_rotate_right_24.xml
│ │ ├── layout
│ │ ├── crop_image_activity.xml
│ │ └── crop_image_view.xml
│ │ ├── menu
│ │ └── crop_image_menu.xml
│ │ ├── values
│ │ ├── attrs.xml
│ │ └── strings.xml
│ │ └── xml
│ │ └── library_file_paths.xml
│ └── test
│ └── java
│ └── com
│ └── aemerse
│ └── cropper
│ ├── BitmapUtilsTest.kt
│ └── ContractTestFragment.kt
├── gradle.properties
├── gradle
├── gradlew
├── gradlew.bat
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── local.properties
└── settings.gradle
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: set up JDK 11
17 | uses: actions/setup-java@v2
18 | with:
19 | java-version: '11'
20 | distribution: 'adopt'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x gradlew
25 | - name: Build with Gradle
26 | run: ./gradlew build
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /.gradle/
3 | /app/build/
4 | /app/build/intermediates/javac/debug/classes/
5 | /cropper/build/
6 | /cropper/build/intermediates/javac/debug/classes/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Cropper - Android Image Cropper
2 |
3 |
4 |
6 |
7 |
8 |
9 |
11 |
13 |
14 |
15 | Android Image Cropper
16 | =======
17 | **Powerful** (Zoom, Rotation, Multi-Source);
18 | **Customizable** (Shape, Limits, Style);
19 | **Optimized** (Async, Sampling, Matrix);
20 | **Simple** image cropping library for Android.
21 |
22 | # Add to your project
23 |
24 | ### Step 1. Add the JitPack repository to your root build.gradle
25 |
26 | ```gradle
27 | allprojects {
28 | repositories {
29 | ....
30 | maven { url 'https://jitpack.io' }
31 | }
32 | }
33 | ```
34 |
35 | ### Step 2. Add the dependency
36 |
37 | ```gradle
38 | dependencies {
39 | implementation 'com.github.akshaaatt:Cropper:1.00'
40 | }
41 | ```
42 |
43 | # Using the Library
44 | There is 3 ways of using the library:
45 | - Calling crop directly (Sample code: `app/crop_image`)
46 | - Using the CropView (Sample code: `app/crop_image_view`)
47 | - Extending the activity (Sample code: `app/extend_activity`)
48 | Your choice depends on how you want your layout to look.
49 |
50 | Obs: The library has a public pick image contract, more on wiki.
51 |
52 | ## Calling crop directly
53 | - Register for activity result with `CropImageContract`
54 | ```kotlin
55 | class MainActivity {
56 | private val cropImage = registerForActivityResult(CropImageContract()) { result ->
57 | if (result.isSuccessful) {
58 | // use the returned uri
59 | val uriContent = result.uriContent
60 | val uriFilePath = result.getUriFilePath(context) // optional usage
61 | } else {
62 | // an error occurred
63 | val exception = result.error
64 | }
65 | }
66 |
67 | private fun startCrop() {
68 | // start picker to get image for cropping and then use the image in cropping activity
69 | cropImage.launch(
70 | options {
71 | setGuidelines(Guidelines.ON)
72 | }
73 | )
74 |
75 | //start picker to get image for cropping from only gallery and then use the image in
76 | //cropping activity
77 | cropImage.launch(
78 | options {
79 | setImagePickerContractOptions(
80 | PickImageContractOptions(includeGallery = true, includeCamera = false)
81 | )
82 | }
83 | )
84 |
85 | // start cropping activity for pre-acquired image saved on the device and customize settings
86 | cropImage.launch(
87 | options(uri = imageUri) {
88 | setGuidelines(Guidelines.ON)
89 | setOutputCompressFormat(CompressFormat.PNG)
90 | }
91 | )
92 | }
93 | }
94 | ```
95 |
96 | ## Using CropView
97 | 2. Add `CropImageView` into your activity
98 | ```xml
99 |
100 |
105 | ```
106 |
107 | 3. Set image to crop
108 | ```kotlin
109 | cropImageView.setImageUriAsync(uri)
110 | // or (prefer using uri for performance and better user experience)
111 | cropImageView.setImageBitmap(bitmap)
112 | ```
113 |
114 | 4. Get cropped image
115 | ```kotlin
116 | // subscribe to async event using cropImageView.setOnCropImageCompleteListener(listener)
117 | cropImageView.getCroppedImageAsync()
118 | // or
119 | val cropped: Bitmap = cropImageView.getCroppedImage()
120 | ```
121 |
122 | ## Extend to make a custom activity
123 | If you want to extend the `CropImageActivity` please be aware you will need to setup your `CropImageView`
124 | You can check a sample code in this project `com.aemerse.cropper.app.extend_activity.app.SExtendActivity`
125 |
126 | - Add `CropImageActivity` into your AndroidManifest.xml
127 | ```xml
128 |
130 | ```
131 | - Setup your `CropImageView` after call `super.onCreate(savedInstanceState)`
132 | ```kotlin
133 | override fun onCreate(savedInstanceState: Bundle?) {
134 | super.onCreate(savedInstanceState)
135 | setCropImageView(binding.cropImageView)
136 | }
137 | ```
138 |
139 | ### Custom dialog for image source pick
140 | When calling crop directly the library will prompt a dialog for the user choose between gallery or camera (If you keep both enable).
141 | We use the Android default AlertDialog for this. If you wanna customised it with your app theme you need to override the method `showImageSourceDialog(..)` when extending the activity _(above)_
142 | ```kotlin
143 | override fun showImageSourceDialog(openSource: (Source) -> Unit) {
144 | super.showImageSourceDialog(openCamera)
145 | }
146 | ```
147 |
148 | ## Features
149 | - Built-in `CropImageActivity`.
150 | - Set cropping image as Bitmap, Resource or Android URI (Gallery, Camera, Dropbox, etc.).
151 | - Image rotation/flipping during cropping.
152 | - Auto zoom-in/out to relevant cropping area.
153 | - Auto rotate bitmap by image Exif data.
154 | - Set result image min/max limits in pixels.
155 | - Set initial crop window size/location.
156 | - Request cropped image resize to specific size.
157 | - Bitmap memory optimization, OOM handling (should never occur)!
158 | - API Level 14.
159 | - More..
160 |
161 | ## Customizations
162 | - Cropping window shape: Rectangular, Oval (square/circle by fixing aspect ratio), as well as
163 | rectangular modes which only allow vertical or horizontal cropping.
164 | - Cropping window aspect ratio: Free, 1:1, 4:3, 16:9 or Custom.
165 | - Guidelines appearance: Off / Always On / Show on Touch.
166 | - Cropping window Border line, border corner and guidelines thickness and color.
167 | - Cropping background color.
168 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-parcelize'
5 | id 'maven-publish'
6 | }
7 |
8 | android {
9 | compileSdk 31
10 | defaultConfig {
11 | applicationId "com.aemerse.cropper.app"
12 | minSdk 21
13 | targetSdk 31
14 | versionCode 1
15 | versionName '1.0.0'
16 | }
17 | buildFeatures {
18 | viewBinding true
19 | }
20 | lintOptions {
21 | abortOnError false
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation "androidx.appcompat:appcompat:1.4.0"
27 | implementation "androidx.core:core-ktx:1.7.0"
28 | implementation "com.google.android.material:material:1.4.0"
29 |
30 | implementation project(':cropper')
31 | }
32 |
--------------------------------------------------------------------------------
/app/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 | # Project target.
13 | target=android-17
14 | android.library.reference.1=../cropper
15 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/SCropResultActivity.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.graphics.Bitmap
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.view.Window
9 | import android.widget.Toast
10 | import androidx.fragment.app.Fragment
11 | import com.aemerse.cropper.app.databinding.ActivityCropResultBinding
12 |
13 | class SCropResultActivity : Activity() {
14 |
15 | companion object {
16 | fun start(fragment: Fragment, imageBitmap: Bitmap?, uri: Uri?, sampleSize: Int?) {
17 | val intent = Intent(fragment.context, SCropResultActivity::class.java)
18 | .putExtra(SAMPLE_SIZE, sampleSize)
19 | .putExtra(URI, uri)
20 |
21 | image = imageBitmap
22 |
23 | fragment.startActivity(intent)
24 | }
25 |
26 | // This is used, because bitmap is huge and cannot be passed in Intent without throw and exceptiont
27 | var image: Bitmap? = null
28 |
29 | private const val SAMPLE_SIZE = "SAMPLE_SIZE"
30 | private const val URI = "URI"
31 | }
32 |
33 | private lateinit var binding: ActivityCropResultBinding
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | requestWindowFeature(Window.FEATURE_NO_TITLE)
38 | binding = ActivityCropResultBinding.inflate(layoutInflater)
39 | setContentView(binding.root)
40 |
41 | binding.resultImageView.setBackgroundResource(R.drawable.backdrop)
42 | binding.resultImageView.setOnClickListener {
43 | releaseBitmap()
44 | finish()
45 | }
46 |
47 | image?.let {
48 | binding.resultImageView.setImageBitmap(it)
49 | val sampleSize = intent.getIntExtra(SAMPLE_SIZE, 1)
50 | val ratio = (10 * it.width / it.height.toDouble()).toInt() / 10.0
51 | val byteCount: Int = it.byteCount / 1024
52 | val desc =
53 | "(${it.width}, ${it.height}), Sample: $sampleSize, Ratio: $ratio, Bytes: $byteCount K"
54 |
55 | binding.resultImageText.text = desc
56 | } ?: run {
57 | val imageUri = intent.getParcelableExtra(URI)
58 |
59 | if (imageUri != null) binding.resultImageView.setImageURI(imageUri)
60 | else Toast.makeText(this, "No image is set to show", Toast.LENGTH_LONG).show()
61 | }
62 | }
63 |
64 | override fun onBackPressed() {
65 | releaseBitmap()
66 | super.onBackPressed()
67 | }
68 |
69 | private fun releaseBitmap() {
70 | image?.let {
71 | it.recycle()
72 | image = null
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/SMainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.Fragment
6 | import com.aemerse.cropper.app.crop_image.app.SCropImageFragment
7 | import com.aemerse.cropper.app.crop_image_view.app.SCropImageViewFragment
8 | import com.aemerse.cropper.app.extend_activity.app.SExtendActivity
9 | import com.aemerse.cropper.app.databinding.ActivityMainBinding
10 |
11 | internal class SMainActivity : AppCompatActivity() {
12 |
13 | private lateinit var binding: ActivityMainBinding
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | binding = ActivityMainBinding.inflate(layoutInflater)
18 |
19 | setContentView(binding.root)
20 |
21 | binding.sampleCropImageView.setOnClickListener {
22 | SCropImageViewFragment.newInstance().show()
23 | }
24 |
25 | binding.sampleCustomActivity.setOnClickListener {
26 | SExtendActivity.start(this)
27 | }
28 |
29 | binding.sampleCropImage.setOnClickListener {
30 | SCropImageFragment.newInstance().show()
31 | }
32 | }
33 |
34 | private fun Fragment.show() {
35 | supportFragmentManager
36 | .beginTransaction()
37 | .replace(binding.container.id, this)
38 | .commit()
39 | }
40 |
41 | override fun onBackPressed() {
42 | supportFragmentManager.findFragmentById(binding.container.id)?.apply {
43 | supportFragmentManager.beginTransaction().remove(this).commit()
44 | return
45 | }
46 | super.onBackPressed()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image/app/SCropImageFragment.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image.app
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.Color
5 | import android.graphics.Color.WHITE
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.os.Environment
9 | import android.util.Log
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.widget.Toast
14 | import androidx.activity.result.contract.ActivityResultContracts
15 | import androidx.core.content.FileProvider
16 | import androidx.fragment.app.Fragment
17 | import com.aemerse.cropper.CropImageContract
18 | import com.aemerse.cropper.CropImageView
19 | import com.aemerse.cropper.options
20 | import com.aemerse.cropper.app.SCropResultActivity
21 | import com.aemerse.cropper.app.crop_image.domain.SCropImageContract
22 | import com.aemerse.cropper.app.crop_image.presenter.SCropImagePresenter
23 | import com.aemerse.cropper.app.R
24 | import com.aemerse.cropper.app.databinding.FragmentCameraBinding
25 | import java.io.File
26 | import java.text.SimpleDateFormat
27 | import java.util.Date
28 | import java.util.Locale
29 |
30 | internal class SCropImageFragment : Fragment(), SCropImageContract.View {
31 |
32 | companion object {
33 |
34 | fun newInstance() = SCropImageFragment()
35 |
36 | const val DATE_FORMAT = "yyyyMMdd_HHmmss"
37 | const val FILE_NAMING_PREFIX = "JPEG_"
38 | const val FILE_NAMING_SUFFIX = "_"
39 | const val FILE_FORMAT = ".jpg"
40 | const val AUTHORITY_SUFFIX = ".cropper.fileprovider"
41 | }
42 |
43 | private lateinit var binding: FragmentCameraBinding
44 | private val presenter: SCropImageContract.Presenter = SCropImagePresenter()
45 | private var outputUri: Uri? = null
46 | private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) {
47 | presenter.onTakePictureResult(it)
48 | }
49 | private val cropImage = registerForActivityResult(CropImageContract()) {
50 | presenter.onCropImageResult(it)
51 | }
52 | private val customCropImage = registerForActivityResult(CropImageContract()) {
53 | presenter.onCustomCropImageResult(outputUri)
54 | }
55 |
56 | override fun onCreateView(
57 | inflater: LayoutInflater,
58 | container: ViewGroup?,
59 | savedInstanceState: Bundle?
60 | ): View {
61 | binding = FragmentCameraBinding.inflate(layoutInflater, container, false)
62 | return binding.root
63 | }
64 |
65 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
66 | super.onViewCreated(view, savedInstanceState)
67 | presenter.bind(this)
68 |
69 | binding.takePictureBeforeCallLibraryWithUri.setOnClickListener { startTakePicture() }
70 | binding.callLibraryWithoutUri.setOnClickListener {
71 | startCameraWithoutUri(
72 | includeCamera = true,
73 | includeGallery = true,
74 | )
75 | }
76 | binding.callLibraryWithoutUriCameraOnly.setOnClickListener {
77 | startCameraWithoutUri(
78 | includeCamera = true,
79 | includeGallery = false,
80 | )
81 | }
82 | binding.callLibraryWithoutUriGalleryOnly.setOnClickListener {
83 | startCameraWithoutUri(
84 | includeCamera = false,
85 | includeGallery = true,
86 | )
87 | }
88 |
89 | presenter.onCreate(activity, context)
90 | }
91 |
92 | override fun onDestroyView() {
93 | presenter.unbind()
94 | super.onDestroyView()
95 | }
96 |
97 | private fun startCameraWithoutUri(includeCamera: Boolean, includeGallery: Boolean) {
98 | setupOutputUri()
99 |
100 | customCropImage.launch(
101 | options {
102 | setImageSource(
103 | includeGallery = includeGallery,
104 | includeCamera = includeCamera,
105 | )
106 | // Normal Settings
107 | setScaleType(CropImageView.ScaleType.FIT_CENTER)
108 | setCropShape(CropImageView.CropShape.RECTANGLE)
109 | setGuidelines(CropImageView.Guidelines.ON_TOUCH)
110 | setAspectRatio(1, 1)
111 | setMaxZoom(4)
112 | setAutoZoomEnabled(true)
113 | setMultiTouchEnabled(true)
114 | setCenterMoveEnabled(true)
115 | setShowCropOverlay(true)
116 | setAllowFlipping(true)
117 | setSnapRadius(3f)
118 | setTouchRadius(48f)
119 | setInitialCropWindowPaddingRatio(0.1f)
120 | setBorderLineThickness(3f)
121 | setBorderLineColor(Color.argb(170, 255, 255, 255))
122 | setBorderCornerThickness(2f)
123 | setBorderCornerOffset(5f)
124 | setBorderCornerLength(14f)
125 | setBorderCornerColor(WHITE)
126 | setGuidelinesThickness(1f)
127 | setGuidelinesColor(R.color.white)
128 | setBackgroundColor(Color.argb(119, 0, 0, 0))
129 | setMinCropWindowSize(24, 24)
130 | setMinCropResultSize(20, 20)
131 | setMaxCropResultSize(99999, 99999)
132 | setActivityTitle("")
133 | setActivityMenuIconColor(0)
134 | setOutputUri(outputUri)
135 | setOutputCompressFormat(Bitmap.CompressFormat.JPEG)
136 | setOutputCompressQuality(90)
137 | setRequestedSize(0, 0)
138 | setRequestedSize(0, 0, CropImageView.RequestSizeOptions.RESIZE_INSIDE)
139 | setInitialCropWindowRectangle(null)
140 | setInitialRotation(0)
141 | setAllowCounterRotation(false)
142 | setFlipHorizontally(false)
143 | setFlipVertically(false)
144 | setCropMenuCropButtonTitle(null)
145 | setCropMenuCropButtonIcon(0)
146 | setAllowRotation(true)
147 | setNoOutputImage(false)
148 | setFixAspectRatio(false)
149 |
150 | // Odd Settings
151 | // setScaleType(CropImageView.ScaleType.CENTER)
152 | // setCropShape(CropImageView.CropShape.OVAL)
153 | // setGuidelines(CropImageView.Guidelines.ON)
154 | // setAspectRatio(4, 16)
155 | // setMaxZoom(8)
156 | // setAutoZoomEnabled(false)
157 | // setMultiTouchEnabled(false)
158 | // setCenterMoveEnabled(true)
159 | // setShowCropOverlay(false)
160 | // setAllowFlipping(false)
161 | // setSnapRadius(10f)
162 | // setTouchRadius(30f)
163 | // setInitialCropWindowPaddingRatio(0.3f)
164 | // setBorderLineThickness(5f)
165 | // setBorderLineColor(R.color.black)
166 | // setBorderCornerThickness(6f)
167 | // setBorderCornerOffset(2f)
168 | // setBorderCornerLength(20f)
169 | // setBorderCornerColor(RED)
170 | // setGuidelinesThickness(5f)
171 | // setGuidelinesColor(RED)
172 | // setBackgroundColor(Color.argb(119, 30, 60, 90))
173 | // setMinCropWindowSize(20, 20)
174 | // setMinCropResultSize(16, 16)
175 | // setMaxCropResultSize(999, 999)
176 | // setActivityTitle("CUSTOM title")
177 | // setActivityMenuIconColor(RED)
178 | // setOutputUri(outputUri)
179 | // setOutputCompressFormat(Bitmap.CompressFormat.PNG)
180 | // setOutputCompressQuality(50)
181 | // setRequestedSize(100, 100)
182 | // setRequestedSize(100, 100, CropImageView.RequestSizeOptions.RESIZE_FIT)
183 | // setInitialCropWindowRectangle(null)
184 | // setInitialRotation(180)
185 | // setAllowCounterRotation(true)
186 | // setFlipHorizontally(true)
187 | // setFlipVertically(true)
188 | // setCropMenuCropButtonTitle("Custom name")
189 | // setCropMenuCropButtonIcon(R.drawable.ic_gear_24)
190 | // setAllowRotation(false)
191 | // setNoOutputImage(false)
192 | // setFixAspectRatio(true)
193 | }
194 | )
195 | }
196 |
197 | override fun startCameraWithUri() {
198 | cropImage.launch(
199 | options(outputUri) {
200 | setScaleType(CropImageView.ScaleType.FIT_CENTER)
201 | setCropShape(CropImageView.CropShape.RECTANGLE)
202 | setGuidelines(CropImageView.Guidelines.ON_TOUCH)
203 | setAspectRatio(1, 1)
204 | setMaxZoom(4)
205 | setAutoZoomEnabled(true)
206 | setMultiTouchEnabled(true)
207 | setCenterMoveEnabled(true)
208 | setShowCropOverlay(true)
209 | setAllowFlipping(true)
210 | setSnapRadius(3f)
211 | setTouchRadius(48f)
212 | setInitialCropWindowPaddingRatio(0.1f)
213 | setBorderLineThickness(3f)
214 | setBorderLineColor(Color.argb(170, 255, 255, 255))
215 | setBorderCornerThickness(2f)
216 | setBorderCornerOffset(5f)
217 | setBorderCornerLength(14f)
218 | setBorderCornerColor(WHITE)
219 | setGuidelinesThickness(1f)
220 | setGuidelinesColor(R.color.white)
221 | setBackgroundColor(Color.argb(119, 0, 0, 0))
222 | setMinCropWindowSize(24, 24)
223 | setMinCropResultSize(20, 20)
224 | setMaxCropResultSize(99999, 99999)
225 | setActivityTitle("")
226 | setActivityMenuIconColor(0)
227 | setOutputUri(null)
228 | setOutputCompressFormat(Bitmap.CompressFormat.JPEG)
229 | setOutputCompressQuality(90)
230 | setRequestedSize(0, 0)
231 | setRequestedSize(0, 0, CropImageView.RequestSizeOptions.RESIZE_INSIDE)
232 | setInitialCropWindowRectangle(null)
233 | setInitialRotation(0)
234 | setAllowCounterRotation(false)
235 | setFlipHorizontally(false)
236 | setFlipVertically(false)
237 | setCropMenuCropButtonTitle(null)
238 | setCropMenuCropButtonIcon(0)
239 | setAllowRotation(true)
240 | setNoOutputImage(false)
241 | setFixAspectRatio(false)
242 | }
243 | )
244 | }
245 |
246 | override fun showErrorMessage(message: String) {
247 | Log.e("Camera Error:", message)
248 | Toast.makeText(activity, "Crop failed: $message", Toast.LENGTH_SHORT).show()
249 | }
250 |
251 | private fun startTakePicture() {
252 | setupOutputUri()
253 | takePicture.launch(outputUri)
254 | }
255 |
256 | override fun handleCropImageResult(uri: String) {
257 | SCropResultActivity.start(this, null, Uri.parse(uri), null)
258 | }
259 |
260 | private fun setupOutputUri() {
261 | if (outputUri == null) context?.let { ctx ->
262 | val authorities = "${ctx.applicationContext?.packageName}$AUTHORITY_SUFFIX"
263 | outputUri = FileProvider.getUriForFile(ctx, authorities, createImageFile())
264 | }
265 | }
266 |
267 | private fun createImageFile(): File {
268 | val timeStamp = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(Date())
269 | val storageDir: File? = activity?.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
270 | return File.createTempFile(
271 | "$FILE_NAMING_PREFIX${timeStamp}$FILE_NAMING_SUFFIX",
272 | FILE_FORMAT,
273 | storageDir
274 | )
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image/domain/SCropImageContract.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image.domain
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.fragment.app.FragmentActivity
6 | import com.aemerse.cropper.CropImageView
7 |
8 | internal interface SCropImageContract {
9 |
10 | interface View {
11 | fun showErrorMessage(message: String)
12 | fun handleCropImageResult(uri: String)
13 | fun startCameraWithUri()
14 | }
15 |
16 | interface Presenter {
17 | fun bind(view: View)
18 | fun unbind()
19 | fun onCreate(activity: FragmentActivity?, context: Context?)
20 | fun onCropImageResult(result: CropImageView.CropResult)
21 | fun onCustomCropImageResult(customUri: Uri?)
22 | fun onTakePictureResult(success: Boolean)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image/presenter/SCropImagePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image.presenter
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.pm.PackageManager
7 | import android.net.Uri
8 | import android.util.Log
9 | import androidx.core.app.ActivityCompat
10 | import androidx.fragment.app.FragmentActivity
11 | import com.aemerse.cropper.CropImage
12 | import com.aemerse.cropper.CropImageView
13 | import com.aemerse.cropper.app.crop_image.domain.SCropImageContract
14 |
15 | internal class SCropImagePresenter : SCropImageContract.Presenter {
16 |
17 | private var view: SCropImageContract.View? = null
18 | private var request = false
19 | private var hasSystemFeature = false
20 | private var context: Context? = null
21 |
22 | override fun bind(view: SCropImageContract.View) {
23 | this.view = view
24 | }
25 |
26 | override fun unbind() {
27 | view = null
28 | }
29 |
30 | override fun onCreate(activity: FragmentActivity?, context: Context?) {
31 | if (activity == null || context == null) {
32 | view?.showErrorMessage("onCreate activity and/or context are null")
33 | return
34 | }
35 | this.context = context
36 |
37 | request = ActivityCompat.shouldShowRequestPermissionRationale(
38 | activity as Activity,
39 | Manifest.permission.CAMERA
40 | )
41 | hasSystemFeature = context.packageManager
42 | ?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) ?: false
43 | }
44 |
45 | override fun onCropImageResult(result: CropImageView.CropResult) {
46 | when {
47 | result.isSuccessful -> {
48 | Log.v("Bitmap", result.bitmap.toString())
49 | Log.v("File Path", context?.let { result.getUriFilePath(it) }.toString())
50 | view?.handleCropImageResult(result.uriContent.toString().replace("file:", ""))
51 | }
52 | result is CropImage.CancelledResult -> {
53 | view?.showErrorMessage("cropping image was cancelled by the user")
54 | }
55 | else -> {
56 | view?.showErrorMessage("cropping image failed")
57 | }
58 | }
59 | }
60 |
61 | override fun onCustomCropImageResult(customUri: Uri?) {
62 | view?.handleCropImageResult(customUri.toString().replace("file:", ""))
63 | }
64 |
65 | override fun onTakePictureResult(success: Boolean) {
66 | if (success) view?.startCameraWithUri()
67 | else view?.showErrorMessage("taking picture failed")
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image_view/app/SCropImageViewFragment.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image_view.app
2 |
3 | import android.graphics.Rect
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.util.Log
7 | import android.view.LayoutInflater
8 | import android.view.Menu
9 | import android.view.MenuInflater
10 | import android.view.MenuItem
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.widget.Toast
14 | import androidx.activity.result.contract.ActivityResultContracts
15 | import androidx.fragment.app.Fragment
16 | import com.aemerse.cropper.CropImage
17 | import com.aemerse.cropper.CropImageView
18 | import com.aemerse.cropper.CropImageView.CropResult
19 | import com.aemerse.cropper.CropImageView.OnCropImageCompleteListener
20 | import com.aemerse.cropper.CropImageView.OnSetImageUriCompleteListener
21 | import com.aemerse.cropper.app.SCropResultActivity
22 | import com.aemerse.cropper.app.crop_image_view.domain.SCropImageViewContract
23 | import com.aemerse.cropper.app.crop_image_view.presenter.SCropImageViewPresenter
24 | import com.aemerse.cropper.app.options_dialog.app.SOptionsDialogBottomSheet
25 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsDomain
26 | import com.aemerse.cropper.app.R
27 | import com.aemerse.cropper.app.databinding.FragmentCropImageViewBinding
28 |
29 | internal class SCropImageViewFragment :
30 | Fragment(),
31 | SCropImageViewContract.View,
32 | SOptionsDialogBottomSheet.Listener,
33 | OnSetImageUriCompleteListener,
34 | OnCropImageCompleteListener {
35 |
36 | companion object {
37 |
38 | fun newInstance() = SCropImageViewFragment()
39 | }
40 |
41 | private lateinit var binding: FragmentCropImageViewBinding
42 | private val presenter = SCropImageViewPresenter()
43 | private var options: SOptionsDomain? = null
44 | private val openPicker =
45 | registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
46 | binding.cropImageView.setImageUriAsync(uri)
47 | }
48 |
49 | override fun onCreateView(
50 | inflater: LayoutInflater,
51 | container: ViewGroup?,
52 | savedInstanceState: Bundle?
53 | ): View {
54 | setHasOptionsMenu(true)
55 | binding = FragmentCropImageViewBinding.inflate(layoutInflater, container, false)
56 | return binding.root
57 | }
58 |
59 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
60 | super.onViewCreated(view, savedInstanceState)
61 |
62 | presenter.bind(this)
63 | presenter.onViewCreated()
64 |
65 | binding.cropImageView.let {
66 | it.setOnSetImageUriCompleteListener(this)
67 | it.setOnCropImageCompleteListener(this)
68 | if (savedInstanceState == null) it.imageResource = R.drawable.cat
69 | }
70 |
71 | binding.settings.setOnClickListener {
72 | SOptionsDialogBottomSheet.show(childFragmentManager, options, this)
73 | }
74 |
75 | binding.searchImage.setOnClickListener {
76 | openPicker.launch("image/*")
77 | }
78 |
79 | binding.reset.setOnClickListener {
80 | binding.cropImageView.apply {
81 | resetCropRect()
82 | options = options?.copy(
83 | scaleType = CropImageView.ScaleType.FIT_CENTER,
84 | flipHorizontal = false,
85 | flipVertically = false,
86 | autoZoom = true,
87 | maxZoomLvl = 2
88 | )
89 | imageResource = R.drawable.cat
90 | }
91 | }
92 | }
93 |
94 | override fun onOptionsApplySelected(options: SOptionsDomain) {
95 | this.options = options
96 |
97 | binding.cropImageView.apply {
98 | scaleType = options.scaleType
99 | cropShape = options.cropShape
100 | guidelines = options.guidelines
101 | if (options.ratio == null) setFixedAspectRatio(false)
102 | else {
103 | setFixedAspectRatio(true)
104 | setAspectRatio(options.ratio.first, options.ratio.second)
105 | }
106 | setMultiTouchEnabled(options.multiTouch)
107 | setCenterMoveEnabled(options.centerMove)
108 | isShowCropOverlay = options.showCropOverlay
109 | isShowProgressBar = options.showProgressBar
110 | isAutoZoomEnabled = options.autoZoom
111 | maxZoom = options.maxZoomLvl
112 | isFlippedHorizontally = options.flipHorizontal
113 | isFlippedVertically = options.flipVertically
114 | }
115 |
116 | if (options.scaleType == CropImageView.ScaleType.CENTER_INSIDE)
117 | binding.cropImageView.imageResource = R.drawable.cat_small
118 | }
119 |
120 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
121 | inflater.inflate(R.menu.main, menu)
122 | super.onCreateOptionsMenu(menu, inflater)
123 | }
124 |
125 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
126 | when (item.itemId) {
127 | R.id.main_action_crop -> {
128 | binding.cropImageView.croppedImageAsync()
129 | return true
130 | }
131 | R.id.main_action_rotate -> {
132 | binding.cropImageView.rotateImage(90)
133 | return true
134 | }
135 | R.id.main_action_flip_horizontally -> {
136 | binding.cropImageView.flipImageHorizontally()
137 | return true
138 | }
139 | R.id.main_action_flip_vertically -> {
140 | binding.cropImageView.flipImageVertically()
141 | return true
142 | }
143 | else -> return super.onOptionsItemSelected(item)
144 | }
145 | }
146 |
147 | override fun onDetach() {
148 | super.onDetach()
149 | binding.cropImageView.setOnSetImageUriCompleteListener(null)
150 | binding.cropImageView.setOnCropImageCompleteListener(null)
151 | }
152 |
153 | override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) {
154 | if (error != null) {
155 | Log.e("AIC", "Failed to load image by URI", error)
156 | Toast.makeText(activity, "Image load failed: " + error.message, Toast.LENGTH_LONG)
157 | .show()
158 | }
159 | }
160 |
161 | override fun onCropImageComplete(view: CropImageView, result: CropResult) {
162 | handleCropResult(result)
163 | }
164 |
165 | private fun handleCropResult(result: CropResult?) {
166 | if (result != null && result.error == null) {
167 | val imageBitmap =
168 | if (binding.cropImageView.cropShape == CropImageView.CropShape.OVAL)
169 | result.bitmap?.let { CropImage.toOvalBitmap(it) }
170 | else result.bitmap
171 | context?.let { Log.v("File Path", result.getUriFilePath(it).toString()) }
172 | SCropResultActivity.start(this, imageBitmap, result.uriContent, result.sampleSize)
173 | } else {
174 | Log.e("AIC", "Failed to crop image", result?.error)
175 | Toast
176 | .makeText(activity, "Crop failed: ${result?.error?.message}", Toast.LENGTH_SHORT)
177 | .show()
178 | }
179 | }
180 |
181 | override fun setOptions(options: SOptionsDomain) {
182 | binding.cropImageView.cropRect = Rect(100, 300, 500, 1200)
183 | onOptionsApplySelected(options)
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image_view/domain/SCropImageViewContract.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image_view.domain
2 |
3 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsDomain
4 |
5 | internal interface SCropImageViewContract {
6 | fun interface View {
7 | fun setOptions(options: SOptionsDomain)
8 | }
9 |
10 | interface Presenter {
11 | fun bind(view: View)
12 | fun unbind()
13 | fun onViewCreated()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/crop_image_view/presenter/SCropImageViewPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.crop_image_view.presenter
2 |
3 | import com.aemerse.cropper.CropImageView
4 | import com.aemerse.cropper.app.crop_image_view.domain.SCropImageViewContract
5 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsDomain
6 |
7 | internal class SCropImageViewPresenter : SCropImageViewContract.Presenter {
8 |
9 | private var view: SCropImageViewContract.View? = null
10 |
11 | override fun bind(view: SCropImageViewContract.View) {
12 | this.view = view
13 | }
14 |
15 | override fun unbind() {
16 | view = null
17 | }
18 |
19 | override fun onViewCreated() {
20 | view?.setOptions(getOptions())
21 | }
22 |
23 | private fun getOptions(): SOptionsDomain = SOptionsDomain(
24 | CropImageView.ScaleType.FIT_CENTER,
25 | CropImageView.CropShape.RECTANGLE,
26 | CropImageView.Guidelines.ON,
27 | Pair(1, 1),
28 | autoZoom = true,
29 | maxZoomLvl = 2,
30 | multiTouch = true,
31 | centerMove = true,
32 | showCropOverlay = true,
33 | showProgressBar = true,
34 | flipHorizontal = false,
35 | flipVertically = false
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/extend_activity/app/SExtendActivity.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.extend_activity.app
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.view.Menu
9 | import android.view.View
10 | import androidx.core.app.ActivityCompat
11 | import com.aemerse.cropper.CropImage
12 | import com.aemerse.cropper.CropImageActivity
13 | import com.aemerse.cropper.app.extend_activity.domain.SExtendContract
14 | import com.aemerse.cropper.app.extend_activity.presenter.SExtendPresenter
15 | import com.aemerse.cropper.app.R
16 | import com.aemerse.cropper.app.databinding.ExtendedActivityBinding
17 |
18 | internal class SExtendActivity : CropImageActivity(), SExtendContract.View {
19 |
20 | companion object {
21 | fun start(activity: Activity) {
22 | ActivityCompat.startActivity(
23 | activity,
24 | Intent(activity, SExtendActivity::class.java),
25 | null
26 | )
27 | }
28 | }
29 |
30 | private lateinit var binding: ExtendedActivityBinding
31 | private val presenter: SExtendContract.Presenter = SExtendPresenter()
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | binding = ExtendedActivityBinding.inflate(layoutInflater)
35 |
36 | super.onCreate(savedInstanceState)
37 | presenter.bindView(this)
38 |
39 | binding.saveBtn.setOnClickListener {
40 | cropImage() // CropImageActivity.cropImage()
41 | }
42 | binding.backBtn.setOnClickListener {
43 | onBackPressed() // CropImageActivity.onBackPressed()
44 | }
45 | binding.rotateText.setOnClickListener {
46 | presenter.onRotateClick()
47 | }
48 |
49 | setCropImageView(binding.cropImageView)
50 | }
51 |
52 | override fun showImageSourceDialog(openSource: (Source) -> Unit) {
53 | // Override this if you wanna a custom dialog layout
54 | super.showImageSourceDialog(openSource)
55 | }
56 |
57 | override fun setContentView(view: View) {
58 | // Override this to use your custom layout
59 | super.setContentView(binding.root)
60 | }
61 |
62 | override fun onDestroy() {
63 | presenter.unbindView()
64 | super.onDestroy()
65 | }
66 |
67 | override fun rotate(counter: Int) {
68 | binding.cropImageView.rotateImage(counter)
69 | }
70 |
71 | override fun updateRotationCounter(counter: String) {
72 | binding.rotateText.text = getString(R.string.rotation_value, counter)
73 | }
74 |
75 | override fun onPickImageResult(resultUri: Uri?) {
76 | super.onPickImageResult(resultUri)
77 |
78 | if (resultUri != null) {
79 | binding.cropImageView.setImageUriAsync(resultUri)
80 | }
81 | }
82 |
83 | // Override this to add more information into the intent
84 | override fun getResultIntent(uri: Uri?, error: java.lang.Exception?, sampleSize: Int): Intent {
85 | val result = super.getResultIntent(uri, error, sampleSize)
86 | return result.putExtra("EXTRA_KEY", "Extra data")
87 | }
88 |
89 | override fun setResult(uri: Uri?, error: Exception?, sampleSize: Int) {
90 | val result = CropImage.ActivityResult(
91 | binding.cropImageView.imageUri,
92 | uri,
93 | error,
94 | binding.cropImageView.cropPoints,
95 | binding.cropImageView.cropRect,
96 | binding.cropImageView.rotatedDegrees,
97 | binding.cropImageView.wholeImageRect,
98 | sampleSize
99 | )
100 |
101 | Log.v("File Path", result.getUriFilePath(this).toString())
102 | binding.cropImageView.setImageUriAsync(result.uriContent)
103 | }
104 |
105 | override fun setResultCancel() {
106 | Log.i("extend", "User this override to change behaviour when cancel")
107 | super.setResultCancel()
108 | }
109 |
110 | override fun updateMenuItemIconColor(menu: Menu, itemId: Int, color: Int) {
111 | Log.i(
112 | "extend",
113 | "If not using your layout, this can be one option to change colours. Check README and wiki for more"
114 | )
115 | super.updateMenuItemIconColor(menu, itemId, color)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/extend_activity/domain/SExtendContract.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.extend_activity.domain
2 |
3 | internal interface SExtendContract {
4 | interface View {
5 | fun updateRotationCounter(counter: String)
6 | fun rotate(counter: Int)
7 | }
8 |
9 | interface Presenter {
10 | fun bindView(view: View)
11 | fun unbindView()
12 | fun onRotateClick()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/extend_activity/presenter/SExtendPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.extend_activity.presenter
2 |
3 | import com.aemerse.cropper.app.extend_activity.domain.SExtendContract
4 |
5 | internal class SExtendPresenter : SExtendContract.Presenter {
6 |
7 | private var view: SExtendContract.View? = null
8 | private var counter = 0
9 |
10 | override fun bindView(view: SExtendContract.View) {
11 | this.view = view
12 | this.view?.updateRotationCounter(counter.toString())
13 | }
14 |
15 | override fun unbindView() {
16 | view = null
17 | }
18 |
19 | override fun onRotateClick() {
20 | counter += 90
21 | view?.rotate(90)
22 | if (counter == 360) counter = 0
23 | view?.updateRotationCounter(counter.toString())
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/options_dialog/app/SOptionsDialogBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.options_dialog.app
2 |
3 | import android.content.Context
4 | import android.content.DialogInterface
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.fragment.app.FragmentManager
10 | import com.aemerse.cropper.CropImageView
11 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsContract
12 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsDomain
13 | import com.aemerse.cropper.app.databinding.FragmentOptionsBinding
14 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
15 |
16 | internal class SOptionsDialogBottomSheet : BottomSheetDialogFragment(), SOptionsContract.View {
17 | fun interface Listener {
18 |
19 | fun onOptionsApplySelected(options: SOptionsDomain)
20 | }
21 |
22 | companion object {
23 |
24 | fun show(
25 | fragmentManager: FragmentManager,
26 | options: SOptionsDomain?,
27 | listener: Listener
28 | ) {
29 | this.listener = listener
30 | SOptionsDialogBottomSheet().apply {
31 | arguments = Bundle().apply { putParcelable(OPTIONS_KEY, options) }
32 | show(fragmentManager, null)
33 | }
34 | }
35 |
36 | private const val DIRECTION_UPWARDS = -1
37 | private lateinit var listener: Listener
38 | private const val OPTIONS_KEY = "OPTIONS_KEY"
39 | }
40 |
41 | private lateinit var presenter: SOptionsContract.Presenter
42 | private lateinit var binding: FragmentOptionsBinding
43 |
44 | override fun onAttach(context: Context) {
45 | super.onAttach(context)
46 | val serviceLocator = SOptionsServiceLocator(context)
47 | presenter = serviceLocator.getPresenter()
48 | }
49 |
50 | override fun onCreateView(
51 | inflater: LayoutInflater,
52 | container: ViewGroup?,
53 | savedInstanceState: Bundle?
54 | ): View {
55 | binding = FragmentOptionsBinding.inflate(layoutInflater, container, false)
56 | return binding.root
57 | }
58 |
59 | override fun onDismiss(dialog: DialogInterface) {
60 | presenter.onDismiss()
61 | super.onDismiss(dialog)
62 | }
63 |
64 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
65 | super.onViewCreated(view, savedInstanceState)
66 |
67 | presenter.bind(this)
68 | val options = arguments?.getParcelable(OPTIONS_KEY)
69 |
70 | presenter.onViewCreated(options)
71 |
72 | binding.optionsHeader.isSelected = false
73 | binding.optionsItemsScroll.setOnScrollChangeListener { _, _, _, _, _ ->
74 | binding.optionsHeader.isSelected =
75 | binding.optionsItemsScroll.canScrollVertically(DIRECTION_UPWARDS)
76 | }
77 |
78 | binding.scaleType.chipCenter.setOnClickListener {
79 | presenter.onScaleTypeSelect(CropImageView.ScaleType.CENTER)
80 | }
81 |
82 | binding.scaleType.chipCenterCrop.setOnClickListener {
83 | presenter.onScaleTypeSelect(CropImageView.ScaleType.CENTER_CROP)
84 | }
85 |
86 | binding.scaleType.chipCenterInside.setOnClickListener {
87 | presenter.onScaleTypeSelect(CropImageView.ScaleType.CENTER_INSIDE)
88 | }
89 |
90 | binding.scaleType.chipFitCenter.setOnClickListener {
91 | presenter.onScaleTypeSelect(CropImageView.ScaleType.FIT_CENTER)
92 | }
93 |
94 | binding.cropShape.chipRectangle.setOnClickListener {
95 | presenter.onCropShapeSelect(CropImageView.CropShape.RECTANGLE)
96 | }
97 |
98 | binding.cropShape.chipOval.setOnClickListener {
99 | presenter.onCropShapeSelect(CropImageView.CropShape.OVAL)
100 | }
101 |
102 | binding.cropShape.chipRectangleVerticalOnly.setOnClickListener {
103 | presenter.onCropShapeSelect(CropImageView.CropShape.RECTANGLE_VERTICAL_ONLY)
104 | }
105 |
106 | binding.cropShape.chipRectangleHorizontalOnly.setOnClickListener {
107 | presenter.onCropShapeSelect(CropImageView.CropShape.RECTANGLE_HORIZONTAL_ONLY)
108 | }
109 |
110 | binding.guidelines.chipOff.setOnClickListener {
111 | presenter.onGuidelinesSelect(CropImageView.Guidelines.OFF)
112 | }
113 |
114 | binding.guidelines.chipOn.setOnClickListener {
115 | presenter.onGuidelinesSelect(CropImageView.Guidelines.ON)
116 | }
117 |
118 | binding.guidelines.chipOnTouch.setOnClickListener {
119 | presenter.onGuidelinesSelect(CropImageView.Guidelines.ON_TOUCH)
120 | }
121 |
122 | binding.ratio.chipFree.setOnClickListener {
123 | presenter.onRatioSelect(null)
124 | }
125 |
126 | binding.ratio.chipOneOne.setOnClickListener {
127 | presenter.onRatioSelect(Pair(1, 1))
128 | }
129 |
130 | binding.ratio.chipFourThree.setOnClickListener {
131 | presenter.onRatioSelect(Pair(16, 9))
132 | }
133 |
134 | binding.ratio.chipSixteenNine.setOnClickListener {
135 | presenter.onRatioSelect(Pair(9, 16))
136 | }
137 |
138 | binding.maxZoom.chipTwo.setOnClickListener {
139 | presenter.onMaxZoomLvlSelect(2)
140 | }
141 |
142 | binding.maxZoom.chipFour.setOnClickListener {
143 | presenter.onMaxZoomLvlSelect(4)
144 | }
145 |
146 | binding.maxZoom.chipEight.setOnClickListener {
147 | presenter.onMaxZoomLvlSelect(8)
148 | }
149 |
150 | binding.autoZoom.toggle.setOnCheckedChangeListener { _, isChecked ->
151 | presenter.onAutoZoomSelect(isChecked)
152 | }
153 |
154 | binding.cropOverlay.toggle.setOnCheckedChangeListener { _, isChecked ->
155 | presenter.onCropOverlaySelect(isChecked)
156 | }
157 |
158 | binding.flipHorizontal.toggle.setOnCheckedChangeListener { _, isChecked ->
159 | presenter.onFlipHorizontalSelect(isChecked)
160 | }
161 |
162 | binding.flipVertical.toggle.setOnCheckedChangeListener { _, isChecked ->
163 | presenter.onFlipVerticallySelect(isChecked)
164 | }
165 |
166 | binding.multiTouch.toggle.setOnCheckedChangeListener { _, isChecked ->
167 | presenter.onMultiTouchSelect(isChecked)
168 | }
169 |
170 | binding.centerMoveEnabled.toggle.setOnCheckedChangeListener { _, isChecked ->
171 | presenter.onCenterMoveSelect(isChecked)
172 | }
173 |
174 | binding.progressBar.toggle.setOnCheckedChangeListener { _, isChecked ->
175 | presenter.onProgressBarSelect(isChecked)
176 | }
177 | }
178 |
179 | override fun updateOptions(options: SOptionsDomain) {
180 | when (options.scaleType) {
181 | CropImageView.ScaleType.CENTER -> binding.scaleType.chipCenter.isChecked = true
182 | CropImageView.ScaleType.FIT_CENTER -> binding.scaleType.chipFitCenter.isChecked = true
183 | CropImageView.ScaleType.CENTER_INSIDE ->
184 | binding.scaleType.chipCenterInside.isChecked = true
185 | CropImageView.ScaleType.CENTER_CROP -> binding.scaleType.chipCenterCrop.isChecked = true
186 | }
187 |
188 | when (options.cropShape) {
189 | CropImageView.CropShape.RECTANGLE -> binding.cropShape.chipRectangle.isChecked = true
190 | CropImageView.CropShape.OVAL -> binding.cropShape.chipOval.isChecked = true
191 | CropImageView.CropShape.RECTANGLE_VERTICAL_ONLY ->
192 | binding.cropShape.chipRectangleVerticalOnly.isChecked = true
193 | CropImageView.CropShape.RECTANGLE_HORIZONTAL_ONLY ->
194 | binding.cropShape.chipRectangleHorizontalOnly.isChecked = true
195 | }
196 |
197 | when (options.guidelines) {
198 | CropImageView.Guidelines.OFF -> binding.guidelines.chipOff.isChecked = true
199 | CropImageView.Guidelines.ON -> binding.guidelines.chipOn.isChecked = true
200 | CropImageView.Guidelines.ON_TOUCH -> binding.guidelines.chipOnTouch.isChecked = true
201 | }
202 |
203 | when (options.ratio) {
204 | Pair(1, 1) -> binding.ratio.chipOneOne.isChecked = true
205 | Pair(4, 3) -> binding.ratio.chipFourThree.isChecked = true
206 | Pair(16, 9) -> binding.ratio.chipSixteenNine.isChecked = true
207 | else -> binding.ratio.chipFree.isChecked = true
208 | }
209 |
210 | when (options.maxZoomLvl) {
211 | 4 -> binding.maxZoom.chipFour.isChecked = true
212 | 8 -> binding.maxZoom.chipEight.isChecked = true
213 | else -> binding.maxZoom.chipTwo.isChecked = true
214 | }
215 |
216 | binding.autoZoom.toggle.isChecked = options.autoZoom
217 | binding.multiTouch.toggle.isChecked = options.multiTouch
218 | binding.centerMoveEnabled.toggle.isChecked = options.centerMove
219 | binding.cropOverlay.toggle.isChecked = options.showCropOverlay
220 | binding.progressBar.toggle.isChecked = options.showProgressBar
221 | binding.flipHorizontal.toggle.isChecked = options.flipHorizontal
222 | binding.flipVertical.toggle.isChecked = options.flipVertically
223 | }
224 |
225 | override fun closeWithResult(options: SOptionsDomain) {
226 | listener.onOptionsApplySelected(options)
227 | }
228 |
229 | override fun onDestroy() {
230 | presenter.unbind()
231 | super.onDestroy()
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/options_dialog/app/SOptionsServiceLocator.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.options_dialog.app
2 |
3 | import android.content.Context
4 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsContract
5 | import com.aemerse.cropper.app.options_dialog.presenter.SOptionsPresenter
6 |
7 | internal class SOptionsServiceLocator(private val context: Context) {
8 |
9 | fun getPresenter(): SOptionsContract.Presenter = SOptionsPresenter()
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/options_dialog/domain/SOptionsContract.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.options_dialog.domain
2 |
3 | import com.aemerse.cropper.CropImageView.CropShape
4 | import com.aemerse.cropper.CropImageView.Guidelines
5 | import com.aemerse.cropper.CropImageView.ScaleType
6 |
7 | internal interface SOptionsContract {
8 |
9 | interface View {
10 | fun updateOptions(options: SOptionsDomain)
11 | fun closeWithResult(options: SOptionsDomain)
12 | }
13 |
14 | interface Presenter {
15 | fun bind(view: View)
16 | fun unbind()
17 | fun onViewCreated(options: SOptionsDomain?)
18 | fun onDismiss()
19 | fun onScaleTypeSelect(scaleType: ScaleType)
20 | fun onCropShapeSelect(cropShape: CropShape)
21 | fun onGuidelinesSelect(guidelines: Guidelines)
22 | fun onRatioSelect(ratio: Pair?)
23 | fun onAutoZoomSelect(enable: Boolean)
24 | fun onMaxZoomLvlSelect(maxZoom: Int)
25 | fun onMultiTouchSelect(enable: Boolean)
26 | fun onCenterMoveSelect(enable: Boolean)
27 | fun onCropOverlaySelect(show: Boolean)
28 | fun onProgressBarSelect(show: Boolean)
29 | fun onFlipHorizontalSelect(enable: Boolean)
30 | fun onFlipVerticallySelect(enable: Boolean)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/options_dialog/domain/SOptionsDomain.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.options_dialog.domain
2 |
3 | import android.os.Parcelable
4 | import com.aemerse.cropper.CropImageView
5 | import kotlinx.parcelize.Parcelize
6 | import kotlinx.parcelize.RawValue
7 |
8 | @Parcelize
9 | internal data class SOptionsDomain(
10 | val scaleType: CropImageView.ScaleType,
11 | val cropShape: CropImageView.CropShape,
12 | val guidelines: CropImageView.Guidelines,
13 | val ratio: @RawValue Pair?,
14 | val maxZoomLvl: Int,
15 | val autoZoom: Boolean,
16 | val multiTouch: Boolean,
17 | val centerMove: Boolean,
18 | val showCropOverlay: Boolean,
19 | val showProgressBar: Boolean,
20 | val flipHorizontal: Boolean,
21 | val flipVertically: Boolean,
22 | ) : Parcelable
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/aemerse/cropper/app/options_dialog/presenter/SOptionsPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.app.options_dialog.presenter
2 |
3 | import com.aemerse.cropper.CropImageView
4 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsContract
5 | import com.aemerse.cropper.app.options_dialog.domain.SOptionsDomain
6 |
7 | internal class SOptionsPresenter : SOptionsContract.Presenter {
8 |
9 | private var view: SOptionsContract.View? = null
10 | private var options = defaultOptions()
11 |
12 | override fun bind(view: SOptionsContract.View) {
13 | this.view = view
14 | }
15 |
16 | override fun unbind() {
17 | view = null
18 | }
19 |
20 | override fun onViewCreated(options: SOptionsDomain?) {
21 | options?.let { this.options = options }
22 | view?.updateOptions(this.options)
23 | }
24 |
25 | override fun onDismiss() {
26 | view?.closeWithResult(options)
27 | }
28 |
29 | override fun onScaleTypeSelect(scaleType: CropImageView.ScaleType) {
30 | options = options.copy(scaleType = scaleType)
31 | }
32 |
33 | override fun onCropShapeSelect(cropShape: CropImageView.CropShape) {
34 | options = options.copy(cropShape = cropShape)
35 | }
36 |
37 | override fun onGuidelinesSelect(guidelines: CropImageView.Guidelines) {
38 | options = options.copy(guidelines = guidelines)
39 | }
40 |
41 | override fun onRatioSelect(ratio: Pair?) {
42 | options = options.copy(ratio = ratio)
43 | }
44 |
45 | override fun onAutoZoomSelect(enable: Boolean) {
46 | options = options.copy(autoZoom = enable)
47 | }
48 |
49 | override fun onMaxZoomLvlSelect(maxZoom: Int) {
50 | options = options.copy(maxZoomLvl = maxZoom)
51 | }
52 |
53 | override fun onMultiTouchSelect(enable: Boolean) {
54 | options = options.copy(multiTouch = enable)
55 | }
56 |
57 | override fun onCenterMoveSelect(enable: Boolean) {
58 | options = options.copy(centerMove = enable)
59 | }
60 |
61 | override fun onCropOverlaySelect(show: Boolean) {
62 | options = options.copy(showCropOverlay = show)
63 | }
64 |
65 | override fun onProgressBarSelect(show: Boolean) {
66 | options = options.copy(showProgressBar = show)
67 | }
68 |
69 | override fun onFlipHorizontalSelect(enable: Boolean) {
70 | options = options.copy(flipHorizontal = enable)
71 | }
72 |
73 | override fun onFlipVerticallySelect(enable: Boolean) {
74 | options = options.copy(flipVertically = enable)
75 | }
76 |
77 | private fun defaultOptions() = SOptionsDomain(
78 | CropImageView.ScaleType.FIT_CENTER,
79 | CropImageView.CropShape.RECTANGLE,
80 | CropImageView.Guidelines.ON,
81 | Pair(1, 1),
82 | maxZoomLvl = 2,
83 | autoZoom = true,
84 | multiTouch = true,
85 | centerMove = true,
86 | showCropOverlay = true,
87 | showProgressBar = true,
88 | flipHorizontal = false,
89 | flipVertically = false
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/toolbar_elevation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
10 |
11 |
12 | -
13 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/color/chip_bg_states.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/chip_text_states.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/switch_thumb_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/switch_track_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/backdrop.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_bottom_sheet_rounded.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_draggable_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_purple_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/drawable/cat.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cat_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/drawable/cat_small.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/checkerboard.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/checktile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/drawable/checktile.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/crop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/drawable/crop.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_gear_24.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_mage_search_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/muted.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_crop_result.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
25 |
26 |
36 |
37 |
45 |
46 |
54 |
55 |
63 |
64 |
65 |
66 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/chip_crop_shape.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
19 |
23 |
24 |
31 |
32 |
38 |
39 |
45 |
46 |
52 |
53 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/chip_guidelines.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
22 |
23 |
30 |
31 |
37 |
38 |
44 |
45 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/chip_max_zoom.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
22 |
23 |
30 |
31 |
37 |
38 |
44 |
45 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/chip_ratio.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
22 |
23 |
30 |
31 |
37 |
38 |
44 |
45 |
51 |
52 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/chip_scale_type.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
22 |
23 |
30 |
31 |
37 |
38 |
44 |
45 |
51 |
52 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/extended_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
18 |
19 |
30 |
31 |
38 |
39 |
50 |
51 |
64 |
65 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_camera.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
27 |
28 |
40 |
41 |
53 |
54 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_crop_image_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
28 |
29 |
41 |
42 |
53 |
54 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_options.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
24 |
25 |
33 |
34 |
35 |
36 |
41 |
42 |
47 |
48 |
51 |
52 |
55 |
56 |
59 |
60 |
63 |
64 |
67 |
68 |
71 |
72 |
75 |
76 |
79 |
80 |
83 |
84 |
87 |
88 |
91 |
92 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_auto_zoom.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_center_move.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_crop_overlay.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_flip_horizontal.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_flip_vertical.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_multi_touch.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/switch_progress_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1a1a1a
4 | #000000
5 | #803399
6 | #FF5722
7 | #952B09
8 |
9 | #FFFFFF
10 | #F3F3F3
11 | #CDCDCD
12 | #000000
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4dp
5 | 8dp
6 | 16dp
7 | 32dp
8 | 44dp
9 |
10 |
11 | 24dp
12 | 16dp
13 | 16dp
14 | 12dp
15 | 8dp
16 | 8dp
17 | 8dp
18 | 8dp
19 | 8dp
20 | 8dp
21 | 6dp
22 | 4dp
23 | 0dp
24 | 3dp
25 | 2dp
26 | 1dp
27 | 1dp
28 | 0dp
29 | 0dp
30 |
31 |
32 | 12dp
33 | 12dp
34 |
35 |
37 | 240dp
38 | 15sp
39 | 8dp
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0A0A0A
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cropper
5 | Image Cropper
6 | crop result
7 |
8 | Load Image for cropping
9 | Open navigation drawer
10 | Close navigation drawer
11 |
12 | CROP
13 | Rotate 90 degrees clockwise
14 | Flip
15 | Flip horizontally
16 | Flip vertically
17 | Crop View Presets
18 | Circular
19 | Rectangular
20 | Min/Max limits override
21 | Customized Overlay
22 | Scale Center Inside
23 | Customization Toggles
24 | Image Scale: %1s
25 | Crop Shape: %1s
26 | Guidelines: %1s
27 | Aspect Ratio: %1s
28 | Auto zoom: %1s
29 | Max zoom level: %1s
30 | Set initial crop rectangle
31 | Reset crop rectangle to initial
32 | Multitouch: %1s
33 | Center move enabled: %1s
34 | Show Overlay: %1s
35 | Show Progress Bar: %1s
36 |
37 |
38 | Options
39 | Default
40 | Custom
41 | Fit Center
42 | Center
43 | Center Crop
44 | Center Inside
45 | Rectangle
46 | Oval
47 | Rectangle - vertical only
48 | Rectangle - horizontal only
49 | Off
50 | On Touch
51 | ON
52 | 16,9
53 | 4,3
54 | 1,1
55 | Free
56 | Activity Type
57 | Ratio
58 | Guidelines
59 | Crop Shape
60 | Scale Type
61 | Max Zoom Level
62 | 2
63 | 4
64 | 8
65 | Auto Zoom
66 | Fix Aspect Ratio
67 | Multi Touch
68 | Enable movement by dragging center
69 | Show Crop Overlay
70 | Show Progress Bar
71 | Flip Horizontal
72 | Flip Vertical
73 | Reset Crop Rect
74 | Sample using CropImageView
75 | Sample of CustomActivity
76 | Sample calling CropImage
77 | Sample calling CropImage Java
78 | Take Picture
79 | Crop Settings
80 | ok
81 | cancel
82 | Camera Permission Need
83 | We need camera permission to take image and crop it
84 | Take camera picture before calling library with uri
85 | Call library directly without uri
86 | Call library directly without uri. Camera only
87 | Call library directly without uri. Gallery only
88 | Library show pick option, startPickImageActivity
89 | Custom selector title, include documents, custom requestCode
90 | Save
91 | This is a sample code using the library. The sample here is for extend CropImageActivity, to be about to custom build your personal view around it. For that we need to use CropImageView.
92 | Click to change rotation: %1$s
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
19 |
20 |
29 |
30 |
35 |
36 |
41 |
42 |
46 |
47 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
30 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = "1.6.0"
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:7.0.3"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | mavenCentral()
17 | google()
18 | maven { url "https://jitpack.io" }
19 | }
20 | }
21 |
22 | task clean(type: Delete) {
23 | delete rootProject.buildDir
24 | }
--------------------------------------------------------------------------------
/cropper/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-parcelize'
5 | id 'maven-publish'
6 | }
7 |
8 | android {
9 | compileSdk 31
10 | defaultConfig {
11 | minSdk 16
12 | targetSdk 31
13 | }
14 | buildFeatures {
15 | viewBinding true
16 | }
17 | lintOptions {
18 | abortOnError false
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation "androidx.appcompat:appcompat:1.4.0"
24 | implementation "androidx.activity:activity-ktx:1.4.0"
25 | implementation "androidx.exifinterface:exifinterface:1.3.3"
26 | implementation "androidx.core:core-ktx:1.7.0"
27 |
28 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
29 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
30 |
31 | //Tests
32 | testImplementation "junit:junit:4.13.2"
33 | testImplementation "androidx.test.ext:junit:1.1.3"
34 | testImplementation "androidx.test:core:1.4.0"
35 | testImplementation "androidx.test:runner:1.4.0"
36 | testImplementation "io.mockk:mockk:1.12.0"
37 | }
38 |
39 | afterEvaluate {
40 | publishing {
41 | publications {
42 | // Creates a Maven publication called "release".
43 | release(MavenPublication) {
44 | // Applies the component for the release build variant.
45 | from components.release
46 |
47 | // You can then customize attributes of the publication as shown below.
48 | groupId = 'com.aemerse'
49 | artifactId = 'cropper'
50 | version = '1.0.0'
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/cropper/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
23 |
24 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/BitmapCroppingWorkerJob.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.net.Uri
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.isActive
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 | import java.lang.ref.WeakReference
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | class BitmapCroppingWorkerJob(
16 | private val context: Context,
17 | private val cropImageViewReference: WeakReference,
18 | val uri: Uri?,
19 | private val bitmap: Bitmap?,
20 | private val cropPoints: FloatArray,
21 | private val degreesRotated: Int,
22 | private val orgWidth: Int,
23 | private val orgHeight: Int,
24 | private val fixAspectRatio: Boolean,
25 | private val aspectRatioX: Int,
26 | private val aspectRatioY: Int,
27 | private val reqWidth: Int,
28 | private val reqHeight: Int,
29 | private val flipHorizontally: Boolean,
30 | private val flipVertically: Boolean,
31 | private val options: CropImageView.RequestSizeOptions,
32 | private val saveCompressFormat: Bitmap.CompressFormat,
33 | private val saveCompressQuality: Int,
34 | private val customOutputUri: Uri?,
35 | ) : CoroutineScope {
36 |
37 | private var job: Job = Job()
38 | override val coroutineContext: CoroutineContext
39 | get() = Dispatchers.Main + job
40 |
41 | fun start() {
42 | job = launch(Dispatchers.Default) {
43 | try {
44 | if (isActive) {
45 | val bitmapSampled: BitmapUtils.BitmapSampled
46 | when {
47 | uri != null -> {
48 | bitmapSampled = BitmapUtils.cropBitmap(
49 | context,
50 | uri,
51 | cropPoints,
52 | degreesRotated,
53 | orgWidth,
54 | orgHeight,
55 | fixAspectRatio,
56 | aspectRatioX,
57 | aspectRatioY,
58 | reqWidth,
59 | reqHeight,
60 | flipHorizontally,
61 | flipVertically
62 | )
63 | }
64 | bitmap != null -> {
65 | bitmapSampled = BitmapUtils.cropBitmapObjectHandleOOM(
66 | bitmap,
67 | cropPoints,
68 | degreesRotated,
69 | fixAspectRatio,
70 | aspectRatioX,
71 | aspectRatioY,
72 | flipHorizontally,
73 | flipVertically
74 | )
75 | }
76 | else -> {
77 | onPostExecute(Result(bitmap = null, 1))
78 | return@launch
79 | }
80 | }
81 | val resizedBitmap =
82 | BitmapUtils.resizeBitmap(bitmapSampled.bitmap, reqWidth, reqHeight, options)
83 |
84 | launch(Dispatchers.IO) {
85 | val newUri = BitmapUtils.writeBitmapToUri(
86 | context = context,
87 | bitmap = resizedBitmap,
88 | compressFormat = saveCompressFormat,
89 | compressQuality = saveCompressQuality,
90 | customOutputUri = customOutputUri
91 | )
92 | resizedBitmap.recycle()
93 | onPostExecute(
94 | Result(
95 | newUri,
96 | bitmapSampled.sampleSize
97 | )
98 | )
99 | }
100 | }
101 | } catch (e: Exception) {
102 | onPostExecute(Result(e, false))
103 | }
104 | }
105 | }
106 |
107 | private suspend fun onPostExecute(result: Result) {
108 | withContext(Dispatchers.Main) {
109 | var completeCalled = false
110 | if (isActive) {
111 | cropImageViewReference.get()?.let {
112 | completeCalled = true
113 | it.onImageCroppingAsyncComplete(result)
114 | }
115 | }
116 | if (!completeCalled && result.bitmap != null) {
117 | // fast release of unused bitmap
118 | result.bitmap.recycle()
119 | }
120 | }
121 | }
122 |
123 | fun cancel() {
124 | job.cancel()
125 | }
126 |
127 | class Result {
128 |
129 | /** The cropped bitmap */
130 | val bitmap: Bitmap?
131 |
132 | /** The saved cropped bitmap uri */
133 | val uri: Uri?
134 |
135 | /** The error that occurred during async bitmap cropping. */
136 | val error: java.lang.Exception?
137 |
138 | /** is the cropping request was to get a bitmap or to save it to uri */
139 | val isSave: Boolean
140 |
141 | /** sample size used creating the crop bitmap to lower its size */
142 | val sampleSize: Int
143 |
144 | constructor(bitmap: Bitmap?, sampleSize: Int) {
145 | this.bitmap = bitmap
146 | uri = null
147 | error = null
148 | isSave = false
149 | this.sampleSize = sampleSize
150 | }
151 |
152 | constructor(uri: Uri?, sampleSize: Int) {
153 | bitmap = null
154 | this.uri = uri
155 | error = null
156 | isSave = true
157 | this.sampleSize = sampleSize
158 | }
159 |
160 | constructor(error: java.lang.Exception?, isSave: Boolean) {
161 | bitmap = null
162 | uri = null
163 | this.error = error
164 | this.isSave = isSave
165 | sampleSize = 1
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/BitmapLoadingWorkerJob.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.net.Uri
6 | import com.aemerse.cropper.utils.getFilePathFromUri
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.Job
10 | import kotlinx.coroutines.isActive
11 | import kotlinx.coroutines.launch
12 | import kotlinx.coroutines.withContext
13 | import java.lang.ref.WeakReference
14 | import kotlin.coroutines.CoroutineContext
15 |
16 | class BitmapLoadingWorkerJob internal constructor(
17 | private val context: Context,
18 | cropImageView: CropImageView,
19 | val uri: Uri
20 | ) : CoroutineScope {
21 |
22 | private val width: Int
23 | private val height: Int
24 | private val cropImageViewReference = WeakReference(cropImageView)
25 | private var currentJob: Job = Job()
26 | override val coroutineContext: CoroutineContext
27 | get() = Dispatchers.Main + currentJob
28 |
29 | init {
30 | val metrics = cropImageView.resources.displayMetrics
31 | val densityAdj: Double = if (metrics.density > 1) (1.0 / metrics.density) else 1.0
32 | width = (metrics.widthPixels * densityAdj).toInt()
33 | height = (metrics.heightPixels * densityAdj).toInt()
34 | }
35 |
36 | fun start() {
37 | currentJob = launch(Dispatchers.Default) {
38 | try {
39 | if (isActive) {
40 | val decodeResult =
41 | BitmapUtils.decodeSampledBitmap(context, uri, width, height)
42 | if (isActive) {
43 | val rotateResult =
44 | BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, context, uri)
45 | onPostExecute(
46 | Result(
47 | uri = uri,
48 | bitmap = rotateResult.bitmap,
49 | loadSampleSize = decodeResult.sampleSize,
50 | degreesRotated = rotateResult.degrees
51 | )
52 | )
53 | }
54 | }
55 | } catch (e: Exception) {
56 | onPostExecute(Result(uri, e))
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * Once complete, see if ImageView is still around and set bitmap.
63 | *
64 | * @param result the result of bitmap loading
65 | */
66 | private suspend fun onPostExecute(result: Result) {
67 | withContext(Dispatchers.Main) {
68 | var completeCalled = false
69 | if (isActive) {
70 | cropImageViewReference.get()?.let {
71 | completeCalled = true
72 | it.onSetImageUriAsyncComplete(result)
73 | }
74 | }
75 | if (!completeCalled && result.bitmap != null) {
76 | // fast release of unused bitmap
77 | result.bitmap.recycle()
78 | }
79 | }
80 | }
81 |
82 | fun cancel() {
83 | currentJob.cancel()
84 | }
85 |
86 | /** The result of BitmapLoadingWorkerJob async loading. */
87 | companion object
88 | class Result {
89 |
90 | /**
91 | * The Android URI of the image to load.
92 | * NOT a file path, for it use [getUriFilePath]
93 | */
94 | val uriContent: Uri
95 |
96 | /** The loaded bitmap */
97 | val bitmap: Bitmap?
98 |
99 | /** The sample size used to load the given bitmap */
100 | val loadSampleSize: Int
101 |
102 | /** The degrees the image was rotated */
103 | val degreesRotated: Int
104 |
105 | /** The error that occurred during async bitmap loading. */
106 | val error: Exception?
107 |
108 | /**
109 | * The file path of the image to load
110 | *
111 | * @param context used to access Android APIs, like content resolve, it is your
112 | * activity/fragment/widget.
113 | * @param uniqueName If true, make each image cropped have a different file name, this could
114 | * cause memory issues, use wisely. [Default: false]
115 | */
116 | fun getUriFilePath(context: Context, uniqueName: Boolean = false): String =
117 | getFilePathFromUri(context, uriContent, uniqueName)
118 |
119 | internal constructor(uri: Uri, bitmap: Bitmap?, loadSampleSize: Int, degreesRotated: Int) {
120 | uriContent = uri
121 | this.bitmap = bitmap
122 | this.loadSampleSize = loadSampleSize
123 | this.degreesRotated = degreesRotated
124 | error = null
125 | }
126 |
127 | internal constructor(uri: Uri, error: Exception?) {
128 | uriContent = uri
129 | bitmap = null
130 | loadSampleSize = 0
131 | degreesRotated = 0
132 | this.error = error
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/CropFileProvider.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import androidx.core.content.FileProvider
4 |
5 | /**
6 | * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions.
7 | *
8 | * See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
9 | */
10 | class CropFileProvider : FileProvider() {
11 | // This class intentionally left blank.
12 | }
13 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/CropImage.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.graphics.Bitmap
6 | import android.graphics.Canvas
7 | import android.graphics.Paint
8 | import android.graphics.PorterDuff
9 | import android.graphics.PorterDuffXfermode
10 | import android.graphics.Rect
11 | import android.graphics.RectF
12 | import android.net.Uri
13 | import android.os.Environment
14 | import android.os.Parcel
15 | import android.os.Parcelable
16 | import android.provider.MediaStore
17 | import androidx.core.content.FileProvider
18 | import com.aemerse.cropper.CropImageView.CropResult
19 | import com.aemerse.cropper.common.CommonValues
20 | import com.aemerse.cropper.common.CommonVersionCheck.isAtLeastQ29
21 | import com.aemerse.cropper.utils.getFilePathFromUri
22 | import java.io.File
23 |
24 | /**
25 | * Helper to simplify crop image work like starting pick-image acitvity and handling camera/gallery
26 | * intents.
27 | * The goal of the helper is to simplify the starting and most-common usage of image cropping and
28 | * not all porpose all possible scenario one-to-rule-them-all code base. So feel free to use it as
29 | * is and as a wiki to make your own.
30 | * Added value you get out-of-the-box is some edge case handling that you may miss otherwise, like
31 | * the stupid-ass Android camera result URI that may differ from version to version and from device
32 | * to device.
33 | */
34 | @Suppress("unused", "MemberVisibilityCanBePrivate")
35 | object CropImage {
36 |
37 | /**
38 | * The key used to pass crop image source URI to [CropImageActivity].
39 | */
40 | const val CROP_IMAGE_EXTRA_SOURCE = "CROP_IMAGE_EXTRA_SOURCE"
41 |
42 | /**
43 | * The key used to pass crop image options to [CropImageActivity].
44 | */
45 | const val CROP_IMAGE_EXTRA_OPTIONS = "CROP_IMAGE_EXTRA_OPTIONS"
46 |
47 | /**
48 | * The key used to pass crop image bundle data to [CropImageActivity].
49 | */
50 | const val CROP_IMAGE_EXTRA_BUNDLE = "CROP_IMAGE_EXTRA_BUNDLE"
51 |
52 | /**
53 | * The key used to pass crop image result data back from [CropImageActivity].
54 | */
55 | const val CROP_IMAGE_EXTRA_RESULT = "CROP_IMAGE_EXTRA_RESULT"
56 |
57 | /**
58 | * The request code used to start pick image activity to be used on result to identify the this
59 | * specific request.
60 | */
61 | const val PICK_IMAGE_CHOOSER_REQUEST_CODE = 200
62 |
63 | /**
64 | * The request code used to request permission to pick image from external storage.
65 | */
66 | const val PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 201
67 |
68 | /**
69 | * The request code used to request permission to capture image from camera.
70 | */
71 | const val CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE = 2011
72 |
73 | /**
74 | * The request code used to start [CropImageActivity] to be used on result to identify the
75 | * this specific request.
76 | */
77 | const val CROP_IMAGE_ACTIVITY_REQUEST_CODE = 203
78 |
79 | /**
80 | * The result code used to return error from [CropImageActivity].
81 | */
82 | const val CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE = 204
83 |
84 | /**
85 | * Create a new bitmap that has all pixels beyond the oval shape transparent. Old bitmap is
86 | * recycled.
87 | */
88 | fun toOvalBitmap(bitmap: Bitmap): Bitmap {
89 | val width = bitmap.width
90 | val height = bitmap.height
91 | val output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
92 | val canvas = Canvas(output)
93 | val color = -0xbdbdbe
94 | val paint = Paint()
95 | paint.isAntiAlias = true
96 | canvas.drawARGB(0, 0, 0, 0)
97 | paint.color = color
98 | val rect = RectF(0f, 0f, width.toFloat(), height.toFloat())
99 | canvas.drawOval(rect, paint)
100 | paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
101 | canvas.drawBitmap(bitmap, 0f, 0f, paint)
102 | bitmap.recycle()
103 | return output
104 | }
105 |
106 | /**
107 | * Get URI to image received from capture by camera.
108 | *
109 | * This is not the File Path, for it please use [getCaptureImageOutputUriFilePath]
110 | *
111 | * @param context used to access Android APIs, like content resolve, it is your
112 | * activity/fragment/widget.
113 | */
114 | fun getCaptureImageOutputUriContent(context: Context): Uri {
115 | val outputFileUri: Uri
116 | val getImage: File?
117 | // We have this because of a HUAWEI path bug when we use getUriForFile
118 | if (isAtLeastQ29()) {
119 | getImage = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
120 | outputFileUri = try {
121 | FileProvider.getUriForFile(
122 | context,
123 | context.packageName + CommonValues.authority,
124 | File(getImage!!.path, "pickImageResult.jpeg")
125 | )
126 | } catch (e: Exception) {
127 | Uri.fromFile(File(getImage!!.path, "pickImageResult.jpeg"))
128 | }
129 | } else {
130 | getImage = context.externalCacheDir
131 | outputFileUri = Uri.fromFile(File(getImage!!.path, "pickImageResult.jpeg"))
132 | }
133 | return outputFileUri
134 | }
135 |
136 | /**
137 | * Get File Path to image received from capture by camera.
138 | *
139 | * @param context used to access Android APIs, like content resolve, it is your
140 | * activity/fragment/widget.
141 | * @param uniqueName If true, make each image cropped have a different file name, this could cause
142 | * memory issues, use wisely. [Default: false]
143 | */
144 | fun getCaptureImageOutputUriFilePath(context: Context, uniqueName: Boolean = false): String =
145 | getFilePathFromUri(context, getCaptureImageOutputUriContent(context), uniqueName)
146 |
147 | /**
148 | * Get the URI of the selected image
149 | * Will return the correct URI for camera and gallery image.
150 | *
151 | * This is not the File Path, for it please use [getPickImageResultUriFilePath]
152 | *
153 | * @param context used to access Android APIs, like content resolve, it is your
154 | * activity/fragment/widget.
155 | * @param data the returned data of the activity result
156 | */
157 | @JvmStatic
158 | fun getPickImageResultUriContent(context: Context, data: Intent?): Uri {
159 | var isCamera = true
160 | val uri = data?.data
161 | if (uri != null) {
162 | val action = data.action
163 | isCamera = action != null && action == MediaStore.ACTION_IMAGE_CAPTURE
164 | }
165 | return if (isCamera || uri == null) getCaptureImageOutputUriContent(context)
166 | else uri
167 | }
168 |
169 | /**
170 | * Get the File Path of the selected image
171 | *
172 | * @param context used to access Android APIs, like content resolve, it is your
173 | * activity/fragment/widget.
174 | * @param data the returned data of the activity result
175 | * @param uniqueName If true, make each image cropped have a different file name, this could cause
176 | * memory issues, use wisely. [Default: false]
177 | */
178 | @JvmStatic
179 | fun getPickImageResultUriFilePath(
180 | context: Context,
181 | data: Intent?,
182 | uniqueName: Boolean = false
183 | ): String =
184 | getFilePathFromUri(context, getPickImageResultUriContent(context, data), uniqueName)
185 |
186 | /**
187 | * Result data of Crop Image Activity.
188 | */
189 | open class ActivityResult : CropResult, Parcelable {
190 |
191 | constructor(
192 | originalUri: Uri?,
193 | uriContent: Uri?,
194 | error: Exception?,
195 | cropPoints: FloatArray?,
196 | cropRect: Rect?,
197 | rotation: Int,
198 | wholeImageRect: Rect?,
199 | sampleSize: Int
200 | ) : super(
201 | originalBitmap = null,
202 | originalUri = originalUri,
203 | bitmap = null,
204 | uriContent = uriContent,
205 | error = error,
206 | cropPoints = cropPoints!!,
207 | cropRect = cropRect,
208 | wholeImageRect = wholeImageRect,
209 | rotation = rotation,
210 | sampleSize = sampleSize
211 | )
212 |
213 | protected constructor(`in`: Parcel) : super(
214 | originalBitmap = null,
215 | originalUri = `in`.readParcelable(Uri::class.java.classLoader) as Uri?,
216 | bitmap = null,
217 | uriContent = `in`.readParcelable(Uri::class.java.classLoader) as Uri?,
218 | error = `in`.readSerializable() as Exception?,
219 | cropPoints = `in`.createFloatArray()!!,
220 | cropRect = `in`.readParcelable(Rect::class.java.classLoader) as Rect?,
221 | wholeImageRect = `in`.readParcelable(Rect::class.java.classLoader) as Rect?,
222 | rotation = `in`.readInt(),
223 | sampleSize = `in`.readInt()
224 | )
225 |
226 | override fun writeToParcel(dest: Parcel, flags: Int) {
227 | dest.writeParcelable(originalUri, flags)
228 | dest.writeParcelable(uriContent, flags)
229 | dest.writeSerializable(error)
230 | dest.writeFloatArray(cropPoints)
231 | dest.writeParcelable(cropRect, flags)
232 | dest.writeParcelable(wholeImageRect, flags)
233 | dest.writeInt(rotation)
234 | dest.writeInt(sampleSize)
235 | }
236 |
237 | override fun describeContents(): Int = 0
238 |
239 | companion object {
240 |
241 | @JvmField
242 | val CREATOR: Parcelable.Creator =
243 | object : Parcelable.Creator {
244 | override fun createFromParcel(`in`: Parcel): ActivityResult =
245 | ActivityResult(`in`)
246 |
247 | override fun newArray(size: Int): Array = arrayOfNulls(size)
248 | }
249 | }
250 | }
251 |
252 | object CancelledResult : CropImageView.CropResult(
253 | originalBitmap = null,
254 | originalUri = null,
255 | bitmap = null,
256 | uriContent = null,
257 | error = Exception("cropping has been cancelled by the user"),
258 | cropPoints = floatArrayOf(),
259 | cropRect = null,
260 | wholeImageRect = null,
261 | rotation = 0,
262 | sampleSize = 0
263 | )
264 | }
265 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/CropImageActivity.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.content.Intent
4 | import android.graphics.drawable.Drawable
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.view.Menu
9 | import android.view.MenuItem
10 | import androidx.activity.result.contract.ActivityResultContracts
11 | import androidx.appcompat.app.AlertDialog
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.core.content.ContextCompat
14 | import androidx.core.graphics.BlendModeColorFilterCompat
15 | import androidx.core.graphics.BlendModeCompat
16 | import com.aemerse.cropper.CropImageView.CropResult
17 | import com.aemerse.cropper.CropImageView.OnCropImageCompleteListener
18 | import com.aemerse.cropper.CropImageView.OnSetImageUriCompleteListener
19 | import com.aemerse.cropper.databinding.CropImageActivityBinding
20 | import com.aemerse.cropper.utils.getUriForFile
21 | import java.io.File
22 |
23 | open class CropImageActivity : AppCompatActivity(), OnSetImageUriCompleteListener, OnCropImageCompleteListener {
24 | /**
25 | * Persist URI image to crop URI if specific permissions are required
26 | */
27 | private var cropImageUri: Uri? = null
28 |
29 | /**
30 | * the options that were set for the crop image
31 | */
32 | private lateinit var cropImageOptions: CropImageOptions
33 |
34 | /** The crop image view library widget used in the activity */
35 | private var cropImageView: CropImageView? = null
36 | private lateinit var binding: CropImageActivityBinding
37 | private var latestTmpUri: Uri? = null
38 | private val pickImageGallery =
39 | registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
40 | onPickImageResult(uri)
41 | }
42 | private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) {
43 | if (it) onPickImageResult(latestTmpUri)
44 | }
45 |
46 | public override fun onCreate(savedInstanceState: Bundle?) {
47 | super.onCreate(savedInstanceState)
48 |
49 | binding = CropImageActivityBinding.inflate(layoutInflater)
50 | setContentView(binding.root)
51 | setCropImageView(binding.cropImageView)
52 | val bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE)
53 | cropImageUri = bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE)
54 | cropImageOptions = bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS) ?: CropImageOptions()
55 |
56 | if (savedInstanceState == null) {
57 | if (cropImageUri == null || cropImageUri == Uri.EMPTY) {
58 | when {
59 | cropImageOptions.imageSourceIncludeGallery &&
60 | cropImageOptions.imageSourceIncludeCamera ->
61 | showImageSourceDialog(::openSource)
62 | cropImageOptions.imageSourceIncludeGallery ->
63 | pickImageGallery.launch("image/*")
64 | cropImageOptions.imageSourceIncludeCamera ->
65 | openCamera()
66 | else -> finish()
67 | }
68 | } else cropImageView?.setImageUriAsync(cropImageUri)
69 | }
70 |
71 | supportActionBar?.let {
72 | title = when {
73 | cropImageOptions.activityTitle.isNotEmpty() -> cropImageOptions.activityTitle
74 | else -> resources.getString(R.string.crop_image_activity_title)
75 | }
76 | it.setDisplayHomeAsUpEnabled(true)
77 | }
78 | }
79 |
80 | private fun openSource(source: Source) {
81 | when (source) {
82 | Source.CAMERA -> openCamera()
83 | Source.GALLERY -> pickImageGallery.launch("image/*")
84 | }
85 | }
86 |
87 | private fun openCamera() {
88 | getTmpFileUri().let { uri ->
89 | latestTmpUri = uri
90 | takePicture.launch(uri)
91 | }
92 | }
93 |
94 | private fun getTmpFileUri(): Uri {
95 | val tmpFile = File.createTempFile("tmp_image_file", ".png", cacheDir).apply {
96 | createNewFile()
97 | deleteOnExit()
98 | }
99 |
100 | return getUriForFile(this, tmpFile)
101 | }
102 |
103 | /**
104 | * This method show the dialog for user source choice, it is an open function so can be override
105 | * and customised with the app layout if need.
106 | */
107 | open fun showImageSourceDialog(openSource: (Source) -> Unit) {
108 | AlertDialog.Builder(this)
109 | .setTitle(R.string.pick_image_chooser_title)
110 | .setItems(
111 | arrayOf(
112 | getString(R.string.pick_image_camera),
113 | getString(R.string.pick_image_gallery),
114 | )
115 | ) { _, position -> openSource(if (position == 0) Source.CAMERA else Source.GALLERY) }
116 | .show()
117 | }
118 |
119 | public override fun onStart() {
120 | super.onStart()
121 | cropImageView?.setOnSetImageUriCompleteListener(this)
122 | cropImageView?.setOnCropImageCompleteListener(this)
123 | }
124 |
125 | public override fun onStop() {
126 | super.onStop()
127 | cropImageView?.setOnSetImageUriCompleteListener(null)
128 | cropImageView?.setOnCropImageCompleteListener(null)
129 | }
130 |
131 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
132 | menuInflater.inflate(R.menu.crop_image_menu, menu)
133 |
134 | if (!cropImageOptions.allowRotation) {
135 | menu.removeItem(R.id.ic_rotate_left_24)
136 | menu.removeItem(R.id.ic_rotate_right_24)
137 | } else if (cropImageOptions.allowCounterRotation) {
138 | menu.findItem(R.id.ic_rotate_left_24).isVisible = true
139 | }
140 |
141 | if (!cropImageOptions.allowFlipping) menu.removeItem(R.id.ic_flip_24)
142 |
143 | if (cropImageOptions.cropMenuCropButtonTitle != null) {
144 | menu.findItem(R.id.crop_image_menu_crop).title =
145 | cropImageOptions.cropMenuCropButtonTitle
146 | }
147 | var cropIcon: Drawable? = null
148 | try {
149 | if (cropImageOptions.cropMenuCropButtonIcon != 0) {
150 | cropIcon = ContextCompat.getDrawable(this, cropImageOptions.cropMenuCropButtonIcon)
151 | menu.findItem(R.id.crop_image_menu_crop).icon = cropIcon
152 | }
153 | } catch (e: Exception) {
154 | Log.w("AIC", "Failed to read menu crop drawable", e)
155 | }
156 | if (cropImageOptions.activityMenuIconColor != 0) {
157 | updateMenuItemIconColor(
158 | menu,
159 | R.id.ic_rotate_left_24,
160 | cropImageOptions.activityMenuIconColor
161 | )
162 | updateMenuItemIconColor(
163 | menu,
164 | R.id.ic_rotate_right_24,
165 | cropImageOptions.activityMenuIconColor
166 | )
167 | updateMenuItemIconColor(menu, R.id.ic_flip_24, cropImageOptions.activityMenuIconColor)
168 |
169 | if (cropIcon != null) {
170 | updateMenuItemIconColor(
171 | menu,
172 | R.id.crop_image_menu_crop,
173 | cropImageOptions.activityMenuIconColor
174 | )
175 | }
176 | }
177 | return true
178 | }
179 |
180 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
181 | when (item.itemId) {
182 | R.id.crop_image_menu_crop -> cropImage()
183 | R.id.ic_rotate_left_24 -> rotateImage(-cropImageOptions.rotationDegrees)
184 | R.id.ic_rotate_right_24 -> rotateImage(cropImageOptions.rotationDegrees)
185 | R.id.ic_flip_24_horizontally -> cropImageView?.flipImageHorizontally()
186 | R.id.ic_flip_24_vertically -> cropImageView?.flipImageVertically()
187 | android.R.id.home -> setResultCancel()
188 | else -> return super.onOptionsItemSelected(item)
189 | }
190 | return true
191 | }
192 |
193 | override fun onBackPressed() {
194 | super.onBackPressed()
195 | setResultCancel()
196 | }
197 |
198 | protected open fun onPickImageResult(resultUri: Uri?) {
199 | when (resultUri) {
200 | null -> setResultCancel()
201 | else -> {
202 | cropImageUri = resultUri
203 | cropImageView?.setImageUriAsync(cropImageUri)
204 | }
205 | }
206 | }
207 |
208 | override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) {
209 | if (error == null) {
210 | if (cropImageOptions.initialCropWindowRectangle != null)
211 | cropImageView?.cropRect = cropImageOptions.initialCropWindowRectangle
212 |
213 | if (cropImageOptions.initialRotation > 0)
214 | cropImageView?.rotatedDegrees = cropImageOptions.initialRotation
215 | } else setResult(null, error, 1)
216 | }
217 |
218 | override fun onCropImageComplete(view: CropImageView, result: CropResult) {
219 | setResult(result.uriContent, result.error, result.sampleSize)
220 | }
221 |
222 | /**
223 | * Execute crop image and save the result tou output uri.
224 | */
225 | open fun cropImage() {
226 | if (cropImageOptions.noOutputImage) setResult(null, null, 1)
227 | else cropImageView?.croppedImageAsync(
228 | saveCompressFormat = cropImageOptions.outputCompressFormat,
229 | saveCompressQuality = cropImageOptions.outputCompressQuality,
230 | reqWidth = cropImageOptions.outputRequestWidth,
231 | reqHeight = cropImageOptions.outputRequestHeight,
232 | options = cropImageOptions.outputRequestSizeOptions,
233 | customOutputUri = cropImageOptions.customOutputUri,
234 | )
235 | }
236 |
237 | /**
238 | * When extending this activity, please set your own ImageCropView
239 | */
240 | open fun setCropImageView(cropImageView: CropImageView) {
241 | this.cropImageView = cropImageView
242 | }
243 |
244 | /**
245 | * Rotate the image in the crop image view.
246 | */
247 | open fun rotateImage(degrees: Int) {
248 | cropImageView?.rotateImage(degrees)
249 | }
250 |
251 | /**
252 | * Result with cropped image data or error if failed.
253 | */
254 | open fun setResult(uri: Uri?, error: Exception?, sampleSize: Int) {
255 | setResult(
256 | error?.let { CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE } ?: RESULT_OK,
257 | getResultIntent(uri, error, sampleSize)
258 | )
259 | finish()
260 | }
261 |
262 | /**
263 | * Cancel of cropping activity.
264 | */
265 | open fun setResultCancel() {
266 | setResult(RESULT_CANCELED)
267 | finish()
268 | }
269 |
270 | /**
271 | * Get intent instance to be used for the result of this activity.
272 | */
273 | open fun getResultIntent(uri: Uri?, error: Exception?, sampleSize: Int): Intent {
274 | val result = CropImage.ActivityResult(
275 | cropImageView?.imageUri,
276 | uri,
277 | error,
278 | cropImageView?.cropPoints,
279 | cropImageView?.cropRect,
280 | cropImageView?.rotatedDegrees ?: 0,
281 | cropImageView?.wholeImageRect,
282 | sampleSize
283 | )
284 | val intent = Intent()
285 | intent.putExtras(getIntent())
286 | intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result)
287 | return intent
288 | }
289 |
290 | /**
291 | * Update the color of a specific menu item to the given color.
292 | */
293 | open fun updateMenuItemIconColor(menu: Menu, itemId: Int, color: Int) {
294 | val menuItem = menu.findItem(itemId)
295 | if (menuItem != null) {
296 | val menuItemIcon = menuItem.icon
297 | if (menuItemIcon != null) {
298 | try {
299 | menuItemIcon.apply {
300 | mutate()
301 | colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
302 | color,
303 | BlendModeCompat.SRC_ATOP
304 | )
305 | }
306 | menuItem.icon = menuItemIcon
307 | } catch (e: Exception) {
308 | Log.w("AIC", "Failed to update menu item color", e)
309 | }
310 | }
311 | }
312 | }
313 |
314 | enum class Source { CAMERA, GALLERY }
315 | }
316 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/CropImageAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.graphics.Matrix
4 | import android.graphics.RectF
5 | import android.view.animation.AccelerateDecelerateInterpolator
6 | import android.view.animation.Animation
7 | import android.view.animation.Animation.AnimationListener
8 | import android.view.animation.Transformation
9 | import android.widget.ImageView
10 |
11 | /**
12 | * Animation to handle smooth cropping image matrix transformation change, specifically for
13 | * zoom-in/out.
14 | */
15 | internal class CropImageAnimation(
16 | private val imageView: ImageView,
17 | private val cropOverlayView: CropOverlayView
18 | ) : Animation(), AnimationListener {
19 |
20 | private val startBoundPoints = FloatArray(8)
21 | private val endBoundPoints = FloatArray(8)
22 | private val startCropWindowRect = RectF()
23 | private val endCropWindowRect = RectF()
24 | private val startImageMatrix = FloatArray(9)
25 | private val endImageMatrix = FloatArray(9)
26 |
27 | init {
28 | duration = 300
29 | fillAfter = true
30 | interpolator = AccelerateDecelerateInterpolator()
31 | setAnimationListener(this)
32 | }
33 |
34 | fun setStartState(boundPoints: FloatArray, imageMatrix: Matrix) {
35 | reset()
36 | System.arraycopy(boundPoints, 0, startBoundPoints, 0, 8)
37 | startCropWindowRect.set(cropOverlayView.cropWindowRect)
38 | imageMatrix.getValues(startImageMatrix)
39 | }
40 |
41 | fun setEndState(boundPoints: FloatArray, imageMatrix: Matrix) {
42 | System.arraycopy(boundPoints, 0, endBoundPoints, 0, 8)
43 | endCropWindowRect.set(cropOverlayView.cropWindowRect)
44 | imageMatrix.getValues(endImageMatrix)
45 | }
46 |
47 | override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
48 | val animRect = RectF().apply {
49 | left = (
50 | startCropWindowRect.left +
51 | (endCropWindowRect.left - startCropWindowRect.left) * interpolatedTime
52 | )
53 | top = (
54 | startCropWindowRect.top +
55 | (endCropWindowRect.top - startCropWindowRect.top) * interpolatedTime
56 | )
57 | right = (
58 | startCropWindowRect.right +
59 | (endCropWindowRect.right - startCropWindowRect.right) * interpolatedTime
60 | )
61 | bottom = (
62 | startCropWindowRect.bottom +
63 | (endCropWindowRect.bottom - startCropWindowRect.bottom) * interpolatedTime
64 | )
65 | }
66 | val animPoints = FloatArray(8)
67 | for (i in animPoints.indices) {
68 | animPoints[i] =
69 | (startBoundPoints[i] + (endBoundPoints[i] - startBoundPoints[i]) * interpolatedTime)
70 | }
71 |
72 | cropOverlayView.apply {
73 | cropWindowRect = animRect
74 | setBounds(animPoints, imageView.width, imageView.height)
75 | invalidate()
76 | }
77 | val animMatrix = FloatArray(9)
78 | for (i in animMatrix.indices) {
79 | animMatrix[i] =
80 | (startImageMatrix[i] + (endImageMatrix[i] - startImageMatrix[i]) * interpolatedTime)
81 | }
82 |
83 | imageView.apply {
84 | imageMatrix.setValues(animMatrix)
85 | invalidate()
86 | }
87 | }
88 |
89 | override fun onAnimationStart(animation: Animation) {}
90 | override fun onAnimationEnd(animation: Animation) {
91 | imageView.clearAnimation()
92 | }
93 |
94 | override fun onAnimationRepeat(animation: Animation) {}
95 | }
96 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/CropImageContract.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.os.Parcelable
8 | import androidx.activity.result.contract.ActivityResultContract
9 |
10 | /**
11 | * An ActivityResultContract to start an activity that allows the user to crop an image.
12 | *
13 | * The activity can be heavily customized by the input CropImageContractOptions.
14 | *
15 | * If you do not provide an uri in the input the user will be asked to pick an image before cropping.
16 | */
17 |
18 | class CropImageContract :
19 | ActivityResultContract() {
20 |
21 | override fun createIntent(context: Context, input: CropImageContractOptions): Intent {
22 | input.cropImageOptions.validate()
23 | return Intent(context, CropImageActivity::class.java).apply {
24 | val bundle = Bundle()
25 | bundle.putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri)
26 | bundle.putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.cropImageOptions)
27 | putExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE, bundle)
28 | }
29 | }
30 |
31 | override fun parseResult(
32 | resultCode: Int,
33 | intent: Intent?
34 | ): CropImageView.CropResult {
35 | val result = intent?.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_RESULT) as? CropImage.ActivityResult?
36 |
37 | return if (result == null || resultCode == Activity.RESULT_CANCELED)
38 | CropImage.CancelledResult
39 | else result
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/common/CommonValues.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.common
2 |
3 | object CommonValues {
4 | const val authority = ".cropper.fileprovider"
5 | const val CROP_LIB_CACHE = "CROP_LIB_CACHE"
6 | }
7 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/common/CommonVersionCheck.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.common
2 |
3 | import android.os.Build
4 | import android.os.Build.VERSION.SDK_INT
5 |
6 | object CommonVersionCheck {
7 |
8 | fun isAtLeastJ18() = SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
9 | fun isAtLeastM23() = SDK_INT >= Build.VERSION_CODES.M
10 | fun isAtLeastO26() = SDK_INT >= Build.VERSION_CODES.O
11 | fun isAtLeastQ29() = SDK_INT >= Build.VERSION_CODES.Q
12 | }
13 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/utils/GetFilePathFromUri.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.utils
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.webkit.MimeTypeMap
7 | import java.io.File
8 | import java.io.FileOutputStream
9 | import java.io.IOException
10 | import java.io.InputStream
11 | import java.io.OutputStream
12 | import java.text.SimpleDateFormat
13 | import java.util.Date
14 | import java.util.Locale.getDefault
15 |
16 | /**
17 | * This class will create a temporary file in the cache if need.
18 | *
19 | * When the uri already have `file://` schema we don't need to create a new file.
20 | * The temporary file will always override a previous one, saving memory.
21 | * Using the cache memory(context.cacheDir) we guarantee to not leak memory
22 | *
23 | * @param context used to access Android APIs, like content resolve, it is your activity/fragment.
24 | * @param uri the URI to load the image from.
25 | * @param uniqueName If true, make each image cropped have a different file name, this could cause
26 | * memory issues, use wisely.
27 | *
28 | * @return string value of the File path.
29 | */
30 | internal fun getFilePathFromUri(context: Context, uri: Uri, uniqueName: Boolean): String =
31 | if (uri.path?.contains("file://") == true) uri.path!!
32 | else getFileFromContentUri(context, uri, uniqueName).path
33 |
34 | private fun getFileFromContentUri(context: Context, contentUri: Uri, uniqueName: Boolean): File {
35 | // Preparing Temp file name
36 | val fileExtension = getFileExtension(context, contentUri) ?: ""
37 | val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", getDefault()).format(Date())
38 | val fileName = ("temp_file_" + if (uniqueName) timeStamp else "") + ".$fileExtension"
39 | // Creating Temp file
40 | val tempFile = File(context.cacheDir, fileName)
41 | tempFile.createNewFile()
42 | // Initialize streams
43 | var oStream: FileOutputStream? = null
44 | var inputStream: InputStream? = null
45 |
46 | try {
47 | oStream = FileOutputStream(tempFile)
48 | inputStream = context.contentResolver.openInputStream(contentUri)
49 |
50 | inputStream?.let { copy(inputStream, oStream) }
51 | oStream.flush()
52 | } catch (e: Exception) {
53 | e.printStackTrace()
54 | } finally {
55 | // Close streams
56 | inputStream?.close()
57 | oStream?.close()
58 | }
59 |
60 | return tempFile
61 | }
62 |
63 | private fun getFileExtension(context: Context, uri: Uri): String? =
64 | if (uri.scheme == ContentResolver.SCHEME_CONTENT)
65 | MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri))
66 | else uri.path?.let { MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(it)).toString()) }
67 |
68 | @Throws(IOException::class)
69 | private fun copy(source: InputStream, target: OutputStream) {
70 | val buf = ByteArray(8192)
71 | var length: Int
72 | while (source.read(buf).also { length = it } > 0) {
73 | target.write(buf, 0, length)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/cropper/src/main/java/com/aemerse/cropper/utils/GetUriForFile.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper.utils
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.util.Log
6 | import androidx.core.content.FileProvider
7 | import com.aemerse.cropper.common.CommonValues
8 | import com.aemerse.cropper.common.CommonVersionCheck
9 | import java.io.File
10 | import java.io.FileInputStream
11 | import java.io.FileOutputStream
12 | import java.io.InputStream
13 | import java.io.OutputStream
14 | import java.nio.file.Files
15 | import java.nio.file.Paths
16 |
17 | /**
18 | * This class exist because of two issues. One is related to the new Scope Storage for OS 10+
19 | * Where we should not access external storage anymore. Because of this we cannot get a external uri
20 | *
21 | * Using FileProvider to retrieve the path can return a value that is not the real one for some devices
22 | * This happen in specific devices and OSs. Because of this is needed to do a lot of if/else and
23 | * try/catch to just use the latest cases when need.
24 | *
25 | * This code is not good, but work. I don't suggest anyone to reproduce it.
26 | *
27 | * Most of the devices will work fine, but if you worry about memory usage, please remember to clean
28 | * the cache from time to time,
29 | */
30 | internal fun getUriForFile(context: Context, file: File): Uri {
31 | val authority = context.packageName + CommonValues.authority
32 | try {
33 | Log.i("AIC", "Try get URI for scope storage - content://")
34 | return FileProvider.getUriForFile(context, authority, file)
35 | } catch (e: Exception) {
36 | try {
37 | Log.e("AIC", "${e.message}")
38 | Log.w(
39 | "AIC",
40 | "ANR Risk -- Copying the file the location cache to avoid 'external-files-path' bug for N+ devices"
41 | )
42 | // Note: Periodically clear this cache
43 | val cacheFolder = File(context.cacheDir, CommonValues.CROP_LIB_CACHE)
44 | val cacheLocation = File(cacheFolder, file.name)
45 | var input: InputStream? = null
46 | var output: OutputStream? = null
47 | try {
48 | input = FileInputStream(file)
49 | output = FileOutputStream(cacheLocation) // appending output stream
50 | input.copyTo(output)
51 | Log.i(
52 | "AIC",
53 | "Completed Android N+ file copy. Attempting to return the cached file"
54 | )
55 | return FileProvider.getUriForFile(context, authority, cacheLocation)
56 | } catch (e: Exception) {
57 | Log.e("AIC", "${e.message}")
58 | Log.i("AIC", "Trying to provide URI manually")
59 | val path = "content://$authority/files/my_images/"
60 |
61 | if (CommonVersionCheck.isAtLeastO26()) {
62 | Files.createDirectories(Paths.get(path))
63 | } else {
64 | val directory = File(path)
65 | if (!directory.exists()) directory.mkdirs()
66 | }
67 |
68 | return Uri.parse("$path${file.name}")
69 | } finally {
70 | input?.close()
71 | output?.close()
72 | }
73 | } catch (e: Exception) {
74 | Log.e("AIC", "${e.message}")
75 |
76 | if (!CommonVersionCheck.isAtLeastQ29()) {
77 | val cacheDir = context.externalCacheDir
78 | cacheDir?.let {
79 | try {
80 | Log.i(
81 | "AIC",
82 | "Use External storage, do not work for OS 29 and above"
83 | )
84 | return Uri.fromFile(File(cacheDir.path, file.absolutePath))
85 | } catch (e: Exception) {
86 | Log.e("AIC", "${e.message}")
87 | }
88 | }
89 | }
90 | // If nothing else work we try
91 | Log.i("AIC", "Try get URI using file://")
92 | return Uri.fromFile(file)
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/cropper/src/main/res/drawable/ic_flip_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/cropper/src/main/res/drawable/ic_rotate_left_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/cropper/src/main/res/drawable/ic_rotate_right_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/cropper/src/main/res/layout/crop_image_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/cropper/src/main/res/layout/crop_image_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
18 |
19 |
24 |
25 |
--------------------------------------------------------------------------------
/cropper/src/main/res/menu/crop_image_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
16 | -
21 |
22 |
25 |
28 |
29 |
30 |
34 |
35 |
--------------------------------------------------------------------------------
/cropper/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/cropper/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Rotate counter clockwise
5 | Rotate
6 | Crop
7 | Flip
8 | Flip horizontally
9 | Flip vertically
10 | Select source
11 | Camera
12 | Gallery
13 | Cancelling, required permissions are not granted
14 |
15 |
--------------------------------------------------------------------------------
/cropper/src/main/res/xml/library_file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
11 |
14 |
--------------------------------------------------------------------------------
/cropper/src/test/java/com/aemerse/cropper/BitmapUtilsTest.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import io.mockk.mockkObject
4 | import io.mockk.unmockkObject
5 | import org.junit.After
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Before
8 | import org.junit.Test
9 |
10 | private const val RECT_WIDTH = 10f
11 | private const val RECT_HEIGHT = 13f
12 | private const val RECT_LEFT = 0f
13 | private const val RECT_RIGHT = 10f
14 | private const val RECT_BOTTOM = 15f
15 | private const val RECT_TOP = 2f
16 | private const val RECT_CENTER_X = 5f
17 | private const val RECT_CENTER_Y = 8.5f
18 | private val RECTANGLE_IMAGE_POINTS: FloatArray = floatArrayOf(RECT_LEFT, RECT_TOP, RECT_WIDTH, RECT_TOP, RECT_WIDTH, RECT_BOTTOM, RECT_LEFT, RECT_BOTTOM)
19 | private val LOW_RECT_POINTS: FloatArray = floatArrayOf(RECT_LEFT)
20 |
21 | class BitmapUtilsTest {
22 |
23 | @Before
24 | fun setup() {
25 | mockkObject(BitmapUtils)
26 | }
27 |
28 | @After
29 | fun teardown() {
30 | unmockkObject(BitmapUtils)
31 | }
32 |
33 | @Test
34 | fun `WHEN float array of rectangle points is provided, THEN result should be left value of the bounding rectangle`() {
35 | // WHEN
36 | val rectLeft = BitmapUtils.getRectLeft(RECTANGLE_IMAGE_POINTS)
37 |
38 | // THEN
39 | assertEquals(RECT_LEFT, rectLeft)
40 | }
41 |
42 | @Test
43 | fun `WHEN float array of rectangle points is provided, THEN result should be right value of the bounding rectangle`() {
44 | // WHEN
45 | val rectRight = BitmapUtils.getRectRight(RECTANGLE_IMAGE_POINTS)
46 |
47 | // THEN
48 | assertEquals(RECT_RIGHT, rectRight)
49 | }
50 |
51 | @Test
52 | fun `WHEN float array of rectangle points is provided, THEN result should be bottom value of the bounding rectangle`() {
53 | // WHEN
54 | val rectBottom = BitmapUtils.getRectBottom(RECTANGLE_IMAGE_POINTS)
55 |
56 | // THEN
57 | assertEquals(RECT_BOTTOM, rectBottom)
58 | }
59 |
60 | @Test
61 | fun `WHEN float array of rectangle points is provided, THEN result should be top value of the bounding rectangle`() {
62 | // WHEN
63 | val rectTop = BitmapUtils.getRectTop(RECTANGLE_IMAGE_POINTS)
64 |
65 | // THEN
66 | assertEquals(RECT_TOP, rectTop)
67 | }
68 |
69 | @Test
70 | fun `WHEN float array of rectangle points is provided, THEN result should be rect height`() {
71 | // WHEN
72 | val rectHeight = BitmapUtils.getRectHeight(RECTANGLE_IMAGE_POINTS)
73 |
74 | // THEN
75 | assertEquals(RECT_HEIGHT, rectHeight)
76 | }
77 |
78 | @Test
79 | fun `WHEN float array of rectangle points is provided, THEN result should be rect width`() {
80 | // WHEN
81 | val rectWidth = BitmapUtils.getRectWidth(RECTANGLE_IMAGE_POINTS)
82 |
83 | // THEN
84 | assertEquals(RECT_WIDTH, rectWidth)
85 | }
86 |
87 | @Test
88 | fun `WHEN float array of rectangle points is provided, THEN result should be rect centerX`() {
89 | // WHEN
90 | val rectCenterX = BitmapUtils.getRectCenterX(RECTANGLE_IMAGE_POINTS)
91 |
92 | // THEN
93 | assertEquals(RECT_CENTER_X, rectCenterX)
94 | }
95 |
96 | @Test
97 | fun `WHEN float array of rectangle points is provided, THEN result should be rect centerY`() {
98 | // WHEN
99 | val rectCenterY = BitmapUtils.getRectCenterY(RECTANGLE_IMAGE_POINTS)
100 |
101 | // THEN
102 | assertEquals(RECT_CENTER_Y, rectCenterY)
103 | }
104 |
105 | @Test(expected = ArrayIndexOutOfBoundsException::class)
106 | fun `WHEN low rectangle points is provided to getRectCenterY, THEN resultArrayOutOfIndexException`() {
107 | BitmapUtils.getRectCenterY(LOW_RECT_POINTS)
108 | }
109 |
110 | @Test(expected = ArrayIndexOutOfBoundsException::class)
111 | fun `WHEN low rectangle points is provided to getRectCenterX, THEN resultArrayOutOfIndexException`() {
112 | BitmapUtils.getRectCenterX(LOW_RECT_POINTS)
113 | }
114 |
115 | @Test(expected = ArrayIndexOutOfBoundsException::class)
116 | fun `WHEN low rectangle points is provided to getRectLeft, THEN resultArrayOutOfIndexException`() {
117 | BitmapUtils.getRectLeft(LOW_RECT_POINTS)
118 | }
119 |
120 | @Test(expected = ArrayIndexOutOfBoundsException::class)
121 | fun `WHEN low rectangle points is provided to getRectRight, THEN resultArrayOutOfIndexException`() {
122 | BitmapUtils.getRectRight(LOW_RECT_POINTS)
123 | }
124 |
125 | @Test(expected = ArrayIndexOutOfBoundsException::class)
126 | fun `WHEN low rectangle points is provided getRectTop, THEN resultArrayOutOfIndexException`() {
127 | BitmapUtils.getRectTop(LOW_RECT_POINTS)
128 | }
129 |
130 | @Test(expected = ArrayIndexOutOfBoundsException::class)
131 | fun `WHEN low rectangle points is provided getRectBottom, THEN resultArrayOutOfIndexException`() {
132 | BitmapUtils.getRectBottom(LOW_RECT_POINTS)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/cropper/src/test/java/com/aemerse/cropper/ContractTestFragment.kt:
--------------------------------------------------------------------------------
1 | package com.aemerse.cropper
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.activity.result.ActivityResultRegistry
6 | import androidx.fragment.app.Fragment
7 |
8 | class ContractTestFragment(registry: ActivityResultRegistry) : Fragment() {
9 |
10 | var cropResult: CropImageView.CropResult? = null
11 | var pickResult: Uri? = null
12 |
13 | val cropImage = registerForActivityResult(CropImageContract(), registry) { result ->
14 | this.cropResult = result
15 | }
16 |
17 | fun cropImage(input: CropImageContractOptions) {
18 | cropImage.launch(input)
19 | }
20 |
21 | fun cropImageIntent(input: CropImageContractOptions): Intent {
22 | return cropImage.contract.createIntent(requireContext(), input)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # https://docs.gradle.org/current/userguide/build_environment.html
2 |
3 | #Specifies the JVM arguments used for the Gradle Daemon. The setting is particularly useful for
4 | # configuring JVM memory settings for build performance.
5 | #-Xmx4G: Increase the amount of memory allocated to the Gradle Daemon VM to 4 Gb.
6 | #-Xmx2048m == 2GB
7 | org.gradle.jvmargs=-Xmx4G
8 |
9 | # When set to true, this will force Gradle to execute tasks in parallel as long as those tasks are
10 | # in different projects.
11 | org.gradle.parallel=true
12 |
13 | # For Gradle to know exactly how to build your app, the build system configures all modules in your
14 | # project, and their dependencies, before every build
15 | # —
16 | # even if you are building and testing only a single module. This slows down the build process for
17 | # large multi-module projects. Setting this field will make Gradle attempt to configure only
18 | # necessary projects.
19 | org.gradle.configureondemand=true
20 |
21 | # When set to true, Gradle will reuse task outputs from any previous build, when possible.
22 | org.gradle.caching=true
23 |
24 | # AndroidX package structure to make it clearer which packages are bundled with the
25 | # Android operating system, and which are packaged with your app's APK
26 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
27 | # Android plugin uses the appropriate AndroidX library instead of a Support Library.
28 | android.useAndroidX=true
--------------------------------------------------------------------------------
/gradle/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradle/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Cropper/fc7bc765ce56594fd39da9e6324ded206bfae3ee/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Nov 28 12:02:04 AEDT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk11
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Sat Dec 04 10:52:52 IST 2021
8 | sdk.dir=/Users/akshaaatt/Library/Android/sdk
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'cropper'
2 | include 'app'
--------------------------------------------------------------------------------