Skip to content

Commit

Permalink
feat: svg support (#138)
Browse files Browse the repository at this point in the history
* 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!
  • Loading branch information
okwasniewski authored Nov 11, 2024
1 parent 28ce672 commit 1b0ed94
Show file tree
Hide file tree
Showing 24 changed files with 462 additions and 77 deletions.
7 changes: 7 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -110,12 +111,18 @@ 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'
}

if (isNewArchitectureEnabled()) {
Expand Down
63 changes: 41 additions & 22 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.rcttabview

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Typeface
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
Expand All @@ -15,11 +17,10 @@ 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 com.bumptech.glide.load.DataSource
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
Expand Down Expand Up @@ -69,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
Expand Down Expand Up @@ -102,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()) {
Expand Down Expand Up @@ -150,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
}
}
}
}
Expand All @@ -169,22 +175,35 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
itemRippleColor = color
}

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<CloseableBitmap>
val bitmap = result.get().underlyingBitmap

CloseableReference.closeSafely(result)
dataSource.close()

return BitmapDrawable(resources, bitmap)
} catch (_: Exception) {
// Asset doesn't exist
}
@SuppressLint("CheckResult")
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
GlideApp.with(context)
.`as`(Drawable::class.java)
.load(imageSource.uri)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean {
Log.e("RCTTabView", "Error loading image: ${imageSource.uri}", e)
return false
}

return null
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
// Update images on the main queue.
post { onDrawableReady(resource) }
return true
}
})
.submit()
}

override fun onDetachedFromWindow() {
Expand Down
18 changes: 18 additions & 0 deletions android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
}
40 changes: 40 additions & 0 deletions android/src/main/java/com/rcttabview/svg/SVGDecoder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.rcttabview.svg

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<InputStream, SVG> {
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<SVG>? {
return try {
val svg: SVG = SVG.getFromInputStream(source)
// 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
if (documentWidth != -1f && documentHeight != -1f) {
svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight)
}
}

svg.documentWidth = DEFAULT_SIZE
svg.documentHeight = DEFAULT_SIZE
SimpleResource(svg)
} catch (ex: SVGParseException) {
throw IOException("Cannot load SVG from stream", ex)
}
}
}
32 changes: 32 additions & 0 deletions android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt
Original file line number Diff line number Diff line change
@@ -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<SVG?, Drawable> {
override fun transcode(toTranscode: Resource<SVG?>, options: Options): Resource<Drawable> {
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)
}
}
25 changes: 25 additions & 0 deletions android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.rcttabview.svg

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())
}
}
30 changes: 29 additions & 1 deletion docs/docs/docs/getting-started/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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).
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }),
}}
/>
```
Expand Down
1 change: 1 addition & 0 deletions example/assets/icons/book-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/assets/icons/message-circle-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/assets/icons/newspaper.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/assets/icons/user-round-search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/assets/icons/user-round.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb"

workspace 'ReactNativeBottomTabsExample.xcworkspace'

use_test_app!
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

Loading

0 comments on commit 1b0ed94

Please sign in to comment.