From a4bf4b15167e68a05736c9a8790e793cb4409ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 8 Nov 2024 09:06:09 +0100 Subject: [PATCH 1/6] wip: svg support --- android/build.gradle | 9 ++ .../main/java/com/rcttabview/RCTTabView.kt | 58 ++++--- .../main/java/com/rcttabview/SVGDecoder.kt | 35 ++++ .../com/rcttabview/SVGDrawableTranscoder.kt | 44 ++++++ .../com/rcttabview/TabViewAppGlideModule.kt | 18 +++ .../java/com/rcttabview/TabViewGlideModule.kt | 25 +++ example/assets/setttings.svg | 1 + example/ios/Podfile | 6 +- example/ios/Podfile.lock | 149 ++++++++++++------ example/package.json | 2 +- .../NativeBottomTabsEmbeddedStacks.tsx | 2 +- ios/TabViewProvider.swift | 69 +++++--- react-native-bottom-tabs.podspec | 9 +- yarn.lock | 18 +-- 14 files changed, 347 insertions(+), 98 deletions(-) create mode 100644 android/src/main/java/com/rcttabview/SVGDecoder.kt create mode 100644 android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt create mode 100644 android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt create mode 100644 android/src/main/java/com/rcttabview/TabViewGlideModule.kt create mode 100644 example/assets/setttings.svg diff --git a/android/build.gradle b/android/build.gradle index 0fc108f8..c522bc2e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,6 +25,7 @@ def isNewArchitectureEnabled() { apply plugin: "com.android.library" apply plugin: "kotlin-android" +apply plugin: "kotlin-kapt" if (isNewArchitectureEnabled()) { apply plugin: "com.facebook.react" @@ -110,12 +111,20 @@ repositories { def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { + def GLIDE_VERSION = "4.16.0" // For < 0.71, this will be from the local maven repo // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'com.google.android.material:material:1.13.0-alpha06' + + api "com.github.bumptech.glide:glide:${GLIDE_VERSION}" + kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}" + + api 'com.caverock:androidsvg-aar:1.4' +// implementation 'com.github.bumptech.glide:glide:4.15.1' +// implementation 'com.github.bumptech.glide:svg:4.15.1' } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 753f8ff2..8de13ffb 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build +import android.util.Log import android.util.TypedValue import android.view.Choreographer import android.view.HapticFeedbackConstants @@ -15,11 +16,12 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources -import com.facebook.common.references.CloseableReference -import com.facebook.datasource.DataSources -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.imagepipeline.image.CloseableBitmap -import com.facebook.imagepipeline.request.ImageRequestBuilder +import androidx.core.graphics.drawable.DrawableCompat +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.WritableMap @@ -28,6 +30,8 @@ import com.facebook.react.modules.core.ReactChoreographer import com.facebook.react.views.imagehelper.ImageSource import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) { @@ -170,21 +174,37 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } private fun getDrawable(imageSource: ImageSource): Drawable? { - try { - val imageRequest = ImageRequestBuilder.newBuilderWithSource(imageSource.uri).build() - val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context) - val result = DataSources.waitForFinalResult(dataSource) as CloseableReference - val bitmap = result.get().underlyingBitmap - - CloseableReference.closeSafely(result) - dataSource.close() - - return BitmapDrawable(resources, bitmap) - } catch (_: Exception) { - // Asset doesn't exist + val uri = imageSource.uri.toString() + val isSvg = uri.contains(".svg", ignoreCase = true) + Log.d("ReactBottomNav", "Loading image: $uri, isSvg: $isSvg") + + return try { + runBlocking(Dispatchers.IO) { + val drawable = GlideApp.with(context) + .`as`(Drawable::class.java) + .load(imageSource.uri) + .apply { + if (isSvg) { + override(200, 200) + } + } + .submit() + .get() + + // Make the drawable tintable + if (isSvg && drawable != null) { + DrawableCompat.wrap(drawable.mutate()).apply { + DrawableCompat.setTintList(this, null) // Clear any existing tint + alpha = 255 + } + } else { + drawable + } + } + } catch (e: Exception) { + Log.e("ReactBottomNav", "Error loading image: $uri", e) + null } - - return null } override fun onDetachedFromWindow() { diff --git a/android/src/main/java/com/rcttabview/SVGDecoder.kt b/android/src/main/java/com/rcttabview/SVGDecoder.kt new file mode 100644 index 00000000..6e11ee08 --- /dev/null +++ b/android/src/main/java/com/rcttabview/SVGDecoder.kt @@ -0,0 +1,35 @@ +package com.rcttabview + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.InputStream + +class SVGDecoder : ResourceDecoder { + override fun handles(source: InputStream, options: Options) = true + + @Throws(IOException::class) + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { + return try { + val svg: SVG = SVG.getFromInputStream(source) + // Use document width and height if view box is not set. + // Later, we will override the document width and height with the dimensions of the native view. + if (svg.documentViewBox == null) { + val documentWidth = svg.documentWidth + val documentHeight = svg.documentHeight + if (documentWidth != -1f && documentHeight != -1f) { + svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight) + } + } + svg.documentWidth = width.toFloat() + svg.documentHeight = height.toFloat() + SimpleResource(svg) + } catch (ex: SVGParseException) { + throw IOException("Cannot load SVG from stream", ex) + } + } +} diff --git a/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt b/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt new file mode 100644 index 00000000..03bec929 --- /dev/null +++ b/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt @@ -0,0 +1,44 @@ +package com.rcttabview + +import android.content.Context +import android.graphics.Picture +import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder +import com.caverock.androidsvg.SVG + +/** + * We have to use the intrinsicWidth/Height from the Picture to render the image at a high enough resolution, but at the same time we want to return the actual + * preferred width and height of the SVG to JS. This class allows us to do that. + */ +class SVGPictureDrawable(picture: Picture, val svgIntrinsicWidth: Int, val svgIntrinsicHeight: Int) : PictureDrawable(picture) + + + +/** + * Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]). + * + * Copied from https://github.com/bumptech/glide/blob/10acc31a16b4c1b5684f69e8de3117371dfa77a8/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgDrawableTranscoder.java + * and rewritten to Kotlin. + */ +class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder { + override fun transcode(toTranscode: Resource, options: Options): Resource { + val svgData = toTranscode.get() + // If the svg doesn't have a viewBox, we can't determine its intrinsic width and height, so we default to 512x512. + // Same dimensions are used in the AndroidSVG library when the viewBox is not set. + val intrinsicWidth = svgData.documentViewBox?.width()?.toInt() ?: 512 + val intrinsicHeight = svgData.documentViewBox?.height()?.toInt() ?: 512 + + val picture = SVGPictureDrawable( + svgData.renderToPicture(), + intrinsicWidth, + intrinsicHeight + ) + return SimpleResource( + picture + ) + } +} diff --git a/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt b/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt new file mode 100644 index 00000000..f6c55408 --- /dev/null +++ b/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt @@ -0,0 +1,18 @@ +package com.rcttabview + +import android.content.Context +import android.util.Log +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class TabViewAppGlideModule : AppGlideModule() { + override fun applyOptions(context: Context, builder: GlideBuilder) { + super.applyOptions(context, builder) + + builder.setLogLevel( + Log.ERROR + ) + } +} diff --git a/android/src/main/java/com/rcttabview/TabViewGlideModule.kt b/android/src/main/java/com/rcttabview/TabViewGlideModule.kt new file mode 100644 index 00000000..e0fe4995 --- /dev/null +++ b/android/src/main/java/com/rcttabview/TabViewGlideModule.kt @@ -0,0 +1,25 @@ +package com.rcttabview + +import android.content.Context +import android.graphics.drawable.Drawable +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.LibraryGlideModule +import com.caverock.androidsvg.SVG +import java.io.InputStream + + +@GlideModule +class SvgModule: LibraryGlideModule() { + override fun registerComponents( + context: Context, glide: Glide, registry: Registry + ) { + registry + .register( + SVG::class.java, + Drawable::class.java, SVGDrawableTranscoder(context) + ) + .append(InputStream::class.java, SVG::class.java, SVGDecoder()) + } +} diff --git a/example/assets/setttings.svg b/example/assets/setttings.svg new file mode 100644 index 00000000..63cebb1c --- /dev/null +++ b/example/assets/setttings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/ios/Podfile b/example/ios/Podfile index 7a7dc9cc..19495bb0 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -6,4 +6,8 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'ReactNativeBottomTabsExample.xcworkspace' -use_test_app! +use_frameworks! :linkage => :static + +use_test_app! do |test_app| +end + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d1cbacfd..658d1840 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1209,7 +1209,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-bottom-tabs (0.0.12): + - react-native-bottom-tabs (0.3.2): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1222,6 +1222,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi + - react-native-bottom-tabs/common (= 0.3.2) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1229,6 +1230,32 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - SDWebImage (~> 5.11.1) + - SDWebImageSVGCoder (~> 1.7.0) + - SwiftUIIntrospect (~> 1.0) + - Yoga + - react-native-bottom-tabs/common (0.3.2): + - DoubleConversion + - glog + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SDWebImage (~> 5.11.1) + - SDWebImageSVGCoder (~> 1.7.0) - SwiftUIIntrospect (~> 1.0) - Yoga - react-native-safe-area-context (4.11.0): @@ -1563,7 +1590,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ReactTestApp-DevSupport (3.10.10): + - ReactTestApp-DevSupport (3.10.21): - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) @@ -1588,7 +1615,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.34.0): + - RNScreens (3.35.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1609,9 +1636,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.34.0) + - RNScreens/common (= 3.35.0) - Yoga - - RNScreens/common (3.34.0): + - RNScreens/common (3.35.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1633,6 +1660,32 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNVectorIcons (10.2.0): + - DoubleConversion + - glog + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageSVGCoder (1.7.0): + - SDWebImage/Core (~> 5.6) - SocketRocket (0.7.0) - SwiftUIIntrospect (1.3.0) - Yoga (0.0.0) @@ -1707,10 +1760,13 @@ DEPENDENCIES: - ReactTestApp-Resources (from `..`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNScreens (from `../node_modules/react-native-screens`) + - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: + - SDWebImage + - SDWebImageSVGCoder - SocketRocket - SwiftUIIntrospect @@ -1847,6 +1903,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNScreens: :path: "../node_modules/react-native-screens" + RNVectorIcons: + :path: "../node_modules/react-native-vector-icons" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1865,62 +1923,65 @@ SPEC CHECKSUMS: React-Core: bcb0b025382981724a4b9f3708c2bf9e1eab1354 React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9 - React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd - React-defaultsnativemodule: 371dc516e5020f8b87f1d32f8fa6872cafcc2081 - React-domnativemodule: 5d1288b9b8666b818a1004b56a03befc00eb5698 - React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23 - React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512 - React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6 - React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac - React-featureflagsnativemodule: e774cf495486b0e2a8b324568051d6b4c722fa93 - React-graphics: 7572851bca7242416b648c45d6af87d93d29281e - React-idlecallbacksnativemodule: d2009bad67ef232a0ee586f53193f37823e81ef1 - React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4 + React-debug: 16366fffc0cf8914d4e69a103b2b9d06b051daa7 + React-defaultsnativemodule: 3eb3b05e995daf2e81b4d7bdde117e1bb2d15855 + React-domnativemodule: 8b3c53b474a1306d6dc386db11fa3a98f6b60ee3 + React-Fabric: 931db7e8b46e27259d87e3ca48433ac7abfe542e + React-FabricComponents: ca1fdd7963375f500a143e76d8c25543f48b8f7c + React-FabricImage: 37bf481e70c1b35f83099f442e65a42197272810 + React-featureflags: 3b80a592aec905c62bcea96f219f33a88f5a8ea3 + React-featureflagsnativemodule: 7b36ba4783663a9dd847ad6d7a7d3d53cac79e7e + React-graphics: 281e54a228ae0541b74e428394b4ac54d2210a2b + React-idlecallbacksnativemodule: 22fa7d7c5ff44a1c7b52ab5837467a026d8d9543 + React-ImageManager: a1ea46ce239f0fcd1df471c35dc559c8b26f31d8 React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab - React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3 + React-jserrorhandler: f5e00daad1d80feb67f3400cf8bf25f782b34b67 React-jsi: 497ac6512d81055258869d1f894472ef71ae85e1 React-jsiexecutor: bcb0a26448cafc995d5c0c8c31960d53fcc93bd9 - React-jsinspector: 1bcd2707dd2601987bc92cbcd56737f353cc4541 - React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b + React-jsinspector: a9b2c443dd92ecd2464d3723bdc1e4627ed5861f + React-jsitracing: 11e3a39d0b47c8ba84aa94d137a7a11a6074a82b React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 - React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 - React-microtasksnativemodule: 987cf7e0e0e7129250a48b807e70d3b906c726cf - react-native-bottom-tabs: 30906150a76d9735a58080792f363dc9ccee9c04 - react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9 - React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 - React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf + React-Mapbuffer: a90dc2b770956814f2f2ac4553063c5ce2618fe8 + React-microtasksnativemodule: dd1f533309f69ccbc452f47912409b0cf56a97a5 + react-native-bottom-tabs: 662c8f2122388a3e8e084e7fff3b9d1e41990a4f + react-native-safe-area-context: 6e4f4a0d19efedadc522cf15111bcec986d9f8f6 + React-nativeconfig: cd572c97f64676dcc2a1c88fc644b693b98408bf + React-NativeModulesApple: 919bd3083d5c007cf973a985dd1399f8c2622813 React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b - React-performancetimeline: d15a723422ed500f47cb271f3175abbeb217f5ba + React-performancetimeline: 2274e28dc1bd9b22c70913fdf93203ea3988a126 React-RCTActionSheet: cb2b38a53d03ec22f1159c89667b86c2c490d92d React-RCTAnimation: 6836c87c7364f471e9077fda80b7349bc674be33 React-RCTAppDelegate: 8939a29da847bc51a01264d020a58d9d5035e119 React-RCTBlob: 984c80df29f3b3e3193bfbc2768bd302c889719b - React-RCTFabric: 4bb022567aacec7417d04741cba7e7baaeec6add + React-RCTFabric: dda27b2578cf768e43579137e8cc58ae299623d1 React-RCTImage: 1b2c2c1716db859ffff2d7a06a30b0ec5c677fc5 React-RCTLinking: 59c07577767e705b0ab95d11e5ad74c61bf2a022 React-RCTNetwork: f9a827e7d6bc428e0d99cd1fbe0427854354b8c1 React-RCTSettings: 614252fecc24840f61590c016aca1664a52cfb0f React-RCTText: 424549f68867265aa25969f50e7b9bf8bd70ae55 React-RCTVibration: c8d156e6cce18f00b0310db7670fa997c7cda407 - React-rendererconsistency: 993f54bb0df644df2922cd87ea55238d510d992b - React-rendererdebug: 7a8cbb632b68d666ad0fc01b3f9dc1a1bcc9a9f9 - React-rncore: 1df26fe0ae861c599f9f2896f45e8834ef4b85f9 - React-RuntimeApple: d20ee6d0cf3a361ec2e43c09d0f2778a863ce154 - React-RuntimeCore: 0fd059fd563e8ea69528ebd8645b319490e449ad + React-rendererconsistency: e5f034a362659ade5e7de13e41b5d77c0b9f5c1e + React-rendererdebug: 4b06fadff173e3cdc1cefa45fc4dfe5ca087415b + React-rncore: 639c8dde2af07944446a829dd784e19332890a38 + React-RuntimeApple: b481af99ce623ba013c545795bab2ddb4221ea40 + React-RuntimeCore: e9747248a4b44df68513c34e2970376e969e783e React-runtimeexecutor: 9a668b94ad5d93755443311715bd57680330286a - React-runtimescheduler: 99993f1fc3d49f13a02784e339e45b36c3aae203 - React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3 - ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6 - ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec - ReactNativeHost: 99c0ffb175cd69de2ac9a70892cd22dac65ea79d - ReactTestApp-DevSupport: b7cd76a3aeee6167f5e14d82f09685059152c426 - ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154 - RNGestureHandler: 18b9b5d65c77c4744a640f69b7fccdd47ed935c0 - RNScreens: 5288a8dbeedb3c5051aa2d5658c1c553c050b80a + React-runtimescheduler: af4987e331fe6f0a9ab2c085b2cbe0f198664749 + React-utils: 6238f5ae85f21e2cc635433dfb983af374ef5705 + ReactCodegen: 858a5ab83a907b369f991a5ca4d6d0d2e64640b1 + ReactCommon: 8ec8f3bf7ae4c4644c61533c8240db61104587dd + ReactNativeHost: 063ec25d0598fe60aa0cdbcced66cd1ca329dfd5 + ReactTestApp-DevSupport: 74ff23aba1f35caa74d1dd5346c2835e0af31770 + ReactTestApp-Resources: f9d4fd5651f8e68f6362f7e5374c7aca3b381c94 + RNGestureHandler: 698266983baef314c1f58e6df630840c504a9224 + RNScreens: 87c05ea10bfea2143ba9a081f873b1519affc8d2 + RNVectorIcons: a4790225fbbf0b66b7cc494023c6152629ba6b0c + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d - Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 + Yoga: 33604ac44957ebe3f30f15b4cd0a3f96634e624a -PODFILE CHECKSUM: 539add55dc6c2e7f9754e288b1ce4fd8583819ae +PODFILE CHECKSUM: 7c6b8973ba92a336cc16b979e96fe17edd99d821 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/example/package.json b/example/package.json index 53ee13fd..206078bd 100644 --- a/example/package.json +++ b/example/package.json @@ -38,7 +38,7 @@ "@rnx-kit/metro-config": "^2.0.0", "@types/react-native-vector-icons": "^6.4.18", "react-native-builder-bob": "^0.30.2", - "react-native-test-app": "^3.10.10" + "react-native-test-app": "^3.10.21" }, "engines": { "node": ">=18" diff --git a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx index 4230ea43..83cf058f 100644 --- a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx +++ b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx @@ -94,7 +94,7 @@ function NativeBottomTabsEmbeddedStacks() { name="Chat" component={ChatStackScreen} options={{ - tabBarIcon: () => require('../../assets/icons/chat_dark.png'), + tabBarIcon: () => require('../../assets/setttings.svg'), }} /> diff --git a/ios/TabViewProvider.swift b/ios/TabViewProvider.swift index e2b2a100..f7565303 100644 --- a/ios/TabViewProvider.swift +++ b/ios/TabViewProvider.swift @@ -1,6 +1,8 @@ import Foundation import SwiftUI import React +import SDWebImage +import SDWebImageSVGCoder @objc public final class TabInfo: NSObject { @objc public let key: String @@ -161,6 +163,7 @@ import React self.init() self.delegate = delegate self.imageLoader = imageLoader + SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) } public override func didUpdateReactSubviews() { @@ -194,29 +197,55 @@ import React private func loadIcons(_ icons: NSArray?) { // TODO: Diff the arrays and update only changed items. // Now if the user passes `unfocusedIcon` we update every item. - if let imageSources = icons as? [RCTImageSource?] { - for (index, imageSource) in imageSources.enumerated() { - guard let imageSource, let imageLoader else { continue } - imageLoader.loadImage( - with: imageSource.request, - size: imageSource.size, - scale: imageSource.scale, - clipped: false, - resizeMode: RCTResizeMode.cover, - progressBlock: { _,_ in }, - partialLoad: { _ in }, - completionBlock: { error, image in - if error != nil { - print("[TabView] Error loading image: \(error!.localizedDescription)") - return - } - guard let image else { return } - DispatchQueue.main.async { - self.props.icons[index] = image.resizeImageTo(size: self.iconSize) + guard let imageSources = icons as? [RCTImageSource?] else { return } + + + for (index, imageSource) in imageSources.enumerated() { + guard let imageSource = imageSource, + let urlString = imageSource.request.url?.absoluteString else { continue } + + let url = URL(string: urlString) + let isSVG = url?.pathExtension.lowercased() == "svg" + + // Configure SVG specific options if needed + var options: SDWebImageOptions = [.continueInBackground] + if isSVG { + options.insert(.decodeFirstFrameOnly) + } + + // Create context options for SVG rendering + let context: [SDWebImageContextOption: Any]? = isSVG ? [ + .svgImageSize: iconSize, + .imageThumbnailPixelSize: iconSize + ] : nil + + SDWebImageManager.shared.loadImage( + with: url, + options: options, + context: context, + progress: nil + ) { [weak self] (image, _, _, _, _, _) in + guard let self = self else { return } + + DispatchQueue.main.async { + if let image = image { + if isSVG { + // SVG images are already sized correctly through the context options + self.props.icons[index] = image + } else { + // Resize non-SVG images + if let resizedImage = image.sd_resizedImage( + with: self.iconSize, + scaleMode: .aspectFit + ) { + self.props.icons[index] = resizedImage + } } - }) + } + } } } + } private func parseTabData(from array: NSArray?) -> [TabInfo] { diff --git a/react-native-bottom-tabs.podspec b/react-native-bottom-tabs.podspec index ea18b756..94cde62b 100644 --- a/react-native-bottom-tabs.podspec +++ b/react-native-bottom-tabs.podspec @@ -16,6 +16,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/okwasniewski/react-native-bottom-tabs.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" + s.static_framework = true if new_arch_enabled s.subspec "common" do |ss| @@ -24,12 +25,14 @@ Pod::Spec.new do |s| end end + s.dependency "SwiftUIIntrospect", '~> 1.0' + s.dependency 'SDWebImage', '~> 5.11.1' + s.dependency 'SDWebImageSVGCoder', '~> 1.7.0' + s.pod_target_xcconfig = { - "DEFINES_MODULE" => "YES" + 'DEFINES_MODULE' => 'YES' } - s.dependency "SwiftUIIntrospect", '~> 1.0' - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) diff --git a/yarn.lock b/yarn.lock index c7f2c92d..821bced5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13757,7 +13757,7 @@ __metadata: react-native-paper: ^5.12.5 react-native-safe-area-context: ^4.11.0 react-native-screens: ^3.35.0 - react-native-test-app: ^3.10.10 + react-native-test-app: ^3.10.21 react-native-vector-icons: ^10.2.0 languageName: unknown linkType: soft @@ -13887,9 +13887,9 @@ __metadata: languageName: node linkType: hard -"react-native-test-app@npm:^3.10.10": - version: 3.10.10 - resolution: "react-native-test-app@npm:3.10.10" +"react-native-test-app@npm:^3.10.21": + version: 3.10.21 + resolution: "react-native-test-app@npm:3.10.21" dependencies: "@rnx-kit/react-native-host": ^0.5.0 ajv: ^8.0.0 @@ -13899,12 +13899,12 @@ __metadata: semver: ^7.3.5 uuid: ^10.0.0 peerDependencies: - "@callstack/react-native-visionos": 0.73 - 0.75 + "@callstack/react-native-visionos": 0.73 - 0.76 "@expo/config-plugins": ">=5.0" react: 17.0.1 - 19.0 - react-native: 0.66 - 0.75 || >=0.76.0-0 <0.76.0 - react-native-macos: ^0.0.0-0 || 0.66 || 0.68 || 0.71 - 0.75 - react-native-windows: ^0.0.0-0 || 0.66 - 0.75 + react-native: 0.66 - 0.76 || >=0.77.0-0 <0.77.0 + react-native-macos: ^0.0.0-0 || 0.66 || 0.68 || 0.71 - 0.76 + react-native-windows: ^0.0.0-0 || 0.66 - 0.76 peerDependenciesMeta: "@callstack/react-native-visionos": optional: true @@ -13919,7 +13919,7 @@ __metadata: init: scripts/init.mjs init-test-app: scripts/init.mjs install-windows-test-app: windows/test-app.mjs - checksum: eab41098fb0358766802198fa5bed16d8344c4d8b1e2a325857ec1ee3b1d6617287c8c5620e35c9e815c40eaee29fc9582d73ad21b03256b568755f5209cbaaa + checksum: ce86f478ddc6f1f150b7790166a4743c812f248405dbe957adcf969889176da6e33010e45213d2833a36d78623330eab83cebd7e56a7a79afb9f537d780341db languageName: node linkType: hard From 24c2c001f41b3ccc523e71e215a627fae3fbdd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Sun, 10 Nov 2024 09:14:43 +0100 Subject: [PATCH 2/6] define modular headers --- example/ios/Podfile | 4 +- example/ios/Podfile.lock | 82 ++++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 19495bb0..4f8c94d0 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -6,8 +6,8 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'ReactNativeBottomTabsExample.xcworkspace' -use_frameworks! :linkage => :static - use_test_app! do |test_app| + pod 'SDWebImage', :modular_headers => true + pod 'SDWebImageSVGCoder', :modular_headers => true end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 658d1840..808477e1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1209,7 +1209,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-bottom-tabs (0.3.2): + - react-native-bottom-tabs (0.4.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1222,7 +1222,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-bottom-tabs/common (= 0.3.2) + - react-native-bottom-tabs/common (= 0.4.0) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1234,7 +1234,7 @@ PODS: - SDWebImageSVGCoder (~> 1.7.0) - SwiftUIIntrospect (~> 1.0) - Yoga - - react-native-bottom-tabs/common (0.3.2): + - react-native-bottom-tabs/common (0.4.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1761,6 +1761,8 @@ DEPENDENCIES: - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNScreens (from `../node_modules/react-native-screens`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) + - SDWebImage + - SDWebImageSVGCoder - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1923,65 +1925,65 @@ SPEC CHECKSUMS: React-Core: bcb0b025382981724a4b9f3708c2bf9e1eab1354 React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9 - React-debug: 16366fffc0cf8914d4e69a103b2b9d06b051daa7 - React-defaultsnativemodule: 3eb3b05e995daf2e81b4d7bdde117e1bb2d15855 - React-domnativemodule: 8b3c53b474a1306d6dc386db11fa3a98f6b60ee3 - React-Fabric: 931db7e8b46e27259d87e3ca48433ac7abfe542e - React-FabricComponents: ca1fdd7963375f500a143e76d8c25543f48b8f7c - React-FabricImage: 37bf481e70c1b35f83099f442e65a42197272810 - React-featureflags: 3b80a592aec905c62bcea96f219f33a88f5a8ea3 - React-featureflagsnativemodule: 7b36ba4783663a9dd847ad6d7a7d3d53cac79e7e - React-graphics: 281e54a228ae0541b74e428394b4ac54d2210a2b - React-idlecallbacksnativemodule: 22fa7d7c5ff44a1c7b52ab5837467a026d8d9543 - React-ImageManager: a1ea46ce239f0fcd1df471c35dc559c8b26f31d8 + React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd + React-defaultsnativemodule: 8dd41726048ad16c135e1585797de81480c161fc + React-domnativemodule: a2f6c53b4edd50da888e240ba92b038a7264e713 + React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23 + React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512 + React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6 + React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac + React-featureflagsnativemodule: 7ee9bb16c9b3039c78eea088bc99819827981e12 + React-graphics: 7572851bca7242416b648c45d6af87d93d29281e + React-idlecallbacksnativemodule: 2369a5e611553b9d43ec56577ad76d8d6b8e2474 + React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4 React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab - React-jserrorhandler: f5e00daad1d80feb67f3400cf8bf25f782b34b67 + React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3 React-jsi: 497ac6512d81055258869d1f894472ef71ae85e1 React-jsiexecutor: bcb0a26448cafc995d5c0c8c31960d53fcc93bd9 - React-jsinspector: a9b2c443dd92ecd2464d3723bdc1e4627ed5861f - React-jsitracing: 11e3a39d0b47c8ba84aa94d137a7a11a6074a82b + React-jsinspector: 1bcd2707dd2601987bc92cbcd56737f353cc4541 + React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 - React-Mapbuffer: a90dc2b770956814f2f2ac4553063c5ce2618fe8 - React-microtasksnativemodule: dd1f533309f69ccbc452f47912409b0cf56a97a5 - react-native-bottom-tabs: 662c8f2122388a3e8e084e7fff3b9d1e41990a4f - react-native-safe-area-context: 6e4f4a0d19efedadc522cf15111bcec986d9f8f6 - React-nativeconfig: cd572c97f64676dcc2a1c88fc644b693b98408bf - React-NativeModulesApple: 919bd3083d5c007cf973a985dd1399f8c2622813 + React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 + React-microtasksnativemodule: 2eb1a69d35e700f752944644c0295cf7161d06c5 + react-native-bottom-tabs: 7c186a561ad1a19c66ebcbbbdc914a7955f3b706 + react-native-safe-area-context: c6e59b0ac0acb3ddc3247235215775441ca1b2ff + React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 + React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b - React-performancetimeline: 2274e28dc1bd9b22c70913fdf93203ea3988a126 + React-performancetimeline: d15a723422ed500f47cb271f3175abbeb217f5ba React-RCTActionSheet: cb2b38a53d03ec22f1159c89667b86c2c490d92d React-RCTAnimation: 6836c87c7364f471e9077fda80b7349bc674be33 React-RCTAppDelegate: 8939a29da847bc51a01264d020a58d9d5035e119 React-RCTBlob: 984c80df29f3b3e3193bfbc2768bd302c889719b - React-RCTFabric: dda27b2578cf768e43579137e8cc58ae299623d1 + React-RCTFabric: 4bb022567aacec7417d04741cba7e7baaeec6add React-RCTImage: 1b2c2c1716db859ffff2d7a06a30b0ec5c677fc5 React-RCTLinking: 59c07577767e705b0ab95d11e5ad74c61bf2a022 React-RCTNetwork: f9a827e7d6bc428e0d99cd1fbe0427854354b8c1 React-RCTSettings: 614252fecc24840f61590c016aca1664a52cfb0f React-RCTText: 424549f68867265aa25969f50e7b9bf8bd70ae55 React-RCTVibration: c8d156e6cce18f00b0310db7670fa997c7cda407 - React-rendererconsistency: e5f034a362659ade5e7de13e41b5d77c0b9f5c1e - React-rendererdebug: 4b06fadff173e3cdc1cefa45fc4dfe5ca087415b - React-rncore: 639c8dde2af07944446a829dd784e19332890a38 - React-RuntimeApple: b481af99ce623ba013c545795bab2ddb4221ea40 - React-RuntimeCore: e9747248a4b44df68513c34e2970376e969e783e + React-rendererconsistency: 993f54bb0df644df2922cd87ea55238d510d992b + React-rendererdebug: 7a8cbb632b68d666ad0fc01b3f9dc1a1bcc9a9f9 + React-rncore: 1df26fe0ae861c599f9f2896f45e8834ef4b85f9 + React-RuntimeApple: d20ee6d0cf3a361ec2e43c09d0f2778a863ce154 + React-RuntimeCore: 0fd059fd563e8ea69528ebd8645b319490e449ad React-runtimeexecutor: 9a668b94ad5d93755443311715bd57680330286a - React-runtimescheduler: af4987e331fe6f0a9ab2c085b2cbe0f198664749 - React-utils: 6238f5ae85f21e2cc635433dfb983af374ef5705 - ReactCodegen: 858a5ab83a907b369f991a5ca4d6d0d2e64640b1 - ReactCommon: 8ec8f3bf7ae4c4644c61533c8240db61104587dd - ReactNativeHost: 063ec25d0598fe60aa0cdbcced66cd1ca329dfd5 + React-runtimescheduler: 99993f1fc3d49f13a02784e339e45b36c3aae203 + React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3 + ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6 + ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec + ReactNativeHost: 8f602474a76f43f2cd7823fa00575b4beb107b21 ReactTestApp-DevSupport: 74ff23aba1f35caa74d1dd5346c2835e0af31770 ReactTestApp-Resources: f9d4fd5651f8e68f6362f7e5374c7aca3b381c94 - RNGestureHandler: 698266983baef314c1f58e6df630840c504a9224 - RNScreens: 87c05ea10bfea2143ba9a081f873b1519affc8d2 - RNVectorIcons: a4790225fbbf0b66b7cc494023c6152629ba6b0c + RNGestureHandler: 6a34af1ea5d9321af615933c271b0c37a00ff473 + RNScreens: d4551ceaec50b2fd6648e36d2e47dd42ef9ccfef + RNVectorIcons: a1344e212e80e6e0f4537a9960148201175f4225 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d - Yoga: 33604ac44957ebe3f30f15b4cd0a3f96634e624a + Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 -PODFILE CHECKSUM: 7c6b8973ba92a336cc16b979e96fe17edd99d821 +PODFILE CHECKSUM: ec8970ff5a624fb462d3b2cd6e966645a41c4daa COCOAPODS: 1.15.2 From 79796e07cd07c4863f82708e22b25eee84a66f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Sun, 10 Nov 2024 13:29:51 +0100 Subject: [PATCH 3/6] add examples, make loading async on Android --- android/build.gradle | 2 - .../main/java/com/rcttabview/RCTTabView.kt | 73 +++++++++---------- .../com/rcttabview/SVGDrawableTranscoder.kt | 44 ----------- .../com/rcttabview/{ => svg}/SVGDecoder.kt | 15 ++-- .../rcttabview/svg/SVGDrawableTranscoder.kt | 32 ++++++++ .../{ => svg}/TabViewGlideModule.kt | 2 +- example/assets/icons/book-image.svg | 1 + example/assets/icons/message-circle-code.svg | 1 + example/assets/icons/newspaper.svg | 1 + example/assets/icons/user-round-search.svg | 1 + example/assets/icons/user-round.svg | 1 + example/assets/setttings.svg | 1 - example/src/App.tsx | 5 ++ .../NativeBottomTabsEmbeddedStacks.tsx | 3 +- example/src/Examples/NativeBottomTabsSVGs.tsx | 50 +++++++++++++ example/src/Examples/TintColors.tsx | 4 +- 16 files changed, 144 insertions(+), 92 deletions(-) delete mode 100644 android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt rename android/src/main/java/com/rcttabview/{ => svg}/SVGDecoder.kt (75%) create mode 100644 android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt rename android/src/main/java/com/rcttabview/{ => svg}/TabViewGlideModule.kt (96%) create mode 100644 example/assets/icons/book-image.svg create mode 100644 example/assets/icons/message-circle-code.svg create mode 100644 example/assets/icons/newspaper.svg create mode 100644 example/assets/icons/user-round-search.svg create mode 100644 example/assets/icons/user-round.svg delete mode 100644 example/assets/setttings.svg create mode 100644 example/src/Examples/NativeBottomTabsSVGs.tsx diff --git a/android/build.gradle b/android/build.gradle index c522bc2e..17081ae5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -123,8 +123,6 @@ dependencies { kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}" api 'com.caverock:androidsvg-aar:1.4' -// implementation 'com.github.bumptech.glide:glide:4.15.1' -// implementation 'com.github.bumptech.glide:svg:4.15.1' } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 8de13ffb..1b062071 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -1,5 +1,6 @@ package com.rcttabview +import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList import android.graphics.Typeface @@ -16,9 +17,7 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources -import androidx.core.graphics.drawable.DrawableCompat import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target @@ -30,8 +29,6 @@ import com.facebook.react.modules.core.ReactChoreographer import com.facebook.react.views.imagehelper.ImageSource import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) { @@ -73,6 +70,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context override fun requestLayout() { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init + if (!isLayoutEnqueued && layoutCallback != null) { isLayoutEnqueued = true // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current @@ -106,7 +104,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context val menuItem = getOrCreateItem(index, item.title) menuItem.isVisible = !item.hidden if (icons.containsKey(index)) { - menuItem.icon = getDrawable(icons[index]!!) + getDrawable(icons[index]!!) { + menuItem.icon = it + } } if (item.badge.isNotEmpty()) { @@ -154,7 +154,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context // Update existing item if exists. menu.findItem(idx)?.let { menuItem -> - menuItem.icon = getDrawable(imageSource) + getDrawable(imageSource) { + menuItem.icon = it + } } } } @@ -173,38 +175,35 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context itemRippleColor = color } - private fun getDrawable(imageSource: ImageSource): Drawable? { - val uri = imageSource.uri.toString() - val isSvg = uri.contains(".svg", ignoreCase = true) - Log.d("ReactBottomNav", "Loading image: $uri, isSvg: $isSvg") - - return try { - runBlocking(Dispatchers.IO) { - val drawable = GlideApp.with(context) - .`as`(Drawable::class.java) - .load(imageSource.uri) - .apply { - if (isSvg) { - override(200, 200) - } - } - .submit() - .get() - - // Make the drawable tintable - if (isSvg && drawable != null) { - DrawableCompat.wrap(drawable.mutate()).apply { - DrawableCompat.setTintList(this, null) // Clear any existing tint - alpha = 255 - } - } else { - drawable + @SuppressLint("CheckResult") + private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) { + GlideApp.with(context) + .`as`(Drawable::class.java) + .load(imageSource.uri) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + Log.e("RCTTabView", "Error loading image: ${imageSource.uri}", e) + return false } - } - } catch (e: Exception) { - Log.e("ReactBottomNav", "Error loading image: $uri", e) - null - } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + // Update images on the main queue. + post { onDrawableReady(resource) } + return true + } + }) + .submit() } override fun onDetachedFromWindow() { diff --git a/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt b/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt deleted file mode 100644 index 03bec929..00000000 --- a/android/src/main/java/com/rcttabview/SVGDrawableTranscoder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.rcttabview - -import android.content.Context -import android.graphics.Picture -import android.graphics.drawable.Drawable -import android.graphics.drawable.PictureDrawable -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.engine.Resource -import com.bumptech.glide.load.resource.SimpleResource -import com.bumptech.glide.load.resource.transcode.ResourceTranscoder -import com.caverock.androidsvg.SVG - -/** - * We have to use the intrinsicWidth/Height from the Picture to render the image at a high enough resolution, but at the same time we want to return the actual - * preferred width and height of the SVG to JS. This class allows us to do that. - */ -class SVGPictureDrawable(picture: Picture, val svgIntrinsicWidth: Int, val svgIntrinsicHeight: Int) : PictureDrawable(picture) - - - -/** - * Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]). - * - * Copied from https://github.com/bumptech/glide/blob/10acc31a16b4c1b5684f69e8de3117371dfa77a8/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgDrawableTranscoder.java - * and rewritten to Kotlin. - */ -class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder { - override fun transcode(toTranscode: Resource, options: Options): Resource { - val svgData = toTranscode.get() - // If the svg doesn't have a viewBox, we can't determine its intrinsic width and height, so we default to 512x512. - // Same dimensions are used in the AndroidSVG library when the viewBox is not set. - val intrinsicWidth = svgData.documentViewBox?.width()?.toInt() ?: 512 - val intrinsicHeight = svgData.documentViewBox?.height()?.toInt() ?: 512 - - val picture = SVGPictureDrawable( - svgData.renderToPicture(), - intrinsicWidth, - intrinsicHeight - ) - return SimpleResource( - picture - ) - } -} diff --git a/android/src/main/java/com/rcttabview/SVGDecoder.kt b/android/src/main/java/com/rcttabview/svg/SVGDecoder.kt similarity index 75% rename from android/src/main/java/com/rcttabview/SVGDecoder.kt rename to android/src/main/java/com/rcttabview/svg/SVGDecoder.kt index 6e11ee08..8f098a05 100644 --- a/android/src/main/java/com/rcttabview/SVGDecoder.kt +++ b/android/src/main/java/com/rcttabview/svg/SVGDecoder.kt @@ -1,4 +1,4 @@ -package com.rcttabview +package com.rcttabview.svg import com.bumptech.glide.load.Options import com.bumptech.glide.load.ResourceDecoder @@ -9,15 +9,19 @@ import com.caverock.androidsvg.SVGParseException import java.io.IOException import java.io.InputStream + class SVGDecoder : ResourceDecoder { override fun handles(source: InputStream, options: Options) = true + companion object { + const val DEFAULT_SIZE = 40f + } + @Throws(IOException::class) override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { return try { val svg: SVG = SVG.getFromInputStream(source) - // Use document width and height if view box is not set. - // Later, we will override the document width and height with the dimensions of the native view. + // Taken from https://github.com/expo/expo/blob/215d8a13a7ef3f0b36b14eead41291e2d2d6cd0c/packages/expo-image/android/src/main/java/expo/modules/image/svg/SVGDecoder.kt#L28 if (svg.documentViewBox == null) { val documentWidth = svg.documentWidth val documentHeight = svg.documentHeight @@ -25,8 +29,9 @@ class SVGDecoder : ResourceDecoder { svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight) } } - svg.documentWidth = width.toFloat() - svg.documentHeight = height.toFloat() + + svg.documentWidth = DEFAULT_SIZE + svg.documentHeight = DEFAULT_SIZE SimpleResource(svg) } catch (ex: SVGParseException) { throw IOException("Cannot load SVG from stream", ex) diff --git a/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt b/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt new file mode 100644 index 00000000..91ad2db4 --- /dev/null +++ b/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt @@ -0,0 +1,32 @@ +package com.rcttabview.svg + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder +import com.caverock.androidsvg.SVG + +class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder { + override fun transcode(toTranscode: Resource, options: Options): Resource { + val svg = toTranscode.get() + val picture = svg.renderToPicture() + val drawable = PictureDrawable(picture) + + val returnedBitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + + val canvas = Canvas(returnedBitmap) + canvas.drawPicture(drawable.picture) + val bitMapDrawable = BitmapDrawable(context.resources, returnedBitmap) + return SimpleResource(bitMapDrawable) + } +} diff --git a/android/src/main/java/com/rcttabview/TabViewGlideModule.kt b/android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt similarity index 96% rename from android/src/main/java/com/rcttabview/TabViewGlideModule.kt rename to android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt index e0fe4995..db6579d3 100644 --- a/android/src/main/java/com/rcttabview/TabViewGlideModule.kt +++ b/android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt @@ -1,4 +1,4 @@ -package com.rcttabview +package com.rcttabview.svg import android.content.Context import android.graphics.drawable.Drawable diff --git a/example/assets/icons/book-image.svg b/example/assets/icons/book-image.svg new file mode 100644 index 00000000..6aebd82f --- /dev/null +++ b/example/assets/icons/book-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/message-circle-code.svg b/example/assets/icons/message-circle-code.svg new file mode 100644 index 00000000..c0901807 --- /dev/null +++ b/example/assets/icons/message-circle-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/newspaper.svg b/example/assets/icons/newspaper.svg new file mode 100644 index 00000000..11e787fe --- /dev/null +++ b/example/assets/icons/newspaper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/user-round-search.svg b/example/assets/icons/user-round-search.svg new file mode 100644 index 00000000..5e005d85 --- /dev/null +++ b/example/assets/icons/user-round-search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/user-round.svg b/example/assets/icons/user-round.svg new file mode 100644 index 00000000..4ea27d97 --- /dev/null +++ b/example/assets/icons/user-round.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/setttings.svg b/example/assets/setttings.svg deleted file mode 100644 index 63cebb1c..00000000 --- a/example/assets/setttings.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/example/src/App.tsx b/example/src/App.tsx index 21db84d2..8f01e73d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -32,6 +32,7 @@ import NativeBottomTabs from './Examples/NativeBottomTabs'; import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsVectorIcons from './Examples/NativeBottomTabsVectorIcons'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; +import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; const FourTabsIgnoreSafeArea = () => { return ; @@ -126,6 +127,10 @@ const examples = [ component: NativeBottomTabsVectorIcons, name: 'Native Bottom Tabs with Vector Icons', }, + { + component: NativeBottomTabsSVGs, + name: 'Native Bottom Tabs with SVG Icons', + }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { diff --git a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx index 83cf058f..3a9f771a 100644 --- a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx +++ b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx @@ -94,7 +94,8 @@ function NativeBottomTabsEmbeddedStacks() { name="Chat" component={ChatStackScreen} options={{ - tabBarIcon: () => require('../../assets/setttings.svg'), + tabBarIcon: () => + require('../../assets/icons/message-circle-code.svg'), }} /> diff --git a/example/src/Examples/NativeBottomTabsSVGs.tsx b/example/src/Examples/NativeBottomTabsSVGs.tsx new file mode 100644 index 00000000..cc627b36 --- /dev/null +++ b/example/src/Examples/NativeBottomTabsSVGs.tsx @@ -0,0 +1,50 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +// This import works properly when library is published +import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsSVGs() { + return ( + + require('../../assets/icons/newspaper.svg'), + }} + /> + require('../../assets/icons/book-image.svg'), + }} + /> + + focused + ? require('../../assets/icons/user-round-search.svg') + : require('../../assets/icons/user-round.svg'), + }} + /> + + require('../../assets/icons/message-circle-code.svg'), + }} + /> + + ); +} + +export default NativeBottomTabsSVGs; diff --git a/example/src/Examples/TintColors.tsx b/example/src/Examples/TintColors.tsx index bab9b8bd..e1514983 100644 --- a/example/src/Examples/TintColors.tsx +++ b/example/src/Examples/TintColors.tsx @@ -30,7 +30,9 @@ export default function TintColorsExample() { }, { key: 'chat', - focusedIcon: require('../../assets/icons/chat_dark.png'), + focusedIcon: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg', + }, title: 'Chat', }, ]); From fa7f4d05d4389c07c1de5f6780b485b9168d3cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 11 Nov 2024 11:54:10 +0100 Subject: [PATCH 4/6] fix: iOS scaling --- ios/TabViewProvider.swift | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/ios/TabViewProvider.swift b/ios/TabViewProvider.swift index f7565303..11ea934a 100644 --- a/ios/TabViewProvider.swift +++ b/ios/TabViewProvider.swift @@ -198,27 +198,26 @@ import SDWebImageSVGCoder // TODO: Diff the arrays and update only changed items. // Now if the user passes `unfocusedIcon` we update every item. guard let imageSources = icons as? [RCTImageSource?] else { return } - - + for (index, imageSource) in imageSources.enumerated() { guard let imageSource = imageSource, - let urlString = imageSource.request.url?.absoluteString else { continue } - - let url = URL(string: urlString) - let isSVG = url?.pathExtension.lowercased() == "svg" - - // Configure SVG specific options if needed - var options: SDWebImageOptions = [.continueInBackground] + let url = imageSource.request.url else { continue } + + let isSVG = url.pathExtension.lowercased() == "svg" + + var options: SDWebImageOptions = [.continueInBackground, + .scaleDownLargeImages, + .avoidDecodeImage, + .highPriority] + if isSVG { options.insert(.decodeFirstFrameOnly) } - - // Create context options for SVG rendering + let context: [SDWebImageContextOption: Any]? = isSVG ? [ - .svgImageSize: iconSize, .imageThumbnailPixelSize: iconSize ] : nil - + SDWebImageManager.shared.loadImage( with: url, options: options, @@ -226,26 +225,17 @@ import SDWebImageSVGCoder progress: nil ) { [weak self] (image, _, _, _, _, _) in guard let self = self else { return } - DispatchQueue.main.async { - if let image = image { + if let image { if isSVG { - // SVG images are already sized correctly through the context options self.props.icons[index] = image } else { - // Resize non-SVG images - if let resizedImage = image.sd_resizedImage( - with: self.iconSize, - scaleMode: .aspectFit - ) { - self.props.icons[index] = resizedImage - } + self.props.icons[index] = image.resizeImageTo(size: self.iconSize) } } } } } - } private func parseTabData(from array: NSArray?) -> [TabInfo] { From ab0fd8c7db1940ad8ecb43311b17ad64ef951e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 11 Nov 2024 12:02:10 +0100 Subject: [PATCH 5/6] add: remote URL example --- .../guides/usage-with-react-navigation.mdx | 4 ++ example/src/App.tsx | 5 ++ .../Examples/NativeBottomTabsRemoteIcons.tsx | 54 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 example/src/Examples/NativeBottomTabsRemoteIcons.tsx diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 6067d409..1601a4da 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -189,8 +189,12 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` component={Albums} options={{ tabBarIcon: () => require('person.png'), + // SVG is also supported + tabBarIcon: () => require('person.svg'), // or tabBarIcon: () => ({ sfSymbol: 'person' }), + // You can also pass a URL + tabBarIcon: () => ({ uri: 'https://example.com/icon.png' }), }} /> ``` diff --git a/example/src/App.tsx b/example/src/App.tsx index 8f01e73d..d516e4e0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -33,6 +33,7 @@ import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsVectorIcons from './Examples/NativeBottomTabsVectorIcons'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; +import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons'; const FourTabsIgnoreSafeArea = () => { return ; @@ -131,6 +132,10 @@ const examples = [ component: NativeBottomTabsSVGs, name: 'Native Bottom Tabs with SVG Icons', }, + { + component: NativeBottomTabsRemoteIcons, + name: 'Native Bottom Tabs with SVG Remote Icons', + }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { diff --git a/example/src/Examples/NativeBottomTabsRemoteIcons.tsx b/example/src/Examples/NativeBottomTabsRemoteIcons.tsx new file mode 100644 index 00000000..7f3dbe6e --- /dev/null +++ b/example/src/Examples/NativeBottomTabsRemoteIcons.tsx @@ -0,0 +1,54 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +// This import works properly when library is published +import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsRemoteIcons() { + return ( + + ({ + uri: 'https://www.svgrepo.com/show/533824/water-container.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533813/hat-chef.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533826/shop.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533828/cheese.svg', + }), + }} + /> + + ); +} + +export default NativeBottomTabsRemoteIcons; From 4f2f2734de37f2a57e73fc59a0b018e924575f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 11 Nov 2024 13:37:03 +0100 Subject: [PATCH 6/6] docs: instruct users to use_frameworks! --- .../docs/docs/getting-started/quick-start.mdx | 30 ++++++++++++++++++- example/ios/Podfile | 1 + example/ios/Podfile.lock | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/docs/docs/getting-started/quick-start.mdx b/docs/docs/docs/getting-started/quick-start.mdx index bab492c0..50ef1344 100644 --- a/docs/docs/docs/getting-started/quick-start.mdx +++ b/docs/docs/docs/getting-started/quick-start.mdx @@ -41,7 +41,8 @@ import { PackageManagerTabs } from '@theme'; ### Expo -Add the library plugin in your `app.json` config file and [create a new build](https://docs.expo.dev/develop/development-builds/create-a-build/): +Add the library plugin in your `app.json` config file and [create a new build](https://docs.expo.dev/develop/development-builds/create-a-build/). + ```diff "expo": { @@ -50,6 +51,27 @@ Add the library plugin in your `app.json` config file and [create a new build](h } ``` +You also need to enable static linking for iOS by adding `"useFrameworks": "static"` in the `expo-build-properties` plugin. + +```diff +{ + "expo": { + "plugins": [ + "react-native-bottom-tabs", ++ [ ++ "expo-build-properties", ++ { ++ "ios": { ++ "useFrameworks": "static" ++ } ++ } ++ ] ++ ] + } +} +``` + + :::warning This library is not supported in [Expo Go](https://expo.dev/go). @@ -72,6 +94,12 @@ Edit `android/app/src/main/res/values/styles.xml` to inherit from provided theme Here you can read more about [Android Native Styling](/docs/guides/android-native-styling). +To enable static linking for iOS, Open the `./ios/Podfile` file and add the following: + +```ruby +use_frameworks!, :linkage => :static +``` + ## Example usage diff --git a/example/ios/Podfile b/example/ios/Podfile index 4f8c94d0..6dfbd4f5 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -7,6 +7,7 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'ReactNativeBottomTabsExample.xcworkspace' use_test_app! do |test_app| + # Workaround for not using use_frameworks! in the Podfile pod 'SDWebImage', :modular_headers => true pod 'SDWebImageSVGCoder', :modular_headers => true end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 808477e1..ede40c18 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1945,7 +1945,7 @@ SPEC CHECKSUMS: React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 React-microtasksnativemodule: 2eb1a69d35e700f752944644c0295cf7161d06c5 - react-native-bottom-tabs: 7c186a561ad1a19c66ebcbbbdc914a7955f3b706 + react-native-bottom-tabs: 930f7bdb6e9122a519f7a50a83908e6fe04090e6 react-native-safe-area-context: c6e59b0ac0acb3ddc3247235215775441ca1b2ff React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf