Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,12 @@ jobs:
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-

- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")

if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi

- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
Expand All @@ -97,7 +94,6 @@ jobs:
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"

- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
Expand All @@ -108,18 +104,16 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"

build-ios:
runs-on: macos-latest

env:
XCODE_VERSION: 16.3
XCODE_VERSION: 16.4
TURBO_CACHE_DIR: .turbo/ios
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1
Expand All @@ -138,15 +132,12 @@ jobs:
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-

- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")

if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi

- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
Expand All @@ -160,7 +151,6 @@ jobs:
bundle install
bundle exec pod repo update --verbose
bundle exec pod install --project-directory=ios

- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ OR
```sh
yarn add react-native-svg-render
```

## Usage

```js
Expand All @@ -23,6 +24,8 @@ import { SvgUrlView } from 'react-native-svg-render';
/>;
```

`SvgUrlView` renders remote SVGs asynchronously on both platforms while keeping a native cache and offering built-in placeholders and skeleton shimmers. Android now mirrors the iOS UX by supporting `showSkeleton`, `placeholder`, and `failedPlaceholder` props (with local or remote assets), and both platforms keep their loaders off the main thread.

iOS Setup

To use react-native-svg-render on iOS, follow these steps:
Expand Down Expand Up @@ -65,6 +68,8 @@ pod install

Rebuild your project in Xcode.

**iOS quirks.** The loader now strips the `clip-path` definition that SVGKit mishandles (such as the JSMO logo used in the example) before parsing, so clipped artboards render correctly on iOS without extra workarounds.


## Example podfile:

Expand Down Expand Up @@ -147,6 +152,11 @@ target 'AFService' do
end
```

## Android updates

- `SvgUrlView` now honours `showSkeleton`, `placeholder`, and `failedPlaceholder` on Android with remote/local assets, matching iOS.
- Networking, decoding, and caching stay off the UI thread thanks to Kotlin coroutines, so Android mirrors the iOS experience.

## Props

| Prop | Type | Description | Default | Platform Support |
Expand All @@ -155,9 +165,9 @@ end
| `width` | `number` | Width of the rendered SVG | `undefined` | iOS + Android |
| `height` | `number` | Height of the rendered SVG | `undefined` | iOS + Android |
| `style` | `ViewStyle` | Custom container styles | `undefined` | iOS + Android |
| `showSkeleton` | `boolean` | Show skeleton shimmer while loading | `true` | iOS only (Android support coming soon) |
| `placeholder` | `string` | Local/remote placeholder while loading | `undefined` | iOS only (Android support coming soon) |
| `failedPlaceholder` | `string` | Local/remote placeholder if the SVG fails | `undefined` | iOS only (Android support coming soon) |
| `showSkeleton` | `boolean` | Show skeleton shimmer while loading | `true` | iOS + Android |
| `placeholder` | `string` | Local/remote placeholder while loading | `undefined` | iOS + Android |
| `failedPlaceholder` | `string` | Local/remote placeholder if the SVG fails | `undefined` | iOS + Android |



Expand Down
135 changes: 132 additions & 3 deletions android/src/main/java/com/svgurl/SvgUrlView.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.svgurl

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.PictureDrawable
import androidx.appcompat.widget.AppCompatImageView
import com.caverock.androidsvg.SVG
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import android.util.LruCache
Expand Down Expand Up @@ -35,9 +40,14 @@ class SvgUrlView(context: Context) : AppCompatImageView(context) {
}

private var currentJob: Job? = null
private val bitmapScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var placeholderJob: Job? = null
private var failedPlaceholderJob: Job? = null

fun setUrl(url: String) {
currentJob?.cancel()
isSvgLoaded = false
lastLoadFailed = false
currentJob = CoroutineScope(Dispatchers.IO).launch {
try {
// Memory cache first
Expand All @@ -46,6 +56,7 @@ class SvgUrlView(context: Context) : AppCompatImageView(context) {
setLayerType(LAYER_TYPE_SOFTWARE, null)
scaleType = ScaleType.FIT_CENTER
setImageDrawable(cached)
stopSkeleton()
}
return@launch
}
Expand All @@ -67,11 +78,19 @@ class SvgUrlView(context: Context) : AppCompatImageView(context) {
setLayerType(LAYER_TYPE_SOFTWARE, null)
scaleType = ScaleType.FIT_CENTER
setImageDrawable(drawable)
isSvgLoaded = true
stopSkeleton()
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
showFailurePlaceholder()
stopSkeleton()
}
}
}
updatePlaceholderIfNeeded()
startSkeleton()
}

fun setWidthProp(width: Int) {
Expand All @@ -89,9 +108,119 @@ class SvgUrlView(context: Context) : AppCompatImageView(context) {
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
currentJob?.cancel()
placeholderJob?.cancel()
failedPlaceholderJob?.cancel()
bitmapScope.cancel()
stopSkeleton()
}

public fun setShowSkeleton(show: Boolean) {
showSkeleton = show
if (showSkeleton && !isSvgLoaded) {
startSkeleton()
} else {
stopSkeleton()
}
}

public fun setPlaceholder(uri: String?) {
placeholderJob?.cancel()
placeholderUri = uri
placeholderBitmap = null
if (uri == null) {
updatePlaceholderIfNeeded()
return
}
placeholderJob = bitmapScope.launch {
val bmp = decodeBitmap(uri)
withContext(Dispatchers.Main) {
if (placeholderUri != uri) return@withContext
placeholderBitmap = bmp
updatePlaceholderIfNeeded()
}
}
}

public fun setFailedPlaceholder(uri: String?) {
failedPlaceholderJob?.cancel()
failedPlaceholderUri = uri
failedPlaceholderBitmap = null
if (uri == null) return
failedPlaceholderJob = bitmapScope.launch {
val bmp = decodeBitmap(uri)
withContext(Dispatchers.Main) {
if (failedPlaceholderUri != uri) return@withContext
failedPlaceholderBitmap = bmp
if (!isSvgLoaded && lastLoadFailed) {
showFailurePlaceholder()
}
}
}
}

private fun updatePlaceholderIfNeeded() {
if (isSvgLoaded) return
placeholderBitmap?.let {
setLayerType(LAYER_TYPE_SOFTWARE, null)
scaleType = ScaleType.FIT_CENTER
setImageBitmap(it)
}
}

private fun showFailurePlaceholder() {
lastLoadFailed = true
setLayerType(LAYER_TYPE_SOFTWARE, null)
scaleType = ScaleType.FIT_CENTER
failedPlaceholderBitmap?.let {
setImageBitmap(it)
} ?: setImageDrawable(null)
}

private fun startSkeleton() {
if (!showSkeleton) return
if (skeletonAnimator?.isRunning == true) return
skeletonAnimator = ValueAnimator.ofArgb(
Color.parseColor("#dddbdb"),
Color.parseColor("#cfcfcf"),
Color.parseColor("#e2e2e2")
).apply {
duration = 900
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
addUpdateListener { setBackgroundColor(it.animatedValue as Int) }
start()
}
}

private fun stopSkeleton() {
skeletonAnimator?.cancel()
skeletonAnimator = null
setBackgroundColor(Color.TRANSPARENT)
}

private suspend fun decodeBitmap(uri: String): Bitmap? {
return try {
if (uri.startsWith("http")) {
val connection = URL(uri).openConnection() as HttpURLConnection
connection.connect()
val bitmap = connection.inputStream.use { BitmapFactory.decodeStream(it) }
connection.disconnect()
bitmap
} else {
val resId = resources.getIdentifier(uri, "drawable", context.packageName)
if (resId != 0) BitmapFactory.decodeResource(resources, resId) else null
}
} catch (e: Exception) {
null
}
}

public fun setShowSkeleton(show: Boolean) { }
public fun setPlaceholder(uri: String?) { }
public fun setFailedPlaceholder(uri: String?) { }
private var placeholderUri: String? = null
private var failedPlaceholderUri: String? = null
private var placeholderBitmap: Bitmap? = null
private var failedPlaceholderBitmap: Bitmap? = null
private var showSkeleton = true
private var isSvgLoaded = false
private var lastLoadFailed = false
private var skeletonAnimator: ValueAnimator? = null
}
4 changes: 2 additions & 2 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ target 'SvgUrlExample' do
)

# ✅ SVGKit (direct from GitHub - latest stable)
pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '3.x'
pod 'CocoaLumberjack/Swift', '~> 3.8.0'
pod 'SVGKit', '3.0.0'
pod 'CocoaLumberjack/Swift', '3.8.0'
post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install(
Expand Down
Loading
Loading