Skip to content
Merged
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
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 @@ -21,6 +16,7 @@ import com.swmansion.rnscreens.gamma.tabs.event.TabScreenDidAppearEvent
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.gamma.tabs.image.loadTabImage
import com.swmansion.rnscreens.utils.RNSLog

@ReactModule(name = TabScreenViewManager.REACT_CLASS)
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,19 @@ 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
loadTabImage(context, uri) { drawable ->
// Since image loading might happen on a background thread
// ref. https://frescolib.org/docs/intro-image-pipeline.html
// We should schedule rendering the result on the UI thread
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,96 @@
package com.swmansion.rnscreens.gamma.tabs.image

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

internal fun loadTabImage(
context: Context,
uri: String,
onLoaded: (Drawable) -> Unit,
) {
val source = resolveTabImageSource(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 resolveTabImageSource(
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