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.
- package/LICENSE +44 -0
- package/README.md +479 -0
- package/RNCustomizableImageCropPicker.podspec +26 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/results.bin +1 -0
- package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/results.bin +1 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/BuildConfig.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperPackage.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.dex +0 -0
- package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
- package/android/build/generated/source/buildConfig/debug/com/rncustomizableimagecroppicker/BuildConfig.java +10 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +26 -0
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
- package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
- package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
- package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
- package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
- package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +1 -0
- package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_nativeimagepicker_file_paths.xml.flat +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
- package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
- package/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
- package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
- package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +3 -0
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -0
- package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +26 -0
- package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
- package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
- package/android/build/intermediates/packaged_res/debug/packageDebugResources/xml/nativeimagepicker_file_paths.xml +6 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
- package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
- package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +2 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s +1 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s +1 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +2 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.s +1 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len +0 -0
- package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
- package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
- package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +46 -0
- package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
- package/android/build/tmp/kotlin-classes/debug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
- package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
- package/android/build.gradle +49 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +23 -0
- package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperActivity.kt +1009 -0
- package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperModule.kt +684 -0
- package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperPackage.kt +17 -0
- package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.kt +6 -0
- package/android/src/main/res/xml/nativeimagepicker_file_paths.xml +6 -0
- package/ios/NativeImageCropperModule.m +13 -0
- package/ios/NativeImageCropperModule.swift +1400 -0
- package/package.json +116 -0
- package/react-native.config.js +13 -0
- package/src/api.ts +41 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +4 -0
- package/src/native/NativeImageCropperModule.ts +34 -0
- package/src/native/mapOptions.ts +164 -0
- package/src/types.ts +258 -0
- 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
|
+
|