diff --git a/android/build.gradle b/android/build.gradle index 114be81b3e..5dd2a32b90 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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") diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt index 62edb40a8e..6871d3c556 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt @@ -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 @@ -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) @@ -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) } @@ -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" } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt new file mode 100644 index 0000000000..3aebea1f5c --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/image/TabsImageLoader.kt @@ -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>() { + override fun onNewResultImpl(dataSource: DataSource?>) { + 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?>) { + 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() +}