react-native-customizable-image-crop-picker 1.0.1

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 (176) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +479 -0
  3. package/RNCustomizableImageCropPicker.podspec +26 -0
  4. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  5. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  10. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  11. package/android/.gradle/vcs-1/gc.properties +0 -0
  12. package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/results.bin +1 -0
  13. package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/transformed/classes/classes_dex/classes.dex +0 -0
  14. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/results.bin +1 -0
  15. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/BuildConfig.dex +0 -0
  16. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.dex +0 -0
  17. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity.dex +0 -0
  18. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.dex +0 -0
  19. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule.dex +0 -0
  20. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperPackage.dex +0 -0
  21. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.dex +0 -0
  22. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
  23. package/android/build/generated/source/buildConfig/debug/com/rncustomizableimagecroppicker/BuildConfig.java +10 -0
  24. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +26 -0
  25. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
  26. package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
  27. package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
  28. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  29. package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
  30. package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +1 -0
  31. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_nativeimagepicker_file_paths.xml.flat +0 -0
  32. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -0
  33. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  34. package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
  35. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  36. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  37. package/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  38. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
  39. package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +3 -0
  40. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -0
  41. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +26 -0
  42. package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
  43. package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
  44. package/android/build/intermediates/packaged_res/debug/packageDebugResources/xml/nativeimagepicker_file_paths.xml +6 -0
  45. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  46. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
  47. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
  48. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
  49. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
  50. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
  51. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
  52. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
  53. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  54. package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +2 -0
  55. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  56. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream +0 -0
  57. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len +0 -0
  58. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len +0 -0
  59. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  60. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i +0 -0
  61. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len +0 -0
  62. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  63. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  64. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  65. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  66. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  67. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  68. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len +0 -0
  69. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  70. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  71. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  72. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  73. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  74. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  75. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len +0 -0
  76. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab +0 -0
  77. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream +0 -0
  78. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len +0 -0
  79. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len +0 -0
  80. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values +0 -0
  81. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at +0 -0
  82. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s +1 -0
  83. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i +0 -0
  84. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len +0 -0
  85. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  86. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  87. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  88. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  89. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  90. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  91. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len +0 -0
  92. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  93. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  94. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  95. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  96. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values +0 -0
  97. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  98. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s +1 -0
  99. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  100. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len +0 -0
  101. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  102. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream +0 -0
  103. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len +0 -0
  104. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len +0 -0
  105. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  106. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i +0 -0
  107. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len +0 -0
  108. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
  109. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream +0 -0
  110. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len +0 -0
  111. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len +0 -0
  112. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
  113. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i +0 -0
  114. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len +0 -0
  115. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
  116. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream +0 -0
  117. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len +0 -0
  118. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len +0 -0
  119. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
  120. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i +0 -0
  121. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len +0 -0
  122. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +2 -0
  123. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
  124. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream +0 -0
  125. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len +0 -0
  126. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len +0 -0
  127. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
  128. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i +0 -0
  129. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len +0 -0
  130. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
  131. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
  132. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
  133. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
  134. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
  135. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i +0 -0
  136. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len +0 -0
  137. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  138. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  139. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  140. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  141. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values +0 -0
  142. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  143. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.s +1 -0
  144. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  145. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len +0 -0
  146. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  147. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  148. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  149. package/android/build/outputs/logs/manifest-merger-debug-report.txt +46 -0
  150. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  151. package/android/build/tmp/kotlin-classes/debug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  152. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
  153. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
  154. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
  155. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
  156. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
  157. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
  158. package/android/build.gradle +49 -0
  159. package/android/consumer-rules.pro +3 -0
  160. package/android/src/main/AndroidManifest.xml +23 -0
  161. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperActivity.kt +1009 -0
  162. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperModule.kt +684 -0
  163. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperPackage.kt +17 -0
  164. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.kt +6 -0
  165. package/android/src/main/res/xml/nativeimagepicker_file_paths.xml +6 -0
  166. package/ios/NativeImageCropperModule.m +13 -0
  167. package/ios/NativeImageCropperModule.swift +1400 -0
  168. package/package.json +116 -0
  169. package/react-native.config.js +13 -0
  170. package/src/api.ts +41 -0
  171. package/src/errors.ts +39 -0
  172. package/src/index.ts +4 -0
  173. package/src/native/NativeImageCropperModule.ts +34 -0
  174. package/src/native/mapOptions.ts +164 -0
  175. package/src/types.ts +258 -0
  176. package/src/ui/ImageCropPickerModal.tsx +322 -0
@@ -0,0 +1,1009 @@
1
+ package com.rncustomizableimagecroppicker
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Typeface
8
+ import android.graphics.drawable.GradientDrawable
9
+ import android.net.Uri
10
+ import android.os.Build
11
+ import android.os.Bundle
12
+ import android.util.Base64
13
+ import android.util.LruCache
14
+ import android.util.TypedValue
15
+ import android.view.Gravity
16
+ import android.view.View
17
+ import android.view.ViewGroup
18
+ import android.view.WindowManager
19
+ import android.widget.FrameLayout
20
+ import android.widget.ImageView
21
+ import android.widget.LinearLayout
22
+ import android.widget.TextView
23
+ import androidx.appcompat.widget.Toolbar
24
+ import androidx.core.content.res.ResourcesCompat
25
+ import androidx.core.view.ViewCompat
26
+ import androidx.core.view.WindowCompat
27
+ import androidx.core.view.WindowInsetsCompat
28
+ import androidx.core.view.WindowInsetsControllerCompat
29
+ import com.caverock.androidsvg.SVG
30
+ import com.yalantis.ucrop.UCropActivity
31
+ import com.yalantis.ucrop.view.OverlayView
32
+ import java.io.BufferedInputStream
33
+ import java.io.InputStream
34
+ import java.net.HttpURLConnection
35
+ import java.net.URL
36
+ import java.net.URLDecoder
37
+ import java.nio.charset.StandardCharsets
38
+
39
+ class NativeImageCropperActivity : UCropActivity() {
40
+
41
+ companion object {
42
+ const val EXTRA_HEADER_TITLE = "native_cropper_header_title"
43
+ const val EXTRA_CANCEL_TEXT = "native_cropper_cancel_text"
44
+ const val EXTRA_UPLOAD_TEXT = "native_cropper_upload_text"
45
+ const val EXTRA_CANCEL_COLOR = "native_cropper_cancel_color"
46
+ const val EXTRA_UPLOAD_COLOR = "native_cropper_upload_color"
47
+ const val EXTRA_STATUS_BAR_COLOR = "native_cropper_status_bar_color"
48
+ const val EXTRA_IS_DARK_THEME = "native_cropper_is_dark_theme"
49
+ const val EXTRA_DRAW_UNDER_STATUS_BAR = "native_cropper_draw_under_status_bar"
50
+ const val EXTRA_STATUS_BAR_STYLE = "native_cropper_status_bar_style"
51
+ const val EXTRA_HEADER_BACKGROUND_COLOR = "native_cropper_header_background_color"
52
+ const val EXTRA_HEADER_TITLE_COLOR = "native_cropper_header_title_color"
53
+ const val EXTRA_HEADER_TITLE_FONT_SIZE = "native_cropper_header_title_font_size"
54
+ const val EXTRA_HEADER_TITLE_FONT_FAMILY = "native_cropper_header_title_font_family"
55
+ const val EXTRA_HEADER_HEIGHT = "native_cropper_header_height"
56
+ const val EXTRA_HEADER_PADDING_HORIZONTAL = "native_cropper_header_padding_horizontal"
57
+ const val EXTRA_HEADER_PADDING_TOP = "native_cropper_header_padding_top"
58
+ const val EXTRA_HEADER_PADDING_BOTTOM = "native_cropper_header_padding_bottom"
59
+ const val EXTRA_HEADER_ALIGNMENT = "native_cropper_header_alignment"
60
+ const val EXTRA_BOTTOM_BACKGROUND_COLOR = "native_cropper_bottom_background_color"
61
+ const val EXTRA_BOTTOM_PADDING_HORIZONTAL = "native_cropper_bottom_padding_horizontal"
62
+ const val EXTRA_BOTTOM_PADDING_TOP = "native_cropper_bottom_padding_top"
63
+ const val EXTRA_BOTTOM_PADDING_BOTTOM = "native_cropper_bottom_padding_bottom"
64
+ const val EXTRA_BOTTOM_BUTTON_GAP = "native_cropper_bottom_button_gap"
65
+ const val EXTRA_BOTTOM_BUTTON_HEIGHT = "native_cropper_bottom_button_height"
66
+ const val EXTRA_BOTTOM_BUTTON_LAYOUT = "native_cropper_bottom_button_layout"
67
+ const val EXTRA_CONTROLS_PLACEMENT = "native_cropper_controls_placement"
68
+ const val EXTRA_TOP_LEFT_CONTROL = "native_cropper_top_left_control"
69
+ const val EXTRA_TOP_RIGHT_CONTROL = "native_cropper_top_right_control"
70
+ const val EXTRA_FOOTER_BUTTON_ORDER = "native_cropper_footer_button_order"
71
+
72
+ const val EXTRA_CANCEL_BUTTON_CONTENT = "native_cropper_cancel_button_content"
73
+ const val EXTRA_CANCEL_BUTTON_ICON_URI = "native_cropper_cancel_button_icon_uri"
74
+ const val EXTRA_CANCEL_BUTTON_ICON_BASE64 = "native_cropper_cancel_button_icon_base64"
75
+ const val EXTRA_CANCEL_BUTTON_ICON_TINT = "native_cropper_cancel_button_icon_tint"
76
+ const val EXTRA_CANCEL_BUTTON_ICON_SIZE = "native_cropper_cancel_button_icon_size"
77
+ const val EXTRA_CANCEL_BUTTON_ICON_GAP = "native_cropper_cancel_button_icon_gap"
78
+ const val EXTRA_CANCEL_BUTTON_PADDING_HORIZONTAL = "native_cropper_cancel_button_padding_horizontal"
79
+ const val EXTRA_CANCEL_BUTTON_PADDING_VERTICAL = "native_cropper_cancel_button_padding_vertical"
80
+
81
+ const val EXTRA_UPLOAD_BUTTON_CONTENT = "native_cropper_upload_button_content"
82
+ const val EXTRA_UPLOAD_BUTTON_ICON_URI = "native_cropper_upload_button_icon_uri"
83
+ const val EXTRA_UPLOAD_BUTTON_ICON_BASE64 = "native_cropper_upload_button_icon_base64"
84
+ const val EXTRA_UPLOAD_BUTTON_ICON_TINT = "native_cropper_upload_button_icon_tint"
85
+ const val EXTRA_UPLOAD_BUTTON_ICON_SIZE = "native_cropper_upload_button_icon_size"
86
+ const val EXTRA_UPLOAD_BUTTON_ICON_GAP = "native_cropper_upload_button_icon_gap"
87
+ const val EXTRA_UPLOAD_BUTTON_PADDING_HORIZONTAL = "native_cropper_upload_button_padding_horizontal"
88
+ const val EXTRA_UPLOAD_BUTTON_PADDING_VERTICAL = "native_cropper_upload_button_padding_vertical"
89
+ const val EXTRA_CANCEL_BACKGROUND_COLOR = "native_cropper_cancel_background_color"
90
+ const val EXTRA_CANCEL_BORDER_COLOR = "native_cropper_cancel_border_color"
91
+ const val EXTRA_CANCEL_BORDER_WIDTH = "native_cropper_cancel_border_width"
92
+ const val EXTRA_CANCEL_FONT_SIZE = "native_cropper_cancel_font_size"
93
+ const val EXTRA_CANCEL_FONT_FAMILY = "native_cropper_cancel_font_family"
94
+ const val EXTRA_CANCEL_BORDER_RADIUS = "native_cropper_cancel_border_radius"
95
+ const val EXTRA_UPLOAD_TEXT_COLOR = "native_cropper_upload_text_color"
96
+ const val EXTRA_UPLOAD_BORDER_COLOR = "native_cropper_upload_border_color"
97
+ const val EXTRA_UPLOAD_BORDER_WIDTH = "native_cropper_upload_border_width"
98
+ const val EXTRA_UPLOAD_FONT_SIZE = "native_cropper_upload_font_size"
99
+ const val EXTRA_UPLOAD_FONT_FAMILY = "native_cropper_upload_font_family"
100
+ const val EXTRA_UPLOAD_BORDER_RADIUS = "native_cropper_upload_border_radius"
101
+
102
+ const val EXTRA_CROP_GRID_ENABLED = "native_cropper_crop_grid_enabled"
103
+ const val EXTRA_CROP_FRAME_COLOR = "native_cropper_crop_frame_color"
104
+ const val EXTRA_CROP_GRID_COLOR = "native_cropper_crop_grid_color"
105
+
106
+ // Simple in-memory caches for remote icon bitmaps.
107
+ // Key includes URL + target size to avoid re-scaling every time.
108
+ private val remoteBitmapCache = LruCache<String, Bitmap>(64)
109
+ }
110
+
111
+ private var headerHeightDp = 84
112
+ private var bottomInsetDp = 120
113
+
114
+ private var headerBackgroundColor = Color.WHITE
115
+ private var headerTitleColor = Color.parseColor("#2D2D2D")
116
+ private var headerTitleFontSizeSp = 22
117
+ private var headerTitleFontFamily = ""
118
+ private var headerAlignment: String = "left" // "left" | "center" | "right"
119
+ private var headerPaddingHorizontalDp = 20
120
+ private var headerPaddingTopDp = 28
121
+ private var headerPaddingBottomDp = 20
122
+
123
+ private var bottomBackgroundColor = Color.WHITE
124
+ private var bottomPaddingHorizontalDp = 20
125
+ private var bottomPaddingTopDp = 16
126
+ private var bottomPaddingBottomDp = 24
127
+ private var bottomButtonGapDp = 12
128
+ private var bottomButtonHeightDp = 54
129
+ private var bottomButtonLayout: String = "vertical"
130
+ private var controlsPlacement: String = "bottom"
131
+ private var topLeftControl: String = "cancel"
132
+ private var topRightControl: String = "upload"
133
+ private var footerButtonOrder: String = "uploadFirst"
134
+
135
+ private var cancelButtonContent: String = "text"
136
+ private var cancelButtonIconUri: String = ""
137
+ private var cancelButtonIconBase64: String = ""
138
+ private var cancelButtonIconTintColor: String = ""
139
+ private var cancelButtonIconSizeDp: Int = 18
140
+ private var cancelButtonIconGapDp: Int = 8
141
+ private var cancelButtonPaddingHorizontalDp: Int = 12
142
+ private var cancelButtonPaddingVerticalDp: Int = 0
143
+
144
+ private var uploadButtonContent: String = "text"
145
+ private var uploadButtonIconUri: String = ""
146
+ private var uploadButtonIconBase64: String = ""
147
+ private var uploadButtonIconTintColor: String = ""
148
+ private var uploadButtonIconSizeDp: Int = 18
149
+ private var uploadButtonIconGapDp: Int = 8
150
+ private var uploadButtonPaddingHorizontalDp: Int = 12
151
+ private var uploadButtonPaddingVerticalDp: Int = 0
152
+
153
+ // Same layout vibe as demo screen:
154
+ // - Primary: black pill (Upload)
155
+ // - Secondary: light pill (Cancel)
156
+ private var cancelTextColor = Color.parseColor("#111111")
157
+ private var cancelBackgroundColor = Color.parseColor("#EFEFF4")
158
+ private var cancelBorderColor = Color.parseColor("#EFEFF4")
159
+ private var cancelBorderWidthDp = 0
160
+ private var cancelFontSizeSp = 18
161
+ private var cancelFontFamily = ""
162
+ private var cancelCornerRadiusDp = 28
163
+
164
+ private var uploadTextColor = Color.WHITE
165
+ private var uploadBackgroundColor = Color.parseColor("#111111")
166
+ private var uploadBorderColor = Color.parseColor("#111111")
167
+ private var uploadBorderWidthDp = 0
168
+ private var uploadFontSizeSp = 18
169
+ private var uploadFontFamily = ""
170
+ private var uploadCornerRadiusDp = 28
171
+
172
+ private var isDarkTheme = false
173
+ private var isCropActionInProgress = false
174
+ private var drawUnderStatusBar = false
175
+ private var statusBarStyle: String = "auto" // "dark" | "light" | "auto"
176
+ private var statusBarColorString: String = "#FFFFFF"
177
+
178
+ private var cropGridEnabled: Boolean = false
179
+ private var cropFrameColorString: String = ""
180
+ private var cropGridColorString: String = ""
181
+
182
+ private var headerView: FrameLayout? = null
183
+ private var bottomActionsView: LinearLayout? = null
184
+ private var baseBottomActionsPaddingBottomPx: Int? = null
185
+
186
+ private var baseCropTopMarginPx: Int? = null
187
+ private var baseCropBottomMarginPx: Int? = null
188
+
189
+ override fun onCreate(savedInstanceState: Bundle?) {
190
+ super.onCreate(savedInstanceState)
191
+ overridePendingTransition(0, 0)
192
+
193
+ isDarkTheme = intent.getBooleanExtra(EXTRA_IS_DARK_THEME, false)
194
+ drawUnderStatusBar = intent.getBooleanExtra(EXTRA_DRAW_UNDER_STATUS_BAR, false)
195
+ statusBarStyle = intent.getStringExtra(EXTRA_STATUS_BAR_STYLE).orEmpty().ifBlank { "auto" }
196
+ statusBarColorString = intent.getStringExtra(EXTRA_STATUS_BAR_COLOR).orEmpty().ifBlank { "#FFFFFF" }
197
+ // We always go edge-to-edge and apply insets ourselves.
198
+ // This is the most reliable behavior across Android versions (esp. Android 15+).
199
+ WindowCompat.setDecorFitsSystemWindows(window, false)
200
+ readStyleExtras()
201
+ applyStatusBarColor()
202
+
203
+ findViewById<Toolbar>(com.yalantis.ucrop.R.id.toolbar)?.visibility = View.GONE
204
+
205
+ val root = findViewById<FrameLayout>(android.R.id.content) ?: return
206
+ enforceOverlayStyle()
207
+ addHeader(root)
208
+ if (controlsPlacement != "top") {
209
+ addBottomActions(root)
210
+ }
211
+ setupInsetsHandling(root)
212
+ }
213
+
214
+ override fun onResume() {
215
+ super.onResume()
216
+ // uCrop / system can re-apply UI flags on resume; re-assert our status bar settings.
217
+ applyStatusBarColor()
218
+ enforceOverlayStyle()
219
+ }
220
+
221
+ override fun finish() {
222
+ super.finish()
223
+ overridePendingTransition(0, 0)
224
+ }
225
+
226
+ private fun enforceOverlayStyle() {
227
+ val overlay = findViewById<OverlayView>(com.yalantis.ucrop.R.id.view_overlay) ?: return
228
+ overlay.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
229
+ overlay.setShowCropGrid(cropGridEnabled)
230
+ overlay.setShowCropFrame(true)
231
+ if (cropGridColorString.isNotBlank()) {
232
+ overlay.setCropGridColor(parseColor(cropGridColorString, Color.WHITE))
233
+ }
234
+ if (cropFrameColorString.isNotBlank()) {
235
+ overlay.setCropFrameColor(parseColor(cropFrameColorString, Color.WHITE))
236
+ }
237
+ overlay.invalidate()
238
+ }
239
+
240
+ private fun applyCropInsets(statusBarTopInsetPx: Int) {
241
+ val cropFrame = findViewById<View>(com.yalantis.ucrop.R.id.ucrop_frame) ?: return
242
+ val params = cropFrame.layoutParams as? ViewGroup.MarginLayoutParams ?: return
243
+ if (baseCropTopMarginPx == null) baseCropTopMarginPx = params.topMargin
244
+ if (baseCropBottomMarginPx == null) baseCropBottomMarginPx = params.bottomMargin
245
+
246
+ params.topMargin = (baseCropTopMarginPx ?: 0) + dp(headerHeightDp) + statusBarTopInsetPx
247
+ params.bottomMargin = (baseCropBottomMarginPx ?: 0) + dp(bottomInsetDp)
248
+ cropFrame.layoutParams = params
249
+ }
250
+
251
+ private fun addHeader(root: FrameLayout) {
252
+ val titleText = intent.getStringExtra(EXTRA_HEADER_TITLE).orEmpty().ifBlank { "Preview Image" }
253
+ val header = FrameLayout(this).apply {
254
+ setBackgroundColor(headerBackgroundColor)
255
+ setPadding(
256
+ dp(headerPaddingHorizontalDp),
257
+ dp(headerPaddingTopDp),
258
+ dp(headerPaddingHorizontalDp),
259
+ dp(headerPaddingBottomDp),
260
+ )
261
+ }
262
+ val headerLp = FrameLayout.LayoutParams(
263
+ ViewGroup.LayoutParams.MATCH_PARENT,
264
+ dp(headerHeightDp),
265
+ ).apply { gravity = Gravity.TOP }
266
+ root.addView(header, headerLp)
267
+ headerView = header
268
+
269
+ if (controlsPlacement == "top") {
270
+ val row = LinearLayout(this).apply {
271
+ orientation = LinearLayout.HORIZONTAL
272
+ gravity = Gravity.CENTER_VERTICAL
273
+ layoutParams = FrameLayout.LayoutParams(
274
+ ViewGroup.LayoutParams.MATCH_PARENT,
275
+ ViewGroup.LayoutParams.MATCH_PARENT,
276
+ )
277
+ }
278
+ header.addView(row)
279
+
280
+ val left = FrameLayout(this)
281
+ val right = FrameLayout(this)
282
+ val title = TextView(this).apply {
283
+ text = titleText
284
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, headerTitleFontSizeSp.toFloat())
285
+ setTextColor(headerTitleColor)
286
+ gravity = Gravity.CENTER
287
+ typeface = getTypefaceByName(headerTitleFontFamily, Typeface.DEFAULT_BOLD)
288
+ }
289
+
290
+ row.addView(left, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
291
+ row.addView(
292
+ title,
293
+ LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply {
294
+ gravity = Gravity.CENTER_VERTICAL
295
+ },
296
+ )
297
+ row.addView(right, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
298
+
299
+ createHeaderControlView(topLeftControl)?.let { v ->
300
+ left.addView(
301
+ v,
302
+ FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER_VERTICAL or Gravity.START),
303
+ )
304
+ }
305
+ createHeaderControlView(topRightControl)?.let { v ->
306
+ right.addView(
307
+ v,
308
+ FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER_VERTICAL or Gravity.END),
309
+ )
310
+ }
311
+ } else {
312
+ val title = TextView(this).apply {
313
+ text = titleText
314
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, headerTitleFontSizeSp.toFloat())
315
+ setTextColor(headerTitleColor)
316
+ gravity = when (headerAlignment) {
317
+ "center" -> Gravity.CENTER
318
+ "right" -> Gravity.END or Gravity.CENTER_VERTICAL
319
+ else -> Gravity.START or Gravity.CENTER_VERTICAL
320
+ }
321
+ typeface = getTypefaceByName(headerTitleFontFamily, Typeface.DEFAULT_BOLD)
322
+ }
323
+ header.addView(
324
+ title,
325
+ FrameLayout.LayoutParams(
326
+ ViewGroup.LayoutParams.MATCH_PARENT,
327
+ ViewGroup.LayoutParams.WRAP_CONTENT,
328
+ Gravity.CENTER_VERTICAL,
329
+ ),
330
+ )
331
+ }
332
+ }
333
+
334
+ private fun createHeaderControlView(which: String): View? {
335
+ val normalized = which.trim()
336
+ if (normalized == "none" || normalized.isEmpty()) return null
337
+ val heightDp = bottomButtonHeightDp.coerceIn(36, 44)
338
+ return when (normalized) {
339
+ "cancel" -> createActionView(
340
+ text = intent.getStringExtra(EXTRA_CANCEL_TEXT).orEmpty().ifBlank { "Cancel" },
341
+ content = cancelButtonContent,
342
+ iconUri = cancelButtonIconUri,
343
+ iconBase64 = cancelButtonIconBase64,
344
+ iconTintColor = cancelButtonIconTintColor,
345
+ iconSizeDp = cancelButtonIconSizeDp,
346
+ iconGapDp = cancelButtonIconGapDp,
347
+ contentPaddingHorizontalDp = cancelButtonPaddingHorizontalDp,
348
+ contentPaddingVerticalDp = cancelButtonPaddingVerticalDp,
349
+ textColor = cancelTextColor,
350
+ fillColor = cancelBackgroundColor,
351
+ strokeColor = cancelBorderColor,
352
+ borderWidthDp = cancelBorderWidthDp,
353
+ fontSizeSp = cancelFontSizeSp,
354
+ fontFamily = cancelFontFamily,
355
+ cornerRadiusDp = cancelCornerRadiusDp,
356
+ fixedHeightDp = heightDp,
357
+ ).apply {
358
+ setOnClickListener { onBackPressedDispatcher.onBackPressed() }
359
+ }
360
+ "upload" -> createActionView(
361
+ text = intent.getStringExtra(EXTRA_UPLOAD_TEXT).orEmpty().ifBlank { "Upload" },
362
+ content = uploadButtonContent,
363
+ iconUri = uploadButtonIconUri,
364
+ iconBase64 = uploadButtonIconBase64,
365
+ iconTintColor = uploadButtonIconTintColor,
366
+ iconSizeDp = uploadButtonIconSizeDp,
367
+ iconGapDp = uploadButtonIconGapDp,
368
+ contentPaddingHorizontalDp = uploadButtonPaddingHorizontalDp,
369
+ contentPaddingVerticalDp = uploadButtonPaddingVerticalDp,
370
+ textColor = uploadTextColor,
371
+ fillColor = uploadBackgroundColor,
372
+ strokeColor = uploadBorderColor,
373
+ borderWidthDp = uploadBorderWidthDp,
374
+ fontSizeSp = uploadFontSizeSp,
375
+ fontFamily = uploadFontFamily,
376
+ cornerRadiusDp = uploadCornerRadiusDp,
377
+ fixedHeightDp = heightDp,
378
+ ).apply {
379
+ setOnClickListener {
380
+ if (isCropActionInProgress || isFinishing || isDestroyed) return@setOnClickListener
381
+ isCropActionInProgress = true
382
+ isEnabled = false
383
+ triggerCropAction()
384
+ }
385
+ }
386
+ else -> null
387
+ }
388
+ }
389
+
390
+ private fun addBottomActions(root: FrameLayout) {
391
+ val cancelText = intent.getStringExtra(EXTRA_CANCEL_TEXT).orEmpty().ifBlank { "Cancel" }
392
+ val uploadText = intent.getStringExtra(EXTRA_UPLOAD_TEXT).orEmpty().ifBlank { "Upload" }
393
+
394
+ val wrapper = LinearLayout(this).apply {
395
+ orientation = LinearLayout.VERTICAL
396
+ setBackgroundColor(bottomBackgroundColor)
397
+ setPadding(
398
+ dp(bottomPaddingHorizontalDp),
399
+ dp(bottomPaddingTopDp),
400
+ dp(bottomPaddingHorizontalDp),
401
+ dp(bottomPaddingBottomDp),
402
+ )
403
+ }
404
+ val wrapperLp = FrameLayout.LayoutParams(
405
+ ViewGroup.LayoutParams.MATCH_PARENT,
406
+ ViewGroup.LayoutParams.WRAP_CONTENT,
407
+ ).apply { gravity = Gravity.BOTTOM }
408
+ root.addView(wrapper, wrapperLp)
409
+ bottomActionsView = wrapper
410
+ baseBottomActionsPaddingBottomPx = wrapper.paddingBottom
411
+
412
+ val cancelButton = createActionView(
413
+ text = cancelText,
414
+ content = cancelButtonContent,
415
+ iconUri = cancelButtonIconUri,
416
+ iconBase64 = cancelButtonIconBase64,
417
+ iconTintColor = cancelButtonIconTintColor,
418
+ iconSizeDp = cancelButtonIconSizeDp,
419
+ iconGapDp = cancelButtonIconGapDp,
420
+ contentPaddingHorizontalDp = cancelButtonPaddingHorizontalDp,
421
+ contentPaddingVerticalDp = cancelButtonPaddingVerticalDp,
422
+ textColor = cancelTextColor,
423
+ fillColor = cancelBackgroundColor,
424
+ strokeColor = cancelBorderColor,
425
+ borderWidthDp = cancelBorderWidthDp,
426
+ fontSizeSp = cancelFontSizeSp,
427
+ fontFamily = cancelFontFamily,
428
+ cornerRadiusDp = cancelCornerRadiusDp,
429
+ fixedHeightDp = bottomButtonHeightDp,
430
+ )
431
+ val uploadButton = createActionView(
432
+ text = uploadText,
433
+ content = uploadButtonContent,
434
+ iconUri = uploadButtonIconUri,
435
+ iconBase64 = uploadButtonIconBase64,
436
+ iconTintColor = uploadButtonIconTintColor,
437
+ iconSizeDp = uploadButtonIconSizeDp,
438
+ iconGapDp = uploadButtonIconGapDp,
439
+ contentPaddingHorizontalDp = uploadButtonPaddingHorizontalDp,
440
+ contentPaddingVerticalDp = uploadButtonPaddingVerticalDp,
441
+ textColor = uploadTextColor,
442
+ fillColor = uploadBackgroundColor,
443
+ strokeColor = uploadBorderColor,
444
+ borderWidthDp = uploadBorderWidthDp,
445
+ fontSizeSp = uploadFontSizeSp,
446
+ fontFamily = uploadFontFamily,
447
+ cornerRadiusDp = uploadCornerRadiusDp,
448
+ fixedHeightDp = bottomButtonHeightDp,
449
+ )
450
+
451
+ val firstIsCancel = footerButtonOrder == "cancelFirst"
452
+ val first = if (firstIsCancel) cancelButton else uploadButton
453
+ val second = if (firstIsCancel) uploadButton else cancelButton
454
+
455
+ if (bottomButtonLayout == "horizontal") {
456
+ val row = LinearLayout(this).apply {
457
+ orientation = LinearLayout.HORIZONTAL
458
+ gravity = Gravity.CENTER_VERTICAL
459
+ }
460
+ wrapper.addView(
461
+ row,
462
+ LinearLayout.LayoutParams(
463
+ ViewGroup.LayoutParams.MATCH_PARENT,
464
+ ViewGroup.LayoutParams.WRAP_CONTENT,
465
+ ),
466
+ )
467
+
468
+ // Horizontal: order is configurable.
469
+ row.addView(first, LinearLayout.LayoutParams(0, dp(bottomButtonHeightDp), 1f))
470
+ row.addView(
471
+ second,
472
+ LinearLayout.LayoutParams(0, dp(bottomButtonHeightDp), 1f).apply {
473
+ marginStart = dp(bottomButtonGapDp)
474
+ },
475
+ )
476
+ } else {
477
+ // Vertical: order is configurable.
478
+ wrapper.addView(
479
+ first,
480
+ LinearLayout.LayoutParams(
481
+ ViewGroup.LayoutParams.MATCH_PARENT,
482
+ dp(bottomButtonHeightDp),
483
+ ),
484
+ )
485
+ wrapper.addView(
486
+ second,
487
+ LinearLayout.LayoutParams(
488
+ ViewGroup.LayoutParams.MATCH_PARENT,
489
+ dp(bottomButtonHeightDp),
490
+ ).apply { topMargin = dp(bottomButtonGapDp) },
491
+ )
492
+ }
493
+
494
+ cancelButton.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
495
+ uploadButton.setOnClickListener {
496
+ if (isCropActionInProgress || isFinishing || isDestroyed) return@setOnClickListener
497
+ isCropActionInProgress = true
498
+ uploadButton.isEnabled = false
499
+ triggerCropAction()
500
+ }
501
+ }
502
+
503
+ private fun createActionView(
504
+ text: String,
505
+ content: String,
506
+ iconUri: String,
507
+ iconBase64: String,
508
+ iconTintColor: String,
509
+ iconSizeDp: Int,
510
+ iconGapDp: Int,
511
+ contentPaddingHorizontalDp: Int,
512
+ contentPaddingVerticalDp: Int,
513
+ textColor: Int,
514
+ fillColor: Int,
515
+ strokeColor: Int,
516
+ borderWidthDp: Int,
517
+ fontSizeSp: Int,
518
+ fontFamily: String,
519
+ cornerRadiusDp: Int,
520
+ fixedHeightDp: Int,
521
+ ): View {
522
+ val normalizedContent = content.trim()
523
+ val resolvedContent = when (normalizedContent) {
524
+ "icon", "text" -> normalizedContent
525
+ "iconText", "icon+text" -> "iconText"
526
+ "textIcon", "TextIcon", "text+icon" -> "textIcon"
527
+ else -> "text"
528
+ }
529
+ val hasText = resolvedContent != "icon"
530
+ val wantsIcon = resolvedContent != "text"
531
+
532
+ val container = LinearLayout(this).apply {
533
+ orientation = LinearLayout.HORIZONTAL
534
+ gravity = Gravity.CENTER
535
+ isClickable = true
536
+ isFocusable = true
537
+ setPadding(
538
+ dp(contentPaddingHorizontalDp.coerceAtLeast(0)),
539
+ dp(contentPaddingVerticalDp.coerceAtLeast(0)),
540
+ dp(contentPaddingHorizontalDp.coerceAtLeast(0)),
541
+ dp(contentPaddingVerticalDp.coerceAtLeast(0)),
542
+ )
543
+ minimumHeight = dp(fixedHeightDp.coerceAtLeast(36))
544
+ background = GradientDrawable().apply {
545
+ setColor(fillColor)
546
+ cornerRadius = dp(cornerRadiusDp.coerceAtLeast(0)).toFloat()
547
+ setStroke(dp(borderWidthDp.coerceAtLeast(0)), strokeColor)
548
+ }
549
+ }
550
+
551
+ val labelView = TextView(this).apply {
552
+ this.text = text
553
+ gravity = Gravity.CENTER
554
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp.toFloat())
555
+ setTextColor(textColor)
556
+ typeface = getTypefaceByName(fontFamily, Typeface.DEFAULT_BOLD)
557
+ }
558
+
559
+ val iconView = ImageView(this).apply {
560
+ scaleType = ImageView.ScaleType.FIT_CENTER
561
+ if (iconTintColor.isNotBlank()) {
562
+ setColorFilter(parseColor(iconTintColor, textColor))
563
+ } else {
564
+ clearColorFilter()
565
+ }
566
+ val size = dp(iconSizeDp.coerceIn(12, 64))
567
+ layoutParams = LinearLayout.LayoutParams(size, size)
568
+ }
569
+
570
+ val iconInputsPresent = iconUri.trim().isNotEmpty() || iconBase64.trim().isNotEmpty()
571
+ val shouldRenderIconSlot = wantsIcon && iconInputsPresent
572
+ if (shouldRenderIconSlot) {
573
+ setIconImageAsync(iconView, iconUri, iconBase64)
574
+ }
575
+
576
+ val gap = dp(iconGapDp.coerceIn(0, 48))
577
+ fun addIcon() {
578
+ if (!shouldRenderIconSlot) return
579
+ container.addView(iconView, iconView.layoutParams)
580
+ }
581
+ fun addText(withStartGap: Boolean) {
582
+ if (!hasText) return
583
+ val lp = LinearLayout.LayoutParams(
584
+ ViewGroup.LayoutParams.WRAP_CONTENT,
585
+ ViewGroup.LayoutParams.WRAP_CONTENT,
586
+ ).apply {
587
+ if (container.childCount > 0 && withStartGap && gap > 0) {
588
+ marginStart = gap
589
+ }
590
+ }
591
+ container.addView(labelView, lp)
592
+ }
593
+
594
+ when (resolvedContent) {
595
+ "icon" -> addIcon()
596
+ "text" -> addText(withStartGap = false)
597
+ "textIcon" -> {
598
+ addText(withStartGap = false)
599
+ if (shouldRenderIconSlot) {
600
+ val lp = LinearLayout.LayoutParams(iconView.layoutParams as LinearLayout.LayoutParams).apply {
601
+ marginStart = gap
602
+ }
603
+ container.addView(iconView, lp)
604
+ }
605
+ }
606
+ else /* iconText */ -> {
607
+ addIcon()
608
+ addText(withStartGap = true)
609
+ }
610
+ }
611
+
612
+ if (container.childCount == 0) {
613
+ container.addView(labelView)
614
+ }
615
+ return container
616
+ }
617
+
618
+ private fun setIconImageAsync(target: ImageView, iconUri: String, iconBase64: String) {
619
+ val targetW = (target.layoutParams?.width ?: 0).coerceAtLeast(1)
620
+ val targetH = (target.layoutParams?.height ?: 0).coerceAtLeast(1)
621
+
622
+ val base64 = iconBase64.trim()
623
+ if (base64.isNotEmpty()) {
624
+ val clean = base64.substringAfter("base64,", base64)
625
+ val bitmap = runCatching {
626
+ val bytes = Base64.decode(clean, Base64.DEFAULT)
627
+ decodeSvgBytesToBitmap(bytes, targetW, targetH)
628
+ ?: BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
629
+ }.getOrNull()
630
+ if (bitmap != null) target.setImageBitmap(bitmap)
631
+ return
632
+ }
633
+
634
+ val uriString = iconUri.trim()
635
+ if (uriString.isEmpty()) return
636
+
637
+ if (uriString.startsWith("data:image/svg+xml", ignoreCase = true)) {
638
+ val svgBitmap = decodeSvgDataUriToBitmap(uriString, targetW, targetH)
639
+ if (svgBitmap != null) target.setImageBitmap(svgBitmap)
640
+ return
641
+ }
642
+
643
+ val scheme = runCatching { Uri.parse(uriString).scheme.orEmpty() }.getOrDefault("")
644
+ if (scheme == "http" || scheme == "https") {
645
+ val cacheKey = "$uriString|$targetW|$targetH"
646
+ val cached = remoteBitmapCache.get(cacheKey)
647
+ if (cached != null) {
648
+ target.setImageBitmap(cached)
649
+ return
650
+ }
651
+ // Load remote icons asynchronously (best-effort).
652
+ Thread {
653
+ val bmp = if (looksLikeSvgUri(uriString)) {
654
+ downloadSvgBitmap(uriString, targetW, targetH)
655
+ } else {
656
+ downloadBitmap(uriString)
657
+ }
658
+ if (bmp != null && !isFinishing && !isDestroyed) {
659
+ remoteBitmapCache.put(cacheKey, bmp)
660
+ runOnUiThread { target.setImageBitmap(bmp) }
661
+ }
662
+ }.start()
663
+ return
664
+ }
665
+
666
+ val bitmap = runCatching {
667
+ val uri = Uri.parse(uriString)
668
+ when (uri.scheme) {
669
+ "content", "file" -> {
670
+ contentResolver.openInputStream(uri)?.use { stream ->
671
+ if (looksLikeSvgUri(uriString)) {
672
+ decodeSvgStreamToBitmap(stream, targetW, targetH)
673
+ } else {
674
+ BitmapFactory.decodeStream(stream)
675
+ }
676
+ }
677
+ }
678
+ else -> {
679
+ if (!uriString.startsWith("/")) return@runCatching null
680
+ if (looksLikeSvgUri(uriString)) {
681
+ contentResolver.openInputStream(Uri.fromFile(java.io.File(uriString)))?.use { stream ->
682
+ decodeSvgStreamToBitmap(stream, targetW, targetH)
683
+ }
684
+ } else {
685
+ BitmapFactory.decodeFile(uriString)
686
+ }
687
+ }
688
+ }
689
+ }.getOrNull()
690
+ if (bitmap != null) target.setImageBitmap(bitmap)
691
+ }
692
+
693
+ private fun looksLikeSvgUri(uri: String): Boolean {
694
+ val s = uri.trim().lowercase()
695
+ return s.endsWith(".svg") || s.contains(".svg?") || s.contains(".svg#")
696
+ }
697
+
698
+ private fun decodeSvgBytesToBitmap(bytes: ByteArray, w: Int, h: Int): Bitmap? {
699
+ val text = runCatching { bytes.toString(Charsets.UTF_8) }.getOrNull() ?: return null
700
+ if (!text.contains("<svg", ignoreCase = true)) return null
701
+ val svg = runCatching { SVG.getFromString(text) }.getOrNull() ?: return null
702
+ return renderSvgToBitmap(svg, w, h)
703
+ }
704
+
705
+ private fun decodeSvgStreamToBitmap(stream: InputStream, w: Int, h: Int): Bitmap? {
706
+ val svg = runCatching { SVG.getFromInputStream(stream) }.getOrNull() ?: return null
707
+ return renderSvgToBitmap(svg, w, h)
708
+ }
709
+
710
+ private fun decodeSvgDataUriToBitmap(dataUri: String, w: Int, h: Int): Bitmap? {
711
+ val commaIdx = dataUri.indexOf(',')
712
+ if (commaIdx < 0) return null
713
+ val meta = dataUri.substring(0, commaIdx)
714
+ val data = dataUri.substring(commaIdx + 1)
715
+ val isBase64 = meta.contains(";base64", ignoreCase = true)
716
+ val svgText = if (isBase64) {
717
+ runCatching {
718
+ val bytes = Base64.decode(data, Base64.DEFAULT)
719
+ bytes.toString(Charsets.UTF_8)
720
+ }.getOrNull()
721
+ } else {
722
+ runCatching { URLDecoder.decode(data, StandardCharsets.UTF_8.name()) }.getOrNull()
723
+ } ?: return null
724
+
725
+ val svg = runCatching { SVG.getFromString(svgText) }.getOrNull() ?: return null
726
+ return renderSvgToBitmap(svg, w, h)
727
+ }
728
+
729
+ private fun renderSvgToBitmap(svg: SVG, w: Int, h: Int): Bitmap? {
730
+ val picture = runCatching { svg.renderToPicture() }.getOrNull() ?: return null
731
+ val pictureW =
732
+ (picture.width.takeIf { it > 0 }?.toFloat()
733
+ ?: svg.documentViewBox?.width()
734
+ ?: w.toFloat())
735
+ .coerceAtLeast(1f)
736
+ val pictureH =
737
+ (picture.height.takeIf { it > 0 }?.toFloat()
738
+ ?: svg.documentViewBox?.height()
739
+ ?: h.toFloat())
740
+ .coerceAtLeast(1f)
741
+
742
+ val scale = kotlin.math.min(w / pictureW, h / pictureH).coerceAtLeast(0.01f)
743
+ val dx = (w - (pictureW * scale)) / 2f
744
+ val dy = (h - (pictureH * scale)) / 2f
745
+
746
+ val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
747
+ val canvas = Canvas(bitmap)
748
+ canvas.translate(dx, dy)
749
+ canvas.scale(scale, scale)
750
+ canvas.drawPicture(picture)
751
+ return bitmap
752
+ }
753
+
754
+ private fun downloadSvgBitmap(urlString: String, w: Int, h: Int): Bitmap? {
755
+ return runCatching {
756
+ val url = URL(urlString)
757
+ val connection = (url.openConnection() as HttpURLConnection).apply {
758
+ connectTimeout = 5000
759
+ readTimeout = 7000
760
+ instanceFollowRedirects = true
761
+ }
762
+ connection.connect()
763
+ if (connection.responseCode !in 200..299) return@runCatching null
764
+ connection.inputStream.use { input ->
765
+ BufferedInputStream(input).use { bis ->
766
+ decodeSvgStreamToBitmap(bis, w, h)
767
+ }
768
+ }
769
+ }.getOrNull()
770
+ }
771
+
772
+ private fun downloadBitmap(urlString: String): android.graphics.Bitmap? {
773
+ return runCatching {
774
+ val url = URL(urlString)
775
+ val connection = (url.openConnection() as HttpURLConnection).apply {
776
+ connectTimeout = 5000
777
+ readTimeout = 7000
778
+ instanceFollowRedirects = true
779
+ }
780
+ connection.connect()
781
+ if (connection.responseCode !in 200..299) return@runCatching null
782
+ connection.inputStream.use { input ->
783
+ BufferedInputStream(input).use { bis ->
784
+ BitmapFactory.decodeStream(bis)
785
+ }
786
+ }
787
+ }.getOrNull()
788
+ }
789
+
790
+ private fun triggerCropAction() {
791
+ val toolbar = findViewById<Toolbar>(com.yalantis.ucrop.R.id.toolbar) ?: return
792
+ val menuItem = toolbar.menu?.findItem(com.yalantis.ucrop.R.id.menu_crop) ?: return
793
+ onOptionsItemSelected(menuItem)
794
+ }
795
+
796
+ private fun parseColor(color: String, fallback: Int): Int {
797
+ val normalized = normalizeColorString(color)
798
+ return runCatching { Color.parseColor(normalized) }.getOrElse { fallback }
799
+ }
800
+
801
+ private fun normalizeColorString(raw: String): String {
802
+ val s = raw.trim()
803
+ if (!s.startsWith("#")) return s
804
+ // Expand shorthand hex colors:
805
+ // - #RGB -> #RRGGBB
806
+ // - #ARGB -> #AARRGGBB
807
+ return when (s.length) {
808
+ 4 -> {
809
+ val r = s[1]
810
+ val g = s[2]
811
+ val b = s[3]
812
+ "#$r$r$g$g$b$b"
813
+ }
814
+ 5 -> {
815
+ val a = s[1]
816
+ val r = s[2]
817
+ val g = s[3]
818
+ val b = s[4]
819
+ "#$a$a$r$r$g$g$b$b"
820
+ }
821
+ else -> s
822
+ }
823
+ }
824
+
825
+ private fun applyStatusBarColor() {
826
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
827
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
828
+ window.statusBarColor = parseColor(statusBarColorString, Color.WHITE)
829
+ if (Build.VERSION.SDK_INT >= 29) {
830
+ // Prevent the system from applying a contrast scrim that can "wash out" custom colors.
831
+ window.isStatusBarContrastEnforced = false
832
+ }
833
+ val insetsController = WindowCompat.getInsetsController(window, window.decorView)
834
+ insetsController.isAppearanceLightStatusBars = when (statusBarStyle) {
835
+ "dark" -> true
836
+ "light" -> false
837
+ else -> !isDarkTheme
838
+ }
839
+ insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
840
+ }
841
+
842
+ private fun readStyleExtras() {
843
+ // Read placement early because it affects sensible header defaults.
844
+ controlsPlacement = intent.getStringExtra(EXTRA_CONTROLS_PLACEMENT).orEmpty().ifBlank { "bottom" }
845
+ topLeftControl = intent.getStringExtra(EXTRA_TOP_LEFT_CONTROL).orEmpty().ifBlank { "cancel" }
846
+ topRightControl = intent.getStringExtra(EXTRA_TOP_RIGHT_CONTROL).orEmpty().ifBlank { "upload" }
847
+
848
+ headerBackgroundColor = parseColor(
849
+ intent.getStringExtra(EXTRA_HEADER_BACKGROUND_COLOR).orEmpty().ifBlank { if (isDarkTheme) "#0B0B0F" else "#FFFFFF" },
850
+ if (isDarkTheme) Color.parseColor("#0B0B0F") else Color.WHITE,
851
+ )
852
+ headerTitleColor = parseColor(
853
+ intent.getStringExtra(EXTRA_HEADER_TITLE_COLOR).orEmpty().ifBlank { if (isDarkTheme) "#F5F5F7" else "#111111" },
854
+ if (isDarkTheme) Color.parseColor("#F5F5F7") else Color.parseColor("#111111"),
855
+ )
856
+ headerTitleFontSizeSp = intent.getIntExtra(EXTRA_HEADER_TITLE_FONT_SIZE, 22).coerceIn(10, 48)
857
+ headerTitleFontFamily = intent.getStringExtra(EXTRA_HEADER_TITLE_FONT_FAMILY).orEmpty()
858
+ headerAlignment = intent.getStringExtra(EXTRA_HEADER_ALIGNMENT).orEmpty().ifBlank { "left" }.let { raw ->
859
+ if (raw == "center" || raw == "right" || raw == "left") raw else "left"
860
+ }
861
+ headerHeightDp = intent.getIntExtra(EXTRA_HEADER_HEIGHT, 84).coerceIn(48, 240)
862
+ headerPaddingHorizontalDp = intent.getIntExtra(EXTRA_HEADER_PADDING_HORIZONTAL, 20).coerceAtLeast(0)
863
+ val defaultHeaderPaddingTop = if (controlsPlacement == "top") 0 else 28
864
+ headerPaddingTopDp = intent.getIntExtra(EXTRA_HEADER_PADDING_TOP, defaultHeaderPaddingTop).coerceAtLeast(0)
865
+ headerPaddingBottomDp = intent.getIntExtra(EXTRA_HEADER_PADDING_BOTTOM, 20).coerceAtLeast(0)
866
+
867
+ bottomBackgroundColor = parseColor(
868
+ intent.getStringExtra(EXTRA_BOTTOM_BACKGROUND_COLOR).orEmpty().ifBlank { if (isDarkTheme) "#121212" else "#FFFFFF" },
869
+ if (isDarkTheme) Color.parseColor("#121212") else Color.WHITE,
870
+ )
871
+ bottomPaddingHorizontalDp = intent.getIntExtra(EXTRA_BOTTOM_PADDING_HORIZONTAL, 20).coerceAtLeast(0)
872
+ bottomPaddingTopDp = intent.getIntExtra(EXTRA_BOTTOM_PADDING_TOP, 16).coerceAtLeast(0)
873
+ bottomPaddingBottomDp = intent.getIntExtra(EXTRA_BOTTOM_PADDING_BOTTOM, 24).coerceAtLeast(0)
874
+ bottomButtonGapDp = intent.getIntExtra(EXTRA_BOTTOM_BUTTON_GAP, 12).coerceAtLeast(0)
875
+ bottomButtonHeightDp = intent.getIntExtra(EXTRA_BOTTOM_BUTTON_HEIGHT, 54).coerceAtLeast(1)
876
+ bottomButtonLayout = intent.getStringExtra(EXTRA_BOTTOM_BUTTON_LAYOUT).orEmpty().ifBlank { "vertical" }
877
+ footerButtonOrder = intent.getStringExtra(EXTRA_FOOTER_BUTTON_ORDER).orEmpty().ifBlank { "uploadFirst" }
878
+
879
+ cancelButtonContent = intent.getStringExtra(EXTRA_CANCEL_BUTTON_CONTENT).orEmpty().ifBlank { "text" }
880
+ cancelButtonIconUri = intent.getStringExtra(EXTRA_CANCEL_BUTTON_ICON_URI).orEmpty()
881
+ cancelButtonIconBase64 = intent.getStringExtra(EXTRA_CANCEL_BUTTON_ICON_BASE64).orEmpty()
882
+ cancelButtonIconTintColor = intent.getStringExtra(EXTRA_CANCEL_BUTTON_ICON_TINT).orEmpty()
883
+ cancelButtonIconSizeDp = intent.getIntExtra(EXTRA_CANCEL_BUTTON_ICON_SIZE, 18).coerceIn(12, 64)
884
+ cancelButtonIconGapDp = intent.getIntExtra(EXTRA_CANCEL_BUTTON_ICON_GAP, 8).coerceIn(0, 48)
885
+ cancelButtonPaddingHorizontalDp = intent.getIntExtra(EXTRA_CANCEL_BUTTON_PADDING_HORIZONTAL, 12).coerceAtLeast(0)
886
+ cancelButtonPaddingVerticalDp = intent.getIntExtra(EXTRA_CANCEL_BUTTON_PADDING_VERTICAL, 0).coerceAtLeast(0)
887
+
888
+ uploadButtonContent = intent.getStringExtra(EXTRA_UPLOAD_BUTTON_CONTENT).orEmpty().ifBlank { "text" }
889
+ uploadButtonIconUri = intent.getStringExtra(EXTRA_UPLOAD_BUTTON_ICON_URI).orEmpty()
890
+ uploadButtonIconBase64 = intent.getStringExtra(EXTRA_UPLOAD_BUTTON_ICON_BASE64).orEmpty()
891
+ uploadButtonIconTintColor = intent.getStringExtra(EXTRA_UPLOAD_BUTTON_ICON_TINT).orEmpty()
892
+ uploadButtonIconSizeDp = intent.getIntExtra(EXTRA_UPLOAD_BUTTON_ICON_SIZE, 18).coerceIn(12, 64)
893
+ uploadButtonIconGapDp = intent.getIntExtra(EXTRA_UPLOAD_BUTTON_ICON_GAP, 8).coerceIn(0, 48)
894
+ uploadButtonPaddingHorizontalDp = intent.getIntExtra(EXTRA_UPLOAD_BUTTON_PADDING_HORIZONTAL, 12).coerceAtLeast(0)
895
+ uploadButtonPaddingVerticalDp = intent.getIntExtra(EXTRA_UPLOAD_BUTTON_PADDING_VERTICAL, 0).coerceAtLeast(0)
896
+
897
+ bottomInsetDp = if (controlsPlacement == "top") {
898
+ 0
899
+ } else if (bottomButtonLayout == "horizontal") {
900
+ (bottomPaddingTopDp + bottomPaddingBottomDp + bottomButtonHeightDp).coerceAtLeast(100)
901
+ } else {
902
+ (bottomPaddingTopDp + bottomPaddingBottomDp + bottomButtonGapDp + (bottomButtonHeightDp * 2))
903
+ .coerceAtLeast(120)
904
+ }
905
+
906
+ cancelTextColor = parseColor(intent.getStringExtra(EXTRA_CANCEL_COLOR).orEmpty().ifBlank { "#111111" }, Color.parseColor("#111111"))
907
+ cancelBackgroundColor = parseColor(intent.getStringExtra(EXTRA_CANCEL_BACKGROUND_COLOR).orEmpty().ifBlank { "#EFEFF4" }, Color.parseColor("#EFEFF4"))
908
+ cancelBorderColor = parseColor(intent.getStringExtra(EXTRA_CANCEL_BORDER_COLOR).orEmpty().ifBlank { "#EFEFF4" }, Color.parseColor("#EFEFF4"))
909
+ cancelBorderWidthDp = intent.getIntExtra(EXTRA_CANCEL_BORDER_WIDTH, 0).coerceAtLeast(0)
910
+ cancelFontSizeSp = intent.getIntExtra(EXTRA_CANCEL_FONT_SIZE, 18).coerceIn(10, 40)
911
+ cancelFontFamily = intent.getStringExtra(EXTRA_CANCEL_FONT_FAMILY).orEmpty()
912
+ cancelCornerRadiusDp = intent.getIntExtra(EXTRA_CANCEL_BORDER_RADIUS, 28).coerceAtLeast(0)
913
+
914
+ uploadTextColor = parseColor(intent.getStringExtra(EXTRA_UPLOAD_TEXT_COLOR).orEmpty().ifBlank { "#FFFFFF" }, Color.WHITE)
915
+ uploadBackgroundColor = parseColor(intent.getStringExtra(EXTRA_UPLOAD_COLOR).orEmpty().ifBlank { "#111111" }, Color.parseColor("#111111"))
916
+ uploadBorderColor = parseColor(intent.getStringExtra(EXTRA_UPLOAD_BORDER_COLOR).orEmpty().ifBlank { "#111111" }, Color.parseColor("#111111"))
917
+ uploadBorderWidthDp = intent.getIntExtra(EXTRA_UPLOAD_BORDER_WIDTH, 0).coerceAtLeast(0)
918
+ uploadFontSizeSp = intent.getIntExtra(EXTRA_UPLOAD_FONT_SIZE, 18).coerceIn(10, 40)
919
+ uploadFontFamily = intent.getStringExtra(EXTRA_UPLOAD_FONT_FAMILY).orEmpty()
920
+ uploadCornerRadiusDp = intent.getIntExtra(EXTRA_UPLOAD_BORDER_RADIUS, 28).coerceAtLeast(0)
921
+
922
+ cropGridEnabled = intent.getBooleanExtra(EXTRA_CROP_GRID_ENABLED, false)
923
+ cropFrameColorString = intent.getStringExtra(EXTRA_CROP_FRAME_COLOR).orEmpty()
924
+ cropGridColorString = intent.getStringExtra(EXTRA_CROP_GRID_COLOR).orEmpty()
925
+ }
926
+
927
+ private fun getTypefaceByName(fontFamily: String, fallback: Typeface): Typeface {
928
+ val cleaned = fontFamily.trim()
929
+ if (cleaned.isEmpty()) return fallback
930
+ val fontResId = resources.getIdentifier(cleaned, "font", packageName)
931
+ if (fontResId != 0) {
932
+ return ResourcesCompat.getFont(this, fontResId) ?: fallback
933
+ }
934
+ // Fallback to system font lookup by family name (best-effort).
935
+ return Typeface.create(cleaned, fallback.style) ?: fallback
936
+ }
937
+
938
+ private fun dp(value: Int): Int {
939
+ return TypedValue.applyDimension(
940
+ TypedValue.COMPLEX_UNIT_DIP,
941
+ value.toFloat(),
942
+ resources.displayMetrics,
943
+ ).toInt()
944
+ }
945
+
946
+ private fun setupInsetsHandling(root: FrameLayout) {
947
+ val decor = window.decorView
948
+ ViewCompat.setOnApplyWindowInsetsListener(decor) { _, insets ->
949
+ val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
950
+ val topInsetPx = maxOf(sysBars.top, getStatusBarHeightPx())
951
+ applySystemInsets(
952
+ statusBarTopInsetPx = topInsetPx,
953
+ navigationBarBottomInsetPx = sysBars.bottom,
954
+ )
955
+ insets
956
+ }
957
+ ViewCompat.requestApplyInsets(decor)
958
+ }
959
+
960
+ private fun applySystemInsets(
961
+ statusBarTopInsetPx: Int,
962
+ navigationBarBottomInsetPx: Int,
963
+ ) {
964
+ headerView?.let { header ->
965
+ val lp = header.layoutParams as? FrameLayout.LayoutParams
966
+ if (lp != null) {
967
+ val desiredTopMargin = if (drawUnderStatusBar) 0 else statusBarTopInsetPx
968
+ val desiredHeight = if (drawUnderStatusBar) dp(headerHeightDp) + statusBarTopInsetPx else dp(headerHeightDp)
969
+ var changed = false
970
+ if (lp.topMargin != desiredTopMargin) {
971
+ lp.topMargin = desiredTopMargin
972
+ changed = true
973
+ }
974
+ if (lp.height != desiredHeight) {
975
+ lp.height = desiredHeight
976
+ changed = true
977
+ }
978
+ if (changed) header.layoutParams = lp
979
+ }
980
+
981
+ header.setPadding(
982
+ dp(headerPaddingHorizontalDp),
983
+ dp(headerPaddingTopDp) + if (drawUnderStatusBar) statusBarTopInsetPx else 0,
984
+ dp(headerPaddingHorizontalDp),
985
+ dp(headerPaddingBottomDp),
986
+ )
987
+ }
988
+
989
+ bottomActionsView?.let { wrapper ->
990
+ if (baseBottomActionsPaddingBottomPx == null) baseBottomActionsPaddingBottomPx = wrapper.paddingBottom
991
+ wrapper.setPadding(
992
+ wrapper.paddingLeft,
993
+ wrapper.paddingTop,
994
+ wrapper.paddingRight,
995
+ (baseBottomActionsPaddingBottomPx ?: wrapper.paddingBottom) + navigationBarBottomInsetPx,
996
+ )
997
+ }
998
+
999
+ // Crop frame always needs to sit below: status bar + header height.
1000
+ applyCropInsets(statusBarTopInsetPx)
1001
+ }
1002
+
1003
+ private fun getStatusBarHeightPx(): Int {
1004
+ val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
1005
+ if (resourceId <= 0) return 0
1006
+ return resources.getDimensionPixelSize(resourceId).coerceAtLeast(0)
1007
+ }
1008
+ }
1009
+