@onekeyfe/react-native-image-crop-picker 3.0.147 → 3.0.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +48 -5
  2. package/ReactNativeImageCropPicker.podspec +5 -3
  3. package/android/src/main/AndroidManifest.xml +2 -2
  4. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +8 -18
  5. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +21 -46
  6. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperActivity.kt +636 -0
  7. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperTheme.kt +188 -0
  8. package/android/src/main/res/drawable/image_crop_picker_rotate.xml +11 -0
  9. package/ios/ImageCropPickerImageProcessor.swift +8 -4
  10. package/ios/ImageCropPickerSession.swift +43 -46
  11. package/ios/ImageCropperTheme.swift +155 -0
  12. package/ios/ImageCropperViewController.swift +462 -0
  13. package/ios/TOCropViewController/Views/TOCropOverlayView.h +9 -0
  14. package/ios/TOCropViewController/Views/TOCropOverlayView.m +47 -3
  15. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +17 -11
  16. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -1
  17. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +8 -0
  18. package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +9 -45
  19. package/nitrogen/generated/android/c++/JImageCropperAppearance.hpp +108 -0
  20. package/nitrogen/generated/android/c++/JImageCropperColorScheme.hpp +58 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +5 -55
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperAppearance.kt +111 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperColorScheme.kt +23 -0
  24. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +36 -0
  25. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +6 -0
  26. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +6 -0
  27. package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +6 -193
  28. package/nitrogen/generated/ios/swift/ImageCropperAppearance.swift +251 -0
  29. package/nitrogen/generated/ios/swift/ImageCropperColorScheme.swift +40 -0
  30. package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +8 -46
  31. package/nitrogen/generated/shared/c++/ImageCropperAppearance.hpp +134 -0
  32. package/nitrogen/generated/shared/c++/ImageCropperColorScheme.hpp +76 -0
  33. package/package.json +1 -1
  34. package/src/ReactNativeImageCropPicker.nitro.ts +29 -13
@@ -0,0 +1,636 @@
1
+ package com.margelo.nitro.reactnativeimagecroppicker
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.annotation.SuppressLint
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.ActivityInfo
10
+ import android.content.res.ColorStateList
11
+ import android.graphics.Bitmap
12
+ import android.graphics.Color
13
+ import android.graphics.Typeface
14
+ import android.graphics.drawable.ColorDrawable
15
+ import android.graphics.drawable.Drawable
16
+ import android.graphics.drawable.GradientDrawable
17
+ import android.graphics.drawable.StateListDrawable
18
+ import android.net.Uri
19
+ import android.os.Build
20
+ import android.os.Bundle
21
+ import android.text.TextUtils
22
+ import android.util.TypedValue
23
+ import android.view.Gravity
24
+ import android.view.MotionEvent
25
+ import android.view.View
26
+ import android.view.ViewGroup
27
+ import android.view.accessibility.AccessibilityNodeInfo
28
+ import android.view.animation.DecelerateInterpolator
29
+ import android.widget.Button
30
+ import android.widget.FrameLayout
31
+ import android.widget.ImageView
32
+ import android.widget.LinearLayout
33
+ import android.widget.ProgressBar
34
+ import android.widget.TextView
35
+ import androidx.activity.OnBackPressedCallback
36
+ import androidx.activity.SystemBarStyle
37
+ import androidx.activity.enableEdgeToEdge
38
+ import androidx.appcompat.app.AppCompatActivity
39
+ import androidx.core.view.ViewCompat
40
+ import androidx.core.view.WindowInsetsCompat
41
+ import com.facebook.react.common.assets.ReactFontManager
42
+ import com.yalantis.ucrop.callback.BitmapCropCallback
43
+ import com.yalantis.ucrop.view.CropImageView
44
+ import com.yalantis.ucrop.view.GestureCropImageView
45
+ import com.yalantis.ucrop.view.OverlayView
46
+ import com.yalantis.ucrop.view.TransformImageView
47
+ import com.yalantis.ucrop.view.UCropView
48
+ import kotlin.math.roundToInt
49
+
50
+ // Full-screen cropper laid out like a OneKey page: a header with the title and
51
+ // a rotate button, the crop area, and a Cancel / Confirm footer. It draws the
52
+ // same screen, with the same metrics, as the iOS ImageCropperViewController.
53
+ class ImageCropperActivity : AppCompatActivity() {
54
+ private lateinit var cropperTheme: ImageCropperTheme
55
+ private lateinit var cropView: UCropView
56
+ private lateinit var cropImageView: GestureCropImageView
57
+ private lateinit var overlayView: OverlayView
58
+ private lateinit var rotateButton: IconButton
59
+ private lateinit var confirmButton: CapsuleButton
60
+
61
+ private var showsGrid = false
62
+ private var gridAlpha = 0f
63
+ private var gridAnimator: ValueAnimator? = null
64
+ private var rotationAnimator: ValueAnimator? = null
65
+ private var isImageLoaded = false
66
+ private var isProcessing = false
67
+ private val hideGrid = Runnable { animateGrid(visible = false) }
68
+
69
+ override fun onCreate(savedInstanceState: Bundle?) {
70
+ super.onCreate(savedInstanceState)
71
+ cropperTheme = ImageCropperTheme.readFrom(intent)
72
+ val barStyle = if (cropperTheme.isDark) {
73
+ SystemBarStyle.dark(Color.TRANSPARENT)
74
+ } else {
75
+ SystemBarStyle.light(Color.TRANSPARENT, DARK_SCRIM)
76
+ }
77
+ enableEdgeToEdge(barStyle, barStyle)
78
+ requestedOrientation = intent.getIntExtra(
79
+ EXTRA_REQUESTED_ORIENTATION,
80
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED,
81
+ )
82
+
83
+ val inputUri = intent.uriExtra(EXTRA_INPUT_URI)
84
+ val outputUri = intent.uriExtra(EXTRA_OUTPUT_URI)
85
+ if (inputUri == null || outputUri == null) {
86
+ finishWithError("Missing image")
87
+ return
88
+ }
89
+
90
+ window.setBackgroundDrawable(ColorDrawable(cropperTheme.backgroundColor))
91
+ setContentView(buildContent())
92
+ configureCropView()
93
+
94
+ onBackPressedDispatcher.addCallback(
95
+ this,
96
+ object : OnBackPressedCallback(true) {
97
+ override fun handleOnBackPressed() = cancel()
98
+ },
99
+ )
100
+
101
+ cropView.alpha = 0f
102
+ cropImageView.setTransformImageListener(
103
+ object : TransformImageView.TransformImageListener {
104
+ override fun onLoadComplete() {
105
+ isImageLoaded = true
106
+ cropView.animate().alpha(1f).setDuration(FADE_IN_DURATION).start()
107
+ }
108
+
109
+ override fun onLoadFailure(error: Exception) {
110
+ finishWithError(error.message ?: "Cannot load image")
111
+ }
112
+
113
+ override fun onRotate(currentAngle: Float) = Unit
114
+
115
+ override fun onScale(currentScale: Float) = Unit
116
+ },
117
+ )
118
+ try {
119
+ cropImageView.setImageUri(inputUri, outputUri)
120
+ } catch (error: Exception) {
121
+ finishWithError(error.message ?: "Cannot load image")
122
+ }
123
+ }
124
+
125
+ override fun onStop() {
126
+ super.onStop()
127
+ if (::cropImageView.isInitialized) {
128
+ cropImageView.cancelAllAnimations()
129
+ }
130
+ }
131
+
132
+ override fun onDestroy() {
133
+ super.onDestroy()
134
+ gridAnimator?.cancel()
135
+ rotationAnimator?.cancel()
136
+ if (::cropView.isInitialized) {
137
+ cropView.removeCallbacks(hideGrid)
138
+ }
139
+ }
140
+
141
+ private fun buildContent(): View {
142
+ val theme = cropperTheme
143
+ val spacing = dp(20f)
144
+ val headerHeight = dp(56f)
145
+ val iconButtonSize = dp(40f)
146
+ val buttonHeight = dp(50f)
147
+
148
+ val column = LinearLayout(this).apply {
149
+ orientation = LinearLayout.VERTICAL
150
+ setBackgroundColor(theme.backgroundColor)
151
+ }
152
+
153
+ val header = FrameLayout(this)
154
+ val title = intent.getStringExtra(EXTRA_TITLE)
155
+ if (!title.isNullOrEmpty()) {
156
+ val titleView = TextView(this).apply {
157
+ text = title
158
+ setTextColor(theme.titleColor)
159
+ setTextSize(TypedValue.COMPLEX_UNIT_DIP, (18 * theme.scale).roundToInt().toFloat())
160
+ typeface = loadTypeface(theme.titleFontFamily, fallbackWeight = 600)
161
+ gravity = Gravity.CENTER
162
+ maxLines = 1
163
+ ellipsize = TextUtils.TruncateAt.END
164
+ includeFontPadding = false
165
+ ViewCompat.setAccessibilityHeading(this, true)
166
+ }
167
+ // Centered, clear of the rotate button on both sides.
168
+ header.addView(
169
+ titleView,
170
+ FrameLayout.LayoutParams(
171
+ ViewGroup.LayoutParams.MATCH_PARENT,
172
+ ViewGroup.LayoutParams.MATCH_PARENT,
173
+ ).apply {
174
+ marginStart = spacing + iconButtonSize
175
+ marginEnd = spacing + iconButtonSize
176
+ },
177
+ )
178
+ }
179
+ rotateButton = IconButton(
180
+ this,
181
+ iconSize = dp(24f),
182
+ iconColor = theme.iconColor,
183
+ pressedColor = theme.cancelButtonColor,
184
+ ).apply {
185
+ contentDescription = "Rotate"
186
+ visibility = if (intent.getBooleanExtra(EXTRA_ROTATE_BUTTON_HIDDEN, false)) {
187
+ View.GONE
188
+ } else {
189
+ View.VISIBLE
190
+ }
191
+ setOnClickListener { rotate() }
192
+ }
193
+ header.addView(
194
+ rotateButton,
195
+ FrameLayout.LayoutParams(iconButtonSize, iconButtonSize, Gravity.END or Gravity.CENTER_VERTICAL)
196
+ .apply { marginEnd = spacing - dp(8f) },
197
+ )
198
+ column.addView(
199
+ header,
200
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, headerHeight),
201
+ )
202
+
203
+ cropView = UCropView(this, null)
204
+ cropImageView = cropView.cropImageView
205
+ overlayView = cropView.overlayView
206
+ // The crop box keeps `spacing` from every edge of the crop area.
207
+ cropImageView.setPadding(spacing, spacing, spacing, spacing)
208
+ overlayView.setPadding(spacing, spacing, spacing, spacing)
209
+ val cropContainer = CropContainer(this).apply { addView(cropView) }
210
+ column.addView(
211
+ cropContainer,
212
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f),
213
+ )
214
+
215
+ val footer = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
216
+ val buttonPadding = dp(20f) + dp(1f)
217
+ val buttonTextSize = (16 * theme.scale).roundToInt().toFloat()
218
+ val buttonTypeface = loadTypeface(theme.buttonFontFamily, fallbackWeight = 500)
219
+ val cancelButton = CapsuleButton(
220
+ this,
221
+ title = intent.getStringExtra(EXTRA_CANCEL_TEXT)?.takeIf { it.isNotEmpty() }
222
+ ?: getString(android.R.string.cancel),
223
+ textSize = buttonTextSize,
224
+ typeface = buttonTypeface,
225
+ textColor = theme.cancelButtonTextColor,
226
+ backgroundColor = theme.cancelButtonColor,
227
+ pressedColor = theme.cancelButtonPressedColor,
228
+ horizontalPadding = buttonPadding,
229
+ spinnerSize = dp(20f),
230
+ spinnerSpacing = dp(8f),
231
+ ).apply { setOnClickListener { cancel() } }
232
+ confirmButton = CapsuleButton(
233
+ this,
234
+ title = intent.getStringExtra(EXTRA_CONFIRM_TEXT)?.takeIf { it.isNotEmpty() }
235
+ ?: getString(android.R.string.ok),
236
+ textSize = buttonTextSize,
237
+ typeface = buttonTypeface,
238
+ textColor = theme.confirmButtonTextColor,
239
+ backgroundColor = theme.confirmButtonColor,
240
+ pressedColor = theme.confirmButtonPressedColor,
241
+ horizontalPadding = buttonPadding,
242
+ spinnerSize = dp(20f),
243
+ spinnerSpacing = dp(8f),
244
+ ).apply { setOnClickListener { confirm() } }
245
+ footer.addView(cancelButton, LinearLayout.LayoutParams(0, buttonHeight, 1f))
246
+ footer.addView(
247
+ confirmButton,
248
+ LinearLayout.LayoutParams(0, buttonHeight, 1f).apply { marginStart = dp(10f) },
249
+ )
250
+ column.addView(
251
+ footer,
252
+ LinearLayout.LayoutParams(
253
+ ViewGroup.LayoutParams.MATCH_PARENT,
254
+ ViewGroup.LayoutParams.WRAP_CONTENT,
255
+ ),
256
+ )
257
+
258
+ val navigationBarReduction = (10 * resources.displayMetrics.density).roundToInt()
259
+ ViewCompat.setOnApplyWindowInsetsListener(column) { _, windowInsets ->
260
+ val insets = windowInsets.getInsets(
261
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
262
+ )
263
+ column.setPadding(insets.left, 0, insets.right, 0)
264
+ header.setPadding(0, insets.top, 0, 0)
265
+ header.layoutParams = header.layoutParams.apply { height = headerHeight + insets.top }
266
+ // Same as OneKey's page footer: 20dp of padding, plus the navigation bar
267
+ // inset less 10dp.
268
+ val bottomInset = if (insets.bottom > navigationBarReduction) {
269
+ insets.bottom - navigationBarReduction
270
+ } else {
271
+ insets.bottom
272
+ }
273
+ footer.setPadding(spacing, 0, spacing, spacing + bottomInset)
274
+ WindowInsetsCompat.CONSUMED
275
+ }
276
+ return column
277
+ }
278
+
279
+ private fun configureCropView() {
280
+ val isCircular = intent.getBooleanExtra(EXTRA_CIRCLE_OVERLAY, false)
281
+ val isFreeStyle = intent.getBooleanExtra(EXTRA_FREE_STYLE, false) && !isCircular
282
+ val aspectRatioX = intent.getFloatExtra(EXTRA_ASPECT_RATIO_X, 0f)
283
+ val aspectRatioY = intent.getFloatExtra(EXTRA_ASPECT_RATIO_Y, 0f)
284
+
285
+ cropImageView.isRotateEnabled = false
286
+ cropImageView.isScaleEnabled = true
287
+ cropImageView.targetAspectRatio = when {
288
+ isCircular -> 1f
289
+ aspectRatioX > 0f && aspectRatioY > 0f -> aspectRatioX / aspectRatioY
290
+ else -> CropImageView.SOURCE_IMAGE_ASPECT_RATIO
291
+ }
292
+
293
+ val density = resources.displayMetrics.density
294
+ overlayView.freestyleCropMode = if (isFreeStyle) {
295
+ OverlayView.FREESTYLE_CROP_MODE_ENABLE
296
+ } else {
297
+ OverlayView.FREESTYLE_CROP_MODE_DISABLE
298
+ }
299
+ overlayView.setCircleDimmedLayer(isCircular)
300
+ overlayView.setDimmedColor(withAlpha(cropperTheme.backgroundColor, DIMMED_ALPHA))
301
+ overlayView.setShowCropFrame(!isCircular)
302
+ overlayView.setCropFrameColor(cropperTheme.titleColor)
303
+ overlayView.setCropGridCornerColor(cropperTheme.titleColor)
304
+ overlayView.setCropFrameStrokeWidth(density.roundToInt().coerceAtLeast(1))
305
+ // A hairline grid that only shows while the image is moved, like iOS.
306
+ showsGrid = intent.getBooleanExtra(EXTRA_SHOW_GRID, true) && !isCircular
307
+ overlayView.setShowCropGrid(showsGrid)
308
+ overlayView.setCropGridStrokeWidth(1)
309
+ overlayView.setCropGridColor(withAlpha(GRID_COLOR, 0f))
310
+ }
311
+
312
+ private fun onCropTouchStart() {
313
+ cropView.removeCallbacks(hideGrid)
314
+ animateGrid(visible = true)
315
+ }
316
+
317
+ private fun onCropTouchEnd() {
318
+ cropView.removeCallbacks(hideGrid)
319
+ cropView.postDelayed(hideGrid, GRID_HIDE_DELAY)
320
+ }
321
+
322
+ private fun animateGrid(visible: Boolean) {
323
+ if (!showsGrid) {
324
+ return
325
+ }
326
+ val target = if (visible) 1f else 0f
327
+ gridAnimator?.cancel()
328
+ if (gridAlpha == target) {
329
+ return
330
+ }
331
+ gridAnimator = ValueAnimator.ofFloat(gridAlpha, target).apply {
332
+ duration = if (visible) GRID_FADE_IN_DURATION else GRID_FADE_OUT_DURATION
333
+ addUpdateListener { animator ->
334
+ gridAlpha = animator.animatedValue as Float
335
+ overlayView.setCropGridColor(withAlpha(GRID_COLOR, gridAlpha))
336
+ overlayView.invalidate()
337
+ }
338
+ start()
339
+ }
340
+ }
341
+
342
+ private fun rotate() {
343
+ if (!isImageLoaded || isProcessing || rotationAnimator != null) {
344
+ return
345
+ }
346
+ cropImageView.cancelAllAnimations()
347
+ var appliedAngle = 0f
348
+ rotationAnimator = ValueAnimator.ofFloat(0f, -90f).apply {
349
+ duration = ROTATION_DURATION
350
+ interpolator = DecelerateInterpolator()
351
+ addUpdateListener { animator ->
352
+ val angle = animator.animatedValue as Float
353
+ cropImageView.postRotate(angle - appliedAngle)
354
+ appliedAngle = angle
355
+ }
356
+ addListener(
357
+ object : AnimatorListenerAdapter() {
358
+ override fun onAnimationEnd(animation: Animator) {
359
+ rotationAnimator = null
360
+ cropImageView.setImageToWrapCropBounds()
361
+ }
362
+ },
363
+ )
364
+ start()
365
+ }
366
+ }
367
+
368
+ private fun cancel() {
369
+ if (isProcessing) {
370
+ return
371
+ }
372
+ setResult(RESULT_CANCELED)
373
+ finish()
374
+ }
375
+
376
+ private fun confirm() {
377
+ if (!isImageLoaded || isProcessing || rotationAnimator != null) {
378
+ return
379
+ }
380
+ isProcessing = true
381
+ confirmButton.isLoading = true
382
+ rotateButton.alpha = DISABLED_ALPHA
383
+ cropImageView.cropAndSaveImage(
384
+ Bitmap.CompressFormat.JPEG,
385
+ 100,
386
+ object : BitmapCropCallback {
387
+ override fun onBitmapCropped(
388
+ resultUri: Uri,
389
+ offsetX: Int,
390
+ offsetY: Int,
391
+ imageWidth: Int,
392
+ imageHeight: Int,
393
+ ) {
394
+ setResult(
395
+ RESULT_OK,
396
+ Intent()
397
+ .putExtra(EXTRA_OUTPUT_URI, resultUri)
398
+ .putExtra(EXTRA_OUTPUT_OFFSET_X, offsetX)
399
+ .putExtra(EXTRA_OUTPUT_OFFSET_Y, offsetY)
400
+ .putExtra(EXTRA_OUTPUT_WIDTH, imageWidth)
401
+ .putExtra(EXTRA_OUTPUT_HEIGHT, imageHeight),
402
+ )
403
+ finish()
404
+ }
405
+
406
+ override fun onCropFailure(error: Throwable) {
407
+ finishWithError(error.message ?: "Cannot crop image")
408
+ }
409
+ },
410
+ )
411
+ }
412
+
413
+ private fun finishWithError(message: String) {
414
+ setResult(RESULT_ERROR, Intent().putExtra(EXTRA_ERROR_MESSAGE, message))
415
+ finish()
416
+ }
417
+
418
+ private fun dp(value: Float): Int =
419
+ (value * cropperTheme.scale * resources.displayMetrics.density).roundToInt()
420
+
421
+ private fun loadTypeface(family: String?, fallbackWeight: Int): Typeface {
422
+ if (family != null) {
423
+ return ReactFontManager.getInstance().getTypeface(family, Typeface.NORMAL, assets)
424
+ }
425
+ return when {
426
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ->
427
+ Typeface.create(Typeface.DEFAULT, fallbackWeight, false)
428
+ fallbackWeight >= 600 -> Typeface.DEFAULT_BOLD
429
+ else -> Typeface.create("sans-serif-medium", Typeface.NORMAL)
430
+ }
431
+ }
432
+
433
+ // Shows the grid while the image is being moved, and blocks gestures while
434
+ // the image is cropped.
435
+ @SuppressLint("ViewConstructor")
436
+ private inner class CropContainer(context: Context) : FrameLayout(context) {
437
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
438
+ when (event.actionMasked) {
439
+ MotionEvent.ACTION_DOWN -> onCropTouchStart()
440
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> onCropTouchEnd()
441
+ }
442
+ return super.dispatchTouchEvent(event)
443
+ }
444
+
445
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
446
+ isProcessing || rotationAnimator != null || super.onInterceptTouchEvent(event)
447
+
448
+ @SuppressLint("ClickableViewAccessibility")
449
+ override fun onTouchEvent(event: MotionEvent): Boolean =
450
+ isProcessing || rotationAnimator != null || super.onTouchEvent(event)
451
+ }
452
+
453
+ // OneKey's large Button: a capsule with a 16dp medium label, a pressed
454
+ // color, and a spinner next to the label while loading.
455
+ @SuppressLint("ViewConstructor")
456
+ private class CapsuleButton(
457
+ context: Context,
458
+ title: String,
459
+ textSize: Float,
460
+ typeface: Typeface,
461
+ textColor: Int,
462
+ backgroundColor: Int,
463
+ pressedColor: Int,
464
+ horizontalPadding: Int,
465
+ spinnerSize: Int,
466
+ spinnerSpacing: Int,
467
+ ) : LinearLayout(context) {
468
+ private val spinner = ProgressBar(context).apply {
469
+ isIndeterminate = true
470
+ indeterminateTintList = ColorStateList.valueOf(textColor)
471
+ visibility = View.GONE
472
+ }
473
+
474
+ var isLoading = false
475
+ set(value) {
476
+ field = value
477
+ spinner.visibility = if (value) View.VISIBLE else View.GONE
478
+ // OneKey dims disabled and loading buttons alike.
479
+ alpha = if (value) DISABLED_ALPHA else 1f
480
+ }
481
+
482
+ init {
483
+ orientation = HORIZONTAL
484
+ gravity = Gravity.CENTER
485
+ setPadding(horizontalPadding, 0, horizontalPadding, 0)
486
+ background = pressable(capsule(backgroundColor), capsule(pressedColor))
487
+ contentDescription = title
488
+ addView(
489
+ spinner,
490
+ LayoutParams(spinnerSize, spinnerSize).apply { marginEnd = spinnerSpacing },
491
+ )
492
+ addView(
493
+ TextView(context).apply {
494
+ text = title
495
+ setTextColor(textColor)
496
+ setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
497
+ this.typeface = typeface
498
+ maxLines = 1
499
+ ellipsize = TextUtils.TruncateAt.END
500
+ includeFontPadding = false
501
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
502
+ },
503
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
504
+ )
505
+ }
506
+
507
+ override fun getAccessibilityClassName(): CharSequence = Button::class.java.name
508
+
509
+ override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
510
+ super.onInitializeAccessibilityNodeInfo(info)
511
+ info.isEnabled = !isLoading
512
+ }
513
+ }
514
+
515
+ // OneKey's header icon button: a 24dp icon with a round pressed background.
516
+ @SuppressLint("ViewConstructor")
517
+ private class IconButton(
518
+ context: Context,
519
+ iconSize: Int,
520
+ iconColor: Int,
521
+ pressedColor: Int,
522
+ ) : FrameLayout(context) {
523
+ init {
524
+ background = pressable(
525
+ ColorDrawable(Color.TRANSPARENT),
526
+ GradientDrawable().apply {
527
+ shape = GradientDrawable.OVAL
528
+ setColor(pressedColor)
529
+ },
530
+ )
531
+ addView(
532
+ ImageView(context).apply {
533
+ setImageResource(R.drawable.image_crop_picker_rotate)
534
+ imageTintList = ColorStateList.valueOf(iconColor)
535
+ scaleType = ImageView.ScaleType.FIT_CENTER
536
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
537
+ },
538
+ LayoutParams(iconSize, iconSize, Gravity.CENTER),
539
+ )
540
+ }
541
+
542
+ override fun getAccessibilityClassName(): CharSequence = Button::class.java.name
543
+ }
544
+
545
+ companion object {
546
+ private const val PREFIX = "com.margelo.nitro.reactnativeimagecroppicker.cropper."
547
+ private const val EXTRA_INPUT_URI = "${PREFIX}inputUri"
548
+ private const val EXTRA_ASPECT_RATIO_X = "${PREFIX}aspectRatioX"
549
+ private const val EXTRA_ASPECT_RATIO_Y = "${PREFIX}aspectRatioY"
550
+ private const val EXTRA_FREE_STYLE = "${PREFIX}freeStyle"
551
+ private const val EXTRA_CIRCLE_OVERLAY = "${PREFIX}circleOverlay"
552
+ private const val EXTRA_SHOW_GRID = "${PREFIX}showGrid"
553
+ private const val EXTRA_ROTATE_BUTTON_HIDDEN = "${PREFIX}rotateButtonHidden"
554
+ private const val EXTRA_TITLE = "${PREFIX}title"
555
+ private const val EXTRA_CANCEL_TEXT = "${PREFIX}cancelText"
556
+ private const val EXTRA_CONFIRM_TEXT = "${PREFIX}confirmText"
557
+ private const val EXTRA_REQUESTED_ORIENTATION = "${PREFIX}requestedOrientation"
558
+
559
+ const val EXTRA_OUTPUT_URI = "${PREFIX}outputUri"
560
+ const val EXTRA_OUTPUT_OFFSET_X = "${PREFIX}outputOffsetX"
561
+ const val EXTRA_OUTPUT_OFFSET_Y = "${PREFIX}outputOffsetY"
562
+ const val EXTRA_OUTPUT_WIDTH = "${PREFIX}outputWidth"
563
+ const val EXTRA_OUTPUT_HEIGHT = "${PREFIX}outputHeight"
564
+ const val EXTRA_ERROR_MESSAGE = "${PREFIX}errorMessage"
565
+ const val RESULT_ERROR = RESULT_FIRST_USER + 1
566
+
567
+ // The area outside the crop box shows the page background at this opacity.
568
+ private const val DIMMED_ALPHA = 0.7f
569
+ private const val DISABLED_ALPHA = 0.4f
570
+ private const val GRID_COLOR = Color.WHITE
571
+ private const val GRID_HIDE_DELAY = 800L
572
+ private const val GRID_FADE_IN_DURATION = 200L
573
+ private const val GRID_FADE_OUT_DURATION = 350L
574
+ private const val ROTATION_DURATION = 250L
575
+ private const val FADE_IN_DURATION = 300L
576
+ private val DARK_SCRIM = Color.argb(0x80, 0x1b, 0x1b, 0x1b)
577
+
578
+ internal fun createIntent(
579
+ context: Context,
580
+ source: Uri,
581
+ destination: Uri,
582
+ config: ImageCropPickerConfig,
583
+ theme: ImageCropperTheme,
584
+ requestedOrientation: Int,
585
+ ): Intent {
586
+ val intent = Intent(context, ImageCropperActivity::class.java)
587
+ .putExtra(EXTRA_INPUT_URI, source)
588
+ .putExtra(EXTRA_OUTPUT_URI, destination)
589
+ .putExtra(EXTRA_FREE_STYLE, config.freeStyleCropEnabled)
590
+ .putExtra(EXTRA_CIRCLE_OVERLAY, config.cropperCircleOverlay)
591
+ .putExtra(EXTRA_SHOW_GRID, config.showCropGuidelines)
592
+ .putExtra(EXTRA_ROTATE_BUTTON_HIDDEN, config.cropperRotateButtonsHidden)
593
+ .putExtra(EXTRA_TITLE, config.cropperToolbarTitle)
594
+ .putExtra(EXTRA_CANCEL_TEXT, config.cropperCancelText)
595
+ .putExtra(EXTRA_CONFIRM_TEXT, config.cropperChooseText)
596
+ .putExtra(EXTRA_REQUESTED_ORIENTATION, requestedOrientation)
597
+ if (config.width != null && config.height != null) {
598
+ intent.putExtra(EXTRA_ASPECT_RATIO_X, config.width.toFloat())
599
+ .putExtra(EXTRA_ASPECT_RATIO_Y, config.height.toFloat())
600
+ }
601
+ theme.writeTo(intent)
602
+ return intent
603
+ }
604
+
605
+ internal fun getOutputUri(data: Intent): Uri? = data.uriExtra(EXTRA_OUTPUT_URI)
606
+
607
+ private fun Intent.uriExtra(name: String): Uri? =
608
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
609
+ getParcelableExtra(name, Uri::class.java)
610
+ } else {
611
+ @Suppress("DEPRECATION")
612
+ getParcelableExtra(name)
613
+ }
614
+
615
+ private fun withAlpha(color: Int, alpha: Float): Int =
616
+ Color.argb(
617
+ (Color.alpha(color) * alpha).roundToInt(),
618
+ Color.red(color),
619
+ Color.green(color),
620
+ Color.blue(color),
621
+ )
622
+
623
+ private fun capsule(color: Int): Drawable =
624
+ GradientDrawable().apply {
625
+ // Clamped to half the height, which makes a capsule.
626
+ cornerRadius = 10_000f
627
+ setColor(color)
628
+ }
629
+
630
+ private fun pressable(normal: Drawable, pressed: Drawable): Drawable =
631
+ StateListDrawable().apply {
632
+ addState(intArrayOf(android.R.attr.state_pressed), pressed)
633
+ addState(intArrayOf(), normal)
634
+ }
635
+ }
636
+ }