Skip to content

Commit 604d85d

Browse files
committed
✨ feat: add custom crop ratio
1 parent d3cc6df commit 604d85d

Some content is hidden

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

44 files changed

+1416
-178
lines changed

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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.margelo.nitro.multipleimagepicker.NitroConfig
66
import com.margelo.nitro.multipleimagepicker.Result
77

88

9-
class MultipleImagePicker: HybridMultipleImagePickerSpec() {
9+
class MultipleImagePicker : HybridMultipleImagePickerSpec() {
1010
override val memorySize: Long
1111
get() = 5
1212

@@ -20,7 +20,15 @@ class MultipleImagePicker: HybridMultipleImagePickerSpec() {
2020
pickerModule.openPicker(config, resolved, rejected)
2121
}
2222

23+
override fun openCrop(
24+
image: String,
25+
config: NitroCropConfig,
26+
resolved: (result: CropResult) -> Unit,
27+
rejected: (reject: Double) -> Unit
28+
) {
2329

30+
pickerModule.openCrop(image, config, resolved, rejected)
31+
}
2432

2533

2634
}

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

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.margelo.nitro.multipleimagepicker
22

33
import android.content.Context
44
import android.graphics.Color
5+
import android.net.Uri
56
import androidx.core.content.ContextCompat
67
import com.facebook.react.bridge.ColorPropConverter
78
import com.facebook.react.bridge.ReactApplicationContext
@@ -23,8 +24,14 @@ import com.luck.picture.lib.style.PictureSelectorStyle
2324
import com.luck.picture.lib.style.PictureWindowAnimationStyle
2425
import com.luck.picture.lib.style.SelectMainStyle
2526
import com.luck.picture.lib.style.TitleBarStyle
27+
import com.luck.picture.lib.utils.DateUtils
2628
import com.luck.picture.lib.utils.DensityUtil
29+
import com.yalantis.ucrop.UCrop
2730
import com.yalantis.ucrop.UCrop.Options
31+
import com.yalantis.ucrop.model.AspectRatio
32+
import java.io.File
33+
import java.net.HttpURLConnection
34+
import java.net.URL
2835

2936
class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
3037
ReactContextBaseJavaModule(reactContext), IApp {
@@ -65,7 +72,6 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
6572
else -> SelectMimeType.ofAll()
6673
}
6774

68-
6975
val maxSelect = config.maxSelect?.toInt() ?: 20
7076
val maxVideo = config.maxVideo?.toInt() ?: 20
7177
val isPreview = config.isPreview ?: true
@@ -82,11 +88,8 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
8288

8389
val isCrop = config.crop != null
8490

85-
PictureSelector.create(activity)
86-
.openGallery(chooseMode)
87-
.setImageEngine(imageEngine)
88-
.setSelectedData(dataList)
89-
.setSelectorUIStyle(style).apply {
91+
PictureSelector.create(activity).openGallery(chooseMode).setImageEngine(imageEngine)
92+
.setSelectedData(dataList).setSelectorUIStyle(style).apply {
9093
if (isCrop) {
9194
setCropOption()
9295
// Disabled force crop engine for multiple
@@ -113,28 +116,18 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
113116
if (videoQuality != null && videoQuality != 1.0) {
114117
setVideoQuality(if (videoQuality > 0.5) 1 else 0)
115118
}
116-
}
117-
.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
118-
.setMaxSelectNum(maxSelect)
119-
.isDirectReturnSingle(true)
120-
.isSelectZoomAnim(true)
121-
.isPageStrategy(true, 50)
119+
}.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3).setMaxSelectNum(maxSelect)
120+
.isDirectReturnSingle(true).isSelectZoomAnim(true).isPageStrategy(true, 50)
122121
.isWithSelectVideoImage(true)
123122
.setMaxVideoSelectNum(if (maxVideo != 20) maxVideo else maxSelect)
124-
.isMaxSelectEnabledMask(true)
125-
.isAutoVideoPlay(true)
126-
.isFastSlidingSelect(allowSwipeToSelect)
127-
.isPageSyncAlbumCount(true)
123+
.isMaxSelectEnabledMask(true).isAutoVideoPlay(true)
124+
.isFastSlidingSelect(allowSwipeToSelect).isPageSyncAlbumCount(true)
128125
// isPreview
129-
.isPreviewImage(isPreview)
130-
.isPreviewVideo(isPreview)
126+
.isPreviewImage(isPreview).isPreviewVideo(isPreview)
131127
//
132-
.isDisplayCamera(config.allowedCamera ?: true)
133-
.isDisplayTimeAxis(true)
134-
.setSelectionMode(selectMode)
135-
.isOriginalControl(config.isHiddenOriginalButton == false)
136-
.setLanguage(getLanguage())
137-
.isPreviewFullScreenMode(true)
128+
.isDisplayCamera(config.allowedCamera ?: true).isDisplayTimeAxis(true)
129+
.setSelectionMode(selectMode).isOriginalControl(config.isHiddenOriginalButton == false)
130+
.setLanguage(getLanguage()).isPreviewFullScreenMode(true)
138131
.forResult(object : OnResultCallbackListener<LocalMedia?> {
139132
override fun onResult(localMedia: ArrayList<LocalMedia?>?) {
140133
var data: Array<Result> = arrayOf()
@@ -161,6 +154,59 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
161154
})
162155
}
163156

157+
@ReactMethod
158+
fun openCrop(
159+
image: String,
160+
options: NitroCropConfig,
161+
resolved: (result: CropResult) -> Unit,
162+
rejected: (reject: Double) -> Unit
163+
) {
164+
setCropOption()
165+
166+
try {
167+
val uri = when {
168+
// image network
169+
image.startsWith("http://") || image.startsWith("https://") -> {
170+
// Handle remote URL
171+
val url = URL(image)
172+
val connection = url.openConnection() as HttpURLConnection
173+
connection.doInput = true
174+
connection.connect()
175+
176+
val inputStream = connection.inputStream
177+
// Create a temp file to store the image
178+
val file = File(appContext.cacheDir, "CROP_")
179+
file.outputStream().use { output ->
180+
inputStream.copyTo(output)
181+
}
182+
183+
Uri.fromFile(file)
184+
}
185+
186+
187+
else -> {
188+
Uri.parse(image)
189+
}
190+
}
191+
192+
val destinationUri = Uri.fromFile(
193+
File(getSandboxPath(appContext), DateUtils.getCreateFileName("CROP_") + ".jpeg")
194+
)
195+
196+
val uCrop = UCrop.of<Any>(uri, destinationUri).withOptions(cropOption)
197+
198+
199+
// set engine
200+
uCrop.setImageEngine(CropImageEngine())
201+
// start edit
202+
currentActivity?.let { uCrop.start(it, 0) }
203+
204+
205+
} catch (e: Exception) {
206+
rejected(0.0)
207+
}
208+
}
209+
164210
private fun getLanguage(): Int {
165211
return when (config.language) {
166212
Language.VI -> LanguageConfig.VIETNAM // -> 🇻🇳 My country. Yeahhh
@@ -193,6 +239,19 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
193239
cropOption.isDragCropImages(true)
194240
cropOption.setFreeStyleCropEnabled(true)
195241
cropOption.setSkipCropMimeType(*getNotSupportCrop())
242+
243+
cropOption.apply {
244+
setAspectRatioOptions(
245+
1,
246+
AspectRatio(null, 1f, 2f),
247+
AspectRatio(null, 3f, 4f),
248+
AspectRatio(null, 5f, 3f),
249+
AspectRatio(null, 16f, 9f),
250+
AspectRatio(null, 1f, 1f)
251+
)
252+
}
253+
254+
cropOption.setRat
196255
}
197256

198257

example/package.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
},
1515
"dependencies": {
1616
"@baronha/react-native-image-grid": "^0.2.7",
17-
"@gorhom/bottom-sheet": "^5.0.6",
1817
"@react-native-segmented-control/segmented-control": "2.5.2",
1918
"expo": "~51.0.38",
2019
"expo-build-properties": "^0.12.5",
@@ -23,9 +22,6 @@
2322
"immer": "^10.1.1",
2423
"react": "18.2.0",
2524
"react-native": "0.75.0",
26-
"react-native-gesture-handler": "~2.16.1",
27-
"react-native-reanimated": "~3.10.1",
28-
"reanimated-color-picker": "^3.0.6",
2925
"use-immer": "^0.10.0"
3026
},
3127
"devDependencies": {

example/src/components/Input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function Input({ ...props }: InputProps) {
1111
<TextInput
1212
{...props}
1313
selectionColor={foreground}
14+
placeholderTextColor={foreground + '92'}
1415
style={[
1516
style.input,
1617
{ backgroundColor: background_2, color: foreground },

example/src/index.tsx

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
LayoutAnimation,
88
Platform,
99
SafeAreaView,
10+
ScrollView,
1011
Switch,
1112
TouchableOpacity,
1213
useColorScheme,
@@ -19,6 +20,7 @@ import {
1920
Result,
2021
defaultOptions,
2122
Config,
23+
openCrop,
2224
} from '@baronha/react-native-multiple-image-picker'
2325
import { useImmer } from 'use-immer'
2426
import { StatusBar } from 'expo-status-bar'
@@ -35,10 +37,6 @@ import {
3537
} from './components'
3638
import useTheme from './hook/useTheme'
3739
import assets from './assets'
38-
import Animated, {
39-
useAnimatedScrollHandler,
40-
useSharedValue,
41-
} from 'react-native-reanimated'
4240
import { WIDTH } from './theme/size'
4341
import { IS_IOS, LOCALIZED_LANGUAGES } from './common/const'
4442
import { AppContext } from './hook/context'
@@ -66,19 +64,9 @@ export default function App() {
6664
const { background, foreground } = useTheme()
6765
const [images, setImages] = useState<Result[]>([])
6866
const [options, changeOptions] = useImmer<Config>(defaultOptions)
69-
const scrollY = useSharedValue(0)
7067

7168
const colorScheme = useColorScheme()
7269

73-
const onScroll = useAnimatedScrollHandler(
74-
{
75-
onScroll: (e) => {
76-
scrollY.value = e.contentOffset.y
77-
},
78-
},
79-
[]
80-
)
81-
8270
const setOptions = (key: keyof Config, value: Config[keyof Config]) => {
8371
changeOptions((draft) => {
8472
draft[key] = value as any
@@ -94,6 +82,7 @@ export default function App() {
9482
const response = await openPicker({
9583
...options,
9684
selectedAssets: images,
85+
crop: {},
9786
})
9887

9988
setImages(Array.isArray(response) ? response : [response])
@@ -103,6 +92,20 @@ export default function App() {
10392
}
10493
}
10594

95+
const onCrop = async () => {
96+
try {
97+
console.log('images: ', images)
98+
const response = await openCrop(images[0].path, {
99+
circle: true,
100+
})
101+
102+
console.log('response: ', response)
103+
layoutEffect()
104+
} catch (e) {
105+
console.log('e: ', e)
106+
}
107+
}
108+
106109
const onRemovePhoto = (_: Result, index: number) => {
107110
const data = [...images].filter((_, idx) => idx !== index)
108111
setImages(data)
@@ -136,30 +139,34 @@ export default function App() {
136139
style={style.keyboardAvoidingView}
137140
>
138141
<AppContext.Provider value={{ options, setOptions }}>
139-
<Animated.ScrollView
142+
<ScrollView
140143
keyboardDismissMode="on-drag"
141144
keyboardShouldPersistTaps="handled"
142145
showsVerticalScrollIndicator={false}
143146
contentContainerStyle={[
144147
style.scrollView,
145148
{ backgroundColor: background },
146149
]}
147-
onScroll={onScroll}
148150
scrollEventThrottle={16}
149151
>
150152
{images.length > 0 ? (
151-
<ImageGrid
152-
dataImage={images}
153-
onPressImage={onPressImage}
154-
width={WIDTH - 6}
155-
sourceKey={'path'}
156-
videoKey={'type'}
157-
prefixPath={Platform.OS === 'ios' ? 'file://' : ''}
158-
conditionCheckVideo={'video'}
159-
videoURLKey={'thumbnail'}
160-
showDelete
161-
onDeleteImage={onRemovePhoto}
162-
/>
153+
<>
154+
<ImageGrid
155+
dataImage={images}
156+
onPressImage={onPressImage}
157+
width={WIDTH - 6}
158+
sourceKey={'path'}
159+
videoKey={'type'}
160+
prefixPath={Platform.OS === 'ios' ? 'file://' : ''}
161+
conditionCheckVideo={'video'}
162+
videoURLKey={'thumbnail'}
163+
showDelete
164+
onDeleteImage={onRemovePhoto}
165+
/>
166+
<Button style={style.buttonOpen} onPress={onCrop}>
167+
Open Cropping
168+
</Button>
169+
</>
163170
) : (
164171
<TouchableOpacity style={style.buttonPlus} onPress={onPicker}>
165172
<Image source={assets.plusSign} style={style.plusSign} />
@@ -504,7 +511,7 @@ export default function App() {
504511
</Row>
505512
</View>
506513
</View>
507-
</Animated.ScrollView>
514+
</ScrollView>
508515
</AppContext.Provider>
509516
</KeyboardAvoidingView>
510517

0 commit comments

Comments
 (0)