├── .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 | GitHub issues 6 | 7 | 8 | 9 | GitHub last commit 11 | App Logo 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 |