Skip to content

Commit 1b0ed94

Browse files
authored
feat: svg support (#138)
* wip: svg support * define modular headers * add examples, make loading async on Android * fix: iOS scaling * add: remote URL example * docs: instruct users to use_frameworks!
1 parent 28ce672 commit 1b0ed94

24 files changed

+462
-77
lines changed

android/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def isNewArchitectureEnabled() {
2525

2626
apply plugin: "com.android.library"
2727
apply plugin: "kotlin-android"
28+
apply plugin: "kotlin-kapt"
2829

2930
if (isNewArchitectureEnabled()) {
3031
apply plugin: "com.facebook.react"
@@ -110,12 +111,18 @@ repositories {
110111
def kotlin_version = getExtOrDefault("kotlinVersion")
111112

112113
dependencies {
114+
def GLIDE_VERSION = "4.16.0"
113115
// For < 0.71, this will be from the local maven repo
114116
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
115117
//noinspection GradleDynamicVersion
116118
implementation "com.facebook.react:react-native:+"
117119
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
118120
implementation 'com.google.android.material:material:1.13.0-alpha06'
121+
122+
api "com.github.bumptech.glide:glide:${GLIDE_VERSION}"
123+
kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}"
124+
125+
api 'com.caverock:androidsvg-aar:1.4'
119126
}
120127

121128
if (isNewArchitectureEnabled()) {

android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.rcttabview
22

3+
import android.annotation.SuppressLint
34
import android.content.Context
45
import android.content.res.ColorStateList
56
import android.graphics.Typeface
67
import android.graphics.drawable.BitmapDrawable
78
import android.graphics.drawable.ColorDrawable
89
import android.graphics.drawable.Drawable
910
import android.os.Build
11+
import android.util.Log
1012
import android.util.TypedValue
1113
import android.view.Choreographer
1214
import android.view.HapticFeedbackConstants
@@ -15,11 +17,10 @@ import android.view.View
1517
import android.view.ViewGroup
1618
import android.widget.TextView
1719
import androidx.appcompat.content.res.AppCompatResources
18-
import com.facebook.common.references.CloseableReference
19-
import com.facebook.datasource.DataSources
20-
import com.facebook.drawee.backends.pipeline.Fresco
21-
import com.facebook.imagepipeline.image.CloseableBitmap
22-
import com.facebook.imagepipeline.request.ImageRequestBuilder
20+
import com.bumptech.glide.load.DataSource
21+
import com.bumptech.glide.load.engine.GlideException
22+
import com.bumptech.glide.request.RequestListener
23+
import com.bumptech.glide.request.target.Target
2324
import com.facebook.react.bridge.Arguments
2425
import com.facebook.react.bridge.ReadableArray
2526
import com.facebook.react.bridge.WritableMap
@@ -69,6 +70,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
6970
override fun requestLayout() {
7071
super.requestLayout()
7172
@Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
73+
7274
if (!isLayoutEnqueued && layoutCallback != null) {
7375
isLayoutEnqueued = true
7476
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
@@ -102,7 +104,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
102104
val menuItem = getOrCreateItem(index, item.title)
103105
menuItem.isVisible = !item.hidden
104106
if (icons.containsKey(index)) {
105-
menuItem.icon = getDrawable(icons[index]!!)
107+
getDrawable(icons[index]!!) {
108+
menuItem.icon = it
109+
}
106110
}
107111

108112
if (item.badge.isNotEmpty()) {
@@ -150,7 +154,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
150154

151155
// Update existing item if exists.
152156
menu.findItem(idx)?.let { menuItem ->
153-
menuItem.icon = getDrawable(imageSource)
157+
getDrawable(imageSource) {
158+
menuItem.icon = it
159+
}
154160
}
155161
}
156162
}
@@ -169,22 +175,35 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
169175
itemRippleColor = color
170176
}
171177

172-
private fun getDrawable(imageSource: ImageSource): Drawable? {
173-
try {
174-
val imageRequest = ImageRequestBuilder.newBuilderWithSource(imageSource.uri).build()
175-
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
176-
val result = DataSources.waitForFinalResult(dataSource) as CloseableReference<CloseableBitmap>
177-
val bitmap = result.get().underlyingBitmap
178-
179-
CloseableReference.closeSafely(result)
180-
dataSource.close()
181-
182-
return BitmapDrawable(resources, bitmap)
183-
} catch (_: Exception) {
184-
// Asset doesn't exist
185-
}
178+
@SuppressLint("CheckResult")
179+
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
180+
GlideApp.with(context)
181+
.`as`(Drawable::class.java)
182+
.load(imageSource.uri)
183+
.listener(object : RequestListener<Drawable> {
184+
override fun onLoadFailed(
185+
e: GlideException?,
186+
model: Any?,
187+
target: Target<Drawable>,
188+
isFirstResource: Boolean
189+
): Boolean {
190+
Log.e("RCTTabView", "Error loading image: ${imageSource.uri}", e)
191+
return false
192+
}
186193

187-
return null
194+
override fun onResourceReady(
195+
resource: Drawable,
196+
model: Any,
197+
target: Target<Drawable>?,
198+
dataSource: DataSource,
199+
isFirstResource: Boolean
200+
): Boolean {
201+
// Update images on the main queue.
202+
post { onDrawableReady(resource) }
203+
return true
204+
}
205+
})
206+
.submit()
188207
}
189208

190209
override fun onDetachedFromWindow() {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.rcttabview
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import com.bumptech.glide.GlideBuilder
6+
import com.bumptech.glide.annotation.GlideModule
7+
import com.bumptech.glide.module.AppGlideModule
8+
9+
@GlideModule
10+
class TabViewAppGlideModule : AppGlideModule() {
11+
override fun applyOptions(context: Context, builder: GlideBuilder) {
12+
super.applyOptions(context, builder)
13+
14+
builder.setLogLevel(
15+
Log.ERROR
16+
)
17+
}
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.rcttabview.svg
2+
3+
import com.bumptech.glide.load.Options
4+
import com.bumptech.glide.load.ResourceDecoder
5+
import com.bumptech.glide.load.engine.Resource
6+
import com.bumptech.glide.load.resource.SimpleResource
7+
import com.caverock.androidsvg.SVG
8+
import com.caverock.androidsvg.SVGParseException
9+
import java.io.IOException
10+
import java.io.InputStream
11+
12+
13+
class SVGDecoder : ResourceDecoder<InputStream, SVG> {
14+
override fun handles(source: InputStream, options: Options) = true
15+
16+
companion object {
17+
const val DEFAULT_SIZE = 40f
18+
}
19+
20+
@Throws(IOException::class)
21+
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<SVG>? {
22+
return try {
23+
val svg: SVG = SVG.getFromInputStream(source)
24+
// Taken from https://github.com/expo/expo/blob/215d8a13a7ef3f0b36b14eead41291e2d2d6cd0c/packages/expo-image/android/src/main/java/expo/modules/image/svg/SVGDecoder.kt#L28
25+
if (svg.documentViewBox == null) {
26+
val documentWidth = svg.documentWidth
27+
val documentHeight = svg.documentHeight
28+
if (documentWidth != -1f && documentHeight != -1f) {
29+
svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight)
30+
}
31+
}
32+
33+
svg.documentWidth = DEFAULT_SIZE
34+
svg.documentHeight = DEFAULT_SIZE
35+
SimpleResource(svg)
36+
} catch (ex: SVGParseException) {
37+
throw IOException("Cannot load SVG from stream", ex)
38+
}
39+
}
40+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.rcttabview.svg
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import android.graphics.Canvas
6+
import android.graphics.drawable.BitmapDrawable
7+
import android.graphics.drawable.Drawable
8+
import android.graphics.drawable.PictureDrawable
9+
import com.bumptech.glide.load.Options
10+
import com.bumptech.glide.load.engine.Resource
11+
import com.bumptech.glide.load.resource.SimpleResource
12+
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
13+
import com.caverock.androidsvg.SVG
14+
15+
class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder<SVG?, Drawable> {
16+
override fun transcode(toTranscode: Resource<SVG?>, options: Options): Resource<Drawable> {
17+
val svg = toTranscode.get()
18+
val picture = svg.renderToPicture()
19+
val drawable = PictureDrawable(picture)
20+
21+
val returnedBitmap = Bitmap.createBitmap(
22+
drawable.intrinsicWidth,
23+
drawable.intrinsicHeight,
24+
Bitmap.Config.ARGB_8888
25+
)
26+
27+
val canvas = Canvas(returnedBitmap)
28+
canvas.drawPicture(drawable.picture)
29+
val bitMapDrawable = BitmapDrawable(context.resources, returnedBitmap)
30+
return SimpleResource(bitMapDrawable)
31+
}
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.rcttabview.svg
2+
3+
import android.content.Context
4+
import android.graphics.drawable.Drawable
5+
import com.bumptech.glide.Glide
6+
import com.bumptech.glide.Registry
7+
import com.bumptech.glide.annotation.GlideModule
8+
import com.bumptech.glide.module.LibraryGlideModule
9+
import com.caverock.androidsvg.SVG
10+
import java.io.InputStream
11+
12+
13+
@GlideModule
14+
class SvgModule: LibraryGlideModule() {
15+
override fun registerComponents(
16+
context: Context, glide: Glide, registry: Registry
17+
) {
18+
registry
19+
.register(
20+
SVG::class.java,
21+
Drawable::class.java, SVGDrawableTranscoder(context)
22+
)
23+
.append(InputStream::class.java, SVG::class.java, SVGDecoder())
24+
}
25+
}

docs/docs/docs/getting-started/quick-start.mdx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import { PackageManagerTabs } from '@theme';
4141

4242
### Expo
4343

44-
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/):
44+
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/).
45+
4546

4647
```diff
4748
"expo": {
@@ -50,6 +51,27 @@ Add the library plugin in your `app.json` config file and [create a new build](h
5051
}
5152
```
5253

54+
You also need to enable static linking for iOS by adding `"useFrameworks": "static"` in the `expo-build-properties` plugin.
55+
56+
```diff
57+
{
58+
"expo": {
59+
"plugins": [
60+
"react-native-bottom-tabs",
61+
+ [
62+
+ "expo-build-properties",
63+
+ {
64+
+ "ios": {
65+
+ "useFrameworks": "static"
66+
+ }
67+
+ }
68+
+ ]
69+
+ ]
70+
}
71+
}
72+
```
73+
74+
5375
:::warning
5476

5577
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
7294

7395
Here you can read more about [Android Native Styling](/docs/guides/android-native-styling).
7496

97+
To enable static linking for iOS, Open the `./ios/Podfile` file and add the following:
98+
99+
```ruby
100+
use_frameworks!, :linkage => :static
101+
```
102+
75103

76104
## Example usage
77105

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,12 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
189189
component={Albums}
190190
options={{
191191
tabBarIcon: () => require('person.png'),
192+
// SVG is also supported
193+
tabBarIcon: () => require('person.svg'),
192194
// or
193195
tabBarIcon: () => ({ sfSymbol: 'person' }),
196+
// You can also pass a URL
197+
tabBarIcon: () => ({ uri: 'https://example.com/icon.png' }),
194198
}}
195199
/>
196200
```

example/assets/icons/book-image.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

example/assets/icons/newspaper.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

example/assets/icons/user-round.svg

Lines changed: 1 addition & 0 deletions
Loading

example/ios/Podfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb"
66

77
workspace 'ReactNativeBottomTabsExample.xcworkspace'
88

9-
use_test_app!
9+
use_test_app! do |test_app|
10+
# Workaround for not using use_frameworks! in the Podfile
11+
pod 'SDWebImage', :modular_headers => true
12+
pod 'SDWebImageSVGCoder', :modular_headers => true
13+
end
14+

0 commit comments

Comments
 (0)