Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 0 additions & 6 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,6 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation "androidx.core:core-ktx:1.8.0"

def COIL_VERSION = "3.0.4"

implementation("io.coil-kt.coil3:coil:${COIL_VERSION}")
implementation("io.coil-kt.coil3:coil-network-okhttp:${COIL_VERSION}")
implementation("io.coil-kt.coil3:coil-svg:${COIL_VERSION}")

constraints {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") {
because("on older React Native versions this dependency conflicts with react-native-screens")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package com.swmansion.rnscreens.gamma.tabs

import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Log
import coil3.ImageLoader
import coil3.asDrawable
import coil3.request.ImageRequest
import coil3.svg.SvgDecoder
import android.os.Handler
import android.os.Looper
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
Expand All @@ -22,6 +17,7 @@ import com.swmansion.rnscreens.gamma.tabs.event.TabScreenDidDisappearEvent
import com.swmansion.rnscreens.gamma.tabs.event.TabScreenWillAppearEvent
import com.swmansion.rnscreens.gamma.tabs.event.TabScreenWillDisappearEvent
import com.swmansion.rnscreens.utils.RNSLog
import com.swmansion.rnscreens.utils.TabsImageLoader

@ReactModule(name = TabScreenViewManager.REACT_CLASS)
class TabScreenViewManager :
Expand All @@ -31,18 +27,9 @@ class TabScreenViewManager :

override fun getName() = REACT_CLASS

var imageLoader: ImageLoader? = null

var context: ThemedReactContext? = null

override fun createViewInstance(reactContext: ThemedReactContext): TabScreen {
imageLoader =
ImageLoader
.Builder(reactContext)
.components {
add(SvgDecoder.Factory())
}.build()
context = reactContext
RNSLog.d(REACT_CLASS, "createViewInstance")
return TabScreen(reactContext)
}
Expand Down Expand Up @@ -186,86 +173,18 @@ class TabScreenViewManager :
value: ReadableMap?,
) {
val uri = value?.getString("uri")

if (uri != null) {
val context = view.context
val source = resolveSource(context, uri)

if (source != null) {
loadUsingCoil(context, source) {
view.icon = it
TabsImageLoader.load(context, uri) { drawable ->
// Scheduling rendering image on the UI thread, because Fresco
// is having issues doing it from other threads.
Handler(Looper.getMainLooper()).post {
view.icon = drawable
}
}
}
}

private fun loadUsingCoil(
context: Context,
source: RNSImageSource,
onLoad: (img: Drawable) -> Unit,
) {
val data =
when (source) {
is RNSImageSource.DrawableRes -> source.resId
is RNSImageSource.UriString -> source.uri
}

val request =
ImageRequest
.Builder(context)
.data(data)
.target { drawable ->
val stateDrawable = drawable.asDrawable(context.resources)
onLoad(stateDrawable)
}.listener(
onError = { _, result ->
Log.e("[RNScreens]", "Error loading image: $data", result.throwable)
},
onCancel = {
Log.w("[RNScreens]", "Image loading request cancelled: $data")
},
).build()

imageLoader?.enqueue(request)
}

private fun resolveSource(
context: Context,
uri: String,
): RNSImageSource? {
// In release builds, assets are coming with bundle and we need to work with resource id.
// In debug, metro is responsible for handling assets via http.
// At the moment, we're supporting images (drawable) and SVG icons (raw).
// For any other type, we may consider adding a support in the future if needed.
if (uri.startsWith("_")) {
val drawableResId = context.resources.getIdentifier(uri, "drawable", context.packageName)
if (drawableResId != 0) {
return RNSImageSource.DrawableRes(drawableResId)
}

val rawResId = context.resources.getIdentifier(uri, "raw", context.packageName)
if (rawResId != 0) {
return RNSImageSource.DrawableRes(rawResId)
}

Log.e("[RNScreens]", "Resource not found in drawable or raw: $uri")
return null
}

// If asset isn't included in android source directories and we're loading it from given path.
return RNSImageSource.UriString(uri)
}

private sealed class RNSImageSource {
data class DrawableRes(
val resId: Int,
) : RNSImageSource()

data class UriString(
val uri: String,
) : RNSImageSource()
}

companion object {
const val REACT_CLASS = "RNSBottomTabsScreen"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.swmansion.rnscreens.utils

import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import com.facebook.common.executors.CallerThreadExecutor
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.BaseDataSubscriber
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.CloseableStaticBitmap
import com.facebook.imagepipeline.request.ImageRequestBuilder

object TabsImageLoader {
fun load(
context: Context,
uri: String,
onLoaded: (Drawable) -> Unit,
) {
val source = resolveSource(context, uri) ?: return
val finalUri =
when (source) {
is RNSImageSource.DrawableRes -> {
"res://${context.packageName}/${source.resId}".toUri()
}
is RNSImageSource.UriString -> {
source.uri.toUri()
}
}

val imageRequest =
ImageRequestBuilder
.newBuilderWithSource(finalUri)
.build()

val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
dataSource.subscribe(
object : BaseDataSubscriber<CloseableReference<CloseableImage>>() {
override fun onNewResultImpl(dataSource: DataSource<CloseableReference<CloseableImage>?>) {
if (!dataSource.isFinished) return
val imageReference = dataSource.result ?: return
val closeableImage = imageReference.get()

if (closeableImage is CloseableStaticBitmap) {
val bitmap = closeableImage.underlyingBitmap
val drawable = bitmap.toDrawable(context.resources)
onLoaded(drawable)
}

imageReference.close()
}

override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>?>) {
Log.e("[RNScreens]", "Error loading image: $uri", dataSource.failureCause)
}
},
CallerThreadExecutor.getInstance(),
)
}

private fun resolveSource(
context: Context,
uri: String,
): RNSImageSource? {
// In release builds, assets are coming with bundle and we need to work with resource id.
// In debug, metro is responsible for handling assets via http.
// At the moment, we're supporting images (drawable) and SVG icons (raw).
// For any other type, we may consider adding a support in the future if needed.
if (uri.startsWith("_")) {
val drawableResId = context.resources.getIdentifier(uri, "drawable", context.packageName)
if (drawableResId != 0) {
return RNSImageSource.DrawableRes(drawableResId)
}
val rawResId = context.resources.getIdentifier(uri, "raw", context.packageName)
if (rawResId != 0) {
return RNSImageSource.DrawableRes(rawResId)
}
Log.e("[RNScreens]", "Resource not found in drawable or raw: $uri")
return null
}

// If asset isn't included in android source directories and we're loading it from given path.
return RNSImageSource.UriString(uri)
}

private sealed class RNSImageSource {
data class DrawableRes(
val resId: Int,
) : RNSImageSource()

data class UriString(
val uri: String,
) : RNSImageSource()
}
}
Loading