Skip to content

Commit 3693506

Browse files
authored
Merge pull request #184 from baronha/feature/open-crop
Crop 🪚
2 parents 8187c65 + 677b316 commit 3693506

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1912
-213
lines changed

android/src/main/java/com/margelo/nitro/multipleimagepicker/CropEngine.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ class CropImageEngine : UCropImageEngine {
5151
}
5252
}
5353

54-
class CropEngine(cropOption: UCrop.Options) : CropFileEngine {
55-
private val options: UCrop.Options = cropOption
54+
class CropEngine(cropOption: Options) : CropFileEngine {
55+
private val options: Options = cropOption
56+
5657
override fun onStartCrop(
5758
fragment: Fragment,
5859
srcUri: Uri?,
@@ -78,6 +79,7 @@ class MediaEditInterceptListener(
7879
val inputUri =
7980
if (PictureMimeType.isContent(currentEditPath)) Uri.parse(currentEditPath)
8081
else Uri.fromFile(File(currentEditPath))
82+
8183
val destinationUri = Uri.fromFile(
8284
File(outputCropPath, DateUtils.getCreateFileName("CROP_") + ".jpeg")
8385
)

android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePicker.kt

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package com.margelo.nitro.multipleimagepicker
22

33
import com.margelo.nitro.NitroModules
4-
import com.margelo.nitro.multipleimagepicker.HybridMultipleImagePickerSpec
5-
import com.margelo.nitro.multipleimagepicker.NitroConfig
6-
import com.margelo.nitro.multipleimagepicker.Result
74

85

9-
class MultipleImagePicker: HybridMultipleImagePickerSpec() {
6+
class MultipleImagePicker : HybridMultipleImagePickerSpec() {
107
override val memorySize: Long
118
get() = 5
129

@@ -20,7 +17,15 @@ class MultipleImagePicker: HybridMultipleImagePickerSpec() {
2017
pickerModule.openPicker(config, resolved, rejected)
2118
}
2219

20+
override fun openCrop(
21+
image: String,
22+
config: NitroCropConfig,
23+
resolved: (result: CropResult) -> Unit,
24+
rejected: (reject: Double) -> Unit
25+
) {
2326

27+
pickerModule.openCrop(image, config, resolved, rejected)
28+
}
2429

2530

2631
}

android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePickerImp.kt

+174-30
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package com.margelo.nitro.multipleimagepicker
22

3+
import android.app.Activity
4+
import android.content.ContentResolver
35
import android.content.Context
6+
import android.content.Intent
47
import android.graphics.Color
8+
import android.net.Uri
59
import androidx.core.content.ContextCompat
10+
import com.facebook.react.bridge.ActivityEventListener
11+
import com.facebook.react.bridge.BaseActivityEventListener
612
import com.facebook.react.bridge.ColorPropConverter
713
import com.facebook.react.bridge.ReactApplicationContext
814
import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -23,8 +29,16 @@ import com.luck.picture.lib.style.PictureSelectorStyle
2329
import com.luck.picture.lib.style.PictureWindowAnimationStyle
2430
import com.luck.picture.lib.style.SelectMainStyle
2531
import com.luck.picture.lib.style.TitleBarStyle
32+
import com.luck.picture.lib.utils.DateUtils
2633
import com.luck.picture.lib.utils.DensityUtil
34+
import com.yalantis.ucrop.UCrop
2735
import com.yalantis.ucrop.UCrop.Options
36+
import com.yalantis.ucrop.UCrop.REQUEST_CROP
37+
import com.yalantis.ucrop.model.AspectRatio
38+
import java.io.File
39+
import java.net.HttpURLConnection
40+
import java.net.URL
41+
2842

2943
class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
3044
ReactContextBaseJavaModule(reactContext), IApp {
@@ -65,7 +79,6 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
6579
else -> SelectMimeType.ofAll()
6680
}
6781

68-
6982
val maxSelect = config.maxSelect?.toInt() ?: 20
7083
val maxVideo = config.maxVideo?.toInt() ?: 20
7184
val isPreview = config.isPreview ?: true
@@ -82,13 +95,10 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
8295

8396
val isCrop = config.crop != null
8497

85-
PictureSelector.create(activity)
86-
.openGallery(chooseMode)
87-
.setImageEngine(imageEngine)
88-
.setSelectedData(dataList)
89-
.setSelectorUIStyle(style).apply {
98+
PictureSelector.create(activity).openGallery(chooseMode).setImageEngine(imageEngine)
99+
.setSelectedData(dataList).setSelectorUIStyle(style).apply {
90100
if (isCrop) {
91-
setCropOption()
101+
setCropOption(config.crop)
92102
// Disabled force crop engine for multiple
93103
if (!isMultiple) setCropEngine(CropEngine(cropOption))
94104
else setEditMediaInterceptListener(setEditMediaEvent())
@@ -113,28 +123,18 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
113123
if (videoQuality != null && videoQuality != 1.0) {
114124
setVideoQuality(if (videoQuality > 0.5) 1 else 0)
115125
}
116-
}
117-
.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
118-
.setMaxSelectNum(maxSelect)
119-
.isDirectReturnSingle(true)
120-
.isSelectZoomAnim(true)
121-
.isPageStrategy(true, 50)
126+
}.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3).setMaxSelectNum(maxSelect)
127+
.isDirectReturnSingle(true).isSelectZoomAnim(true).isPageStrategy(true, 50)
122128
.isWithSelectVideoImage(true)
123129
.setMaxVideoSelectNum(if (maxVideo != 20) maxVideo else maxSelect)
124-
.isMaxSelectEnabledMask(true)
125-
.isAutoVideoPlay(true)
126-
.isFastSlidingSelect(allowSwipeToSelect)
127-
.isPageSyncAlbumCount(true)
130+
.isMaxSelectEnabledMask(true).isAutoVideoPlay(true)
131+
.isFastSlidingSelect(allowSwipeToSelect).isPageSyncAlbumCount(true)
128132
// isPreview
129-
.isPreviewImage(isPreview)
130-
.isPreviewVideo(isPreview)
133+
.isPreviewImage(isPreview).isPreviewVideo(isPreview)
131134
//
132-
.isDisplayCamera(config.allowedCamera ?: true)
133-
.isDisplayTimeAxis(true)
134-
.setSelectionMode(selectMode)
135-
.isOriginalControl(config.isHiddenOriginalButton == false)
136-
.setLanguage(getLanguage())
137-
.isPreviewFullScreenMode(true)
135+
.isDisplayCamera(config.allowedCamera ?: true).isDisplayTimeAxis(true)
136+
.setSelectionMode(selectMode).isOriginalControl(config.isHiddenOriginalButton == false)
137+
.setLanguage(getLanguage()).isPreviewFullScreenMode(true)
138138
.forResult(object : OnResultCallbackListener<LocalMedia?> {
139139
override fun onResult(localMedia: ArrayList<LocalMedia?>?) {
140140
var data: Array<Result> = arrayOf()
@@ -161,6 +161,112 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
161161
})
162162
}
163163

164+
@ReactMethod
165+
fun openCrop(
166+
image: String,
167+
options: NitroCropConfig,
168+
resolved: (result: CropResult) -> Unit,
169+
rejected: (reject: Double) -> Unit
170+
) {
171+
172+
173+
fun isImage(uri: Uri, contentResolver: ContentResolver): Boolean {
174+
val mimeType: String? = contentResolver.getType(uri)
175+
return mimeType?.startsWith("image/") == true
176+
}
177+
178+
val uri = Uri.parse(image)
179+
val isImageFile = isImage(uri, appContext.contentResolver)
180+
181+
if (!isImageFile) return rejected(0.0)
182+
183+
cropOption = Options()
184+
185+
setCropOption(
186+
PickerCropConfig(
187+
circle = options.circle,
188+
ratio = options.ratio,
189+
defaultRatio = options.defaultRatio,
190+
freeStyle = options.freeStyle
191+
)
192+
)
193+
194+
try {
195+
val uri = when {
196+
// image network
197+
image.startsWith("http://") || image.startsWith("https://") -> {
198+
// Handle remote URL
199+
val url = URL(image)
200+
val connection = url.openConnection() as HttpURLConnection
201+
connection.doInput = true
202+
connection.connect()
203+
204+
val inputStream = connection.inputStream
205+
// Create a temp file to store the image
206+
val file = File(appContext.cacheDir, "CROP_")
207+
file.outputStream().use { output ->
208+
inputStream.copyTo(output)
209+
}
210+
211+
Uri.fromFile(file)
212+
}
213+
214+
215+
else -> {
216+
Uri.parse(image)
217+
}
218+
}
219+
220+
221+
val destinationUri = Uri.fromFile(
222+
File(getSandboxPath(appContext), DateUtils.getCreateFileName("CROP_") + ".jpeg")
223+
)
224+
225+
val uCrop = UCrop.of<Any>(uri, destinationUri).withOptions(cropOption)
226+
227+
// set engine
228+
uCrop.setImageEngine(CropImageEngine())
229+
// start edit
230+
231+
val cropActivityEventListener = object : BaseActivityEventListener() {
232+
override fun onActivityResult(
233+
activity: Activity,
234+
requestCode: Int,
235+
resultCode: Int,
236+
data: Intent?
237+
) {
238+
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CROP) {
239+
val resultUri = UCrop.getOutput(data!!)
240+
val width = UCrop.getOutputImageWidth(data).toDouble()
241+
val height = UCrop.getOutputImageHeight(data).toDouble()
242+
243+
resultUri?.let {
244+
val result = CropResult(
245+
path = it.toString(),
246+
width,
247+
height,
248+
)
249+
resolved(result)
250+
}
251+
} else if (resultCode == UCrop.RESULT_ERROR) {
252+
val cropError = UCrop.getError(data!!)
253+
rejected(0.0)
254+
}
255+
256+
// Remove listener after getting result
257+
reactApplicationContext.removeActivityEventListener(this)
258+
}
259+
}
260+
261+
// Add listener before starting UCrop
262+
reactApplicationContext.addActivityEventListener(cropActivityEventListener)
263+
264+
currentActivity?.let { uCrop.start(it, REQUEST_CROP) }
265+
} catch (e: Exception) {
266+
rejected(0.0)
267+
}
268+
}
269+
164270
private fun getLanguage(): Int {
165271
return when (config.language) {
166272
Language.VI -> LanguageConfig.VIETNAM // -> 🇻🇳 My country. Yeahhh
@@ -177,12 +283,10 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
177283
}
178284
}
179285

180-
private fun setCropOption() {
181-
// val mainStyle: SelectMainStyle = style.selectMainStyle
182-
286+
private fun setCropOption(config: PickerCropConfig?) {
183287
cropOption.setShowCropFrame(true)
184288
cropOption.setShowCropGrid(true)
185-
cropOption.setCircleDimmedLayer(config.crop?.circle ?: false)
289+
cropOption.setCircleDimmedLayer(config?.circle ?: false)
186290
cropOption.setCropOutputPathDir(getSandboxPath(appContext))
187291
cropOption.isCropDragSmoothToCenter(true)
188292
cropOption.isForbidSkipMultipleCrop(true)
@@ -191,8 +295,48 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
191295
cropOption.setStatusBarColor(Color.WHITE)
192296
cropOption.isDarkStatusBarBlack(true)
193297
cropOption.isDragCropImages(true)
194-
cropOption.setFreeStyleCropEnabled(true)
298+
cropOption.setFreeStyleCropEnabled(config?.freeStyle ?: true)
195299
cropOption.setSkipCropMimeType(*getNotSupportCrop())
300+
301+
302+
val ratioCount = config?.ratio?.size ?: 0
303+
304+
if (config?.defaultRatio != null || ratioCount > 0) {
305+
306+
var ratioList = arrayOf(AspectRatio("Original", 0f, 0f))
307+
308+
if (ratioCount > 0) {
309+
config?.ratio?.take(4)?.toTypedArray()?.forEach { item ->
310+
ratioList += AspectRatio(
311+
item.title, item.width.toFloat(), item.height.toFloat()
312+
)
313+
}
314+
}
315+
316+
// Add default Aspects
317+
ratioList += arrayOf(
318+
AspectRatio(null, 1f, 1f),
319+
AspectRatio(null, 16f, 9f),
320+
AspectRatio(null, 4f, 3f),
321+
AspectRatio(null, 3f, 2f)
322+
)
323+
324+
config?.defaultRatio?.let {
325+
val defaultRatio = AspectRatio(it.title, it.width.toFloat(), it.height.toFloat())
326+
ratioList = arrayOf(defaultRatio) + ratioList
327+
328+
}
329+
330+
cropOption.apply {
331+
332+
setAspectRatioOptions(
333+
0,
334+
*ratioList.take(5).toTypedArray()
335+
)
336+
337+
}
338+
339+
}
196340
}
197341

198342

docs/docs/CONFIG.mdx

+45-2
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,59 @@ Maximum number of videos allowed.
8181
- **Required**: No
8282
- **Platform**: iOS, Android
8383

84-
### `crop`
84+
## Crop 🪚
8585

8686
Configuration for image cropping functionality.
8787

8888
- **Type**: object
8989
- **Default**: `undefined`
9090
- **Required**: No
9191
- **Platform**: iOS, Android
92+
93+
### `circle`
94+
95+
Enable circular crop mask.
96+
97+
- **Type**: boolean
98+
- **Default**: `false`
99+
- **Required**: No
100+
- **Platform**: iOS, Android
101+
102+
### `ratio`
103+
104+
Aspect ratios for cropping.
105+
Android: Maximum: 4 items
106+
107+
- **Type**: `array`
108+
- **Default**: `undefined`
109+
- **Required**: No
110+
- **Platform**: iOS, Android
92111
- **Properties**:
93-
- `circle`: boolean - Enable circular crop mask
112+
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
113+
- `width`: number - Width value for the aspect ratio
114+
- `height`: number - Height value for the aspect ratio
115+
116+
### `defaultRatio`
117+
118+
Default ratio to be selected when opening the crop interface.
119+
120+
- **Type**: `object`
121+
- **Default**: `undefined`
122+
- **Required**: No
123+
- **Platform**: iOS, Android
124+
- **Properties**:
125+
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
126+
- `width`: number - Width value for the aspect ratio
127+
- `height`: number - Height value for the aspect ratio
128+
129+
### `freeStyle`
130+
131+
Enable free style cropping.
132+
133+
- **Type**: `boolean`
134+
- **Default**: `false`
135+
- **Required**: No
136+
- **Platform**: iOS, Android
94137

95138
---
96139

0 commit comments

Comments
 (0)