diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt index e8a82750fa38a0..a774fbcc8f6276 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt @@ -8,6 +8,7 @@ package com.facebook.react.uimanager.drawable import android.content.Context +import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Paint @@ -45,11 +46,13 @@ internal class BackgroundImageDrawable( private var backgroundImageClipPath: Path? = null private var backgroundPositioningArea: RectF? = null private var backgroundPaintingArea: RectF? = null + private val urlImageLoader = BackgroundImageURLLoader() var backgroundImageLayers: List? = null set(value) { if (field != value) { field = value + loadUrlImages(value) invalidateSelf() } } @@ -111,7 +114,7 @@ internal class BackgroundImageDrawable( } override fun draw(canvas: Canvas) { - if (backgroundImageLayers == null || backgroundImageLayers?.isEmpty() == true) { + if (backgroundImageLayers.isNullOrEmpty()) { return } @@ -134,17 +137,33 @@ internal class BackgroundImageDrawable( // So we draw in reverse (last drawn in canvas appears closest) for (index in layers.indices.reversed()) { val backgroundImageLayer = layers[index] - val size = backgroundSize?.let { it.getOrNull(index % it.size) } - val repeat = backgroundRepeat?.let { it.getOrNull(index % it.size) } - val position = backgroundPosition?.let { it.getOrNull(index % it.size) } + val size = backgroundSize?.takeIf { it.isNotEmpty() }?.let { it[index % it.size] } + val repeat = backgroundRepeat?.takeIf { it.isNotEmpty() }?.let { it[index % it.size] } + val position = backgroundPosition?.takeIf { it.isNotEmpty() }?.let { it[index % it.size] } + + val urlBitmap: Bitmap? + val (intrinsicWidth, intrinsicHeight) = when (backgroundImageLayer) { + is BackgroundImageLayer.GradientLayer -> { + urlBitmap = null + backgroundPositioningArea.width() to backgroundPositioningArea.height() + } + is BackgroundImageLayer.URLImageLayer -> { + val bitmap = urlImageLoader.loadedBitmapForUri(backgroundImageLayer.uri) + if (bitmap == null) { + continue + } + urlBitmap = bitmap + bitmap.width.toFloat() to bitmap.height.toFloat() + } + } // 2. Calculate the size of a single tile. val (tileWidth, tileHeight) = calculateBackgroundImageSize( backgroundPositioningArea.width(), backgroundPositioningArea.height(), - backgroundPositioningArea.width(), - backgroundPositioningArea.height(), + intrinsicWidth, + intrinsicHeight, size, repeat, ) @@ -153,8 +172,12 @@ internal class BackgroundImageDrawable( continue } - // 3. Set paint shader - backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight)) + // 3. Set paint shader for gradients (URL images don't use shaders) + if (backgroundImageLayer is BackgroundImageLayer.GradientLayer) { + backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight)) + } else { + backgroundPaint.setShader(null) + } // 4. Calculate spacing, x and y tiles count and position for tiles var (initialX, initialY) = calculateBackgroundPosition(tileWidth, tileHeight, position) @@ -253,7 +276,13 @@ internal class BackgroundImageDrawable( repeat(yTilesCount) { canvas.save() canvas.translate(translateX, translateY) - canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint) + if (urlBitmap != null) { + val srcRect = Rect(0, 0, urlBitmap.width, urlBitmap.height) + val dstRect = RectF(0f, 0f, tileWidth, tileHeight) + canvas.drawBitmap(urlBitmap, srcRect, dstRect, backgroundPaint) + } else { + canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint) + } canvas.restore() translateY += tileHeight + ySpacing } @@ -412,4 +441,14 @@ internal class BackgroundImageDrawable( return translateX to translateY } + + private fun loadUrlImages(layers: List?) { + val uris = layers?.filterIsInstance()?.map { it.uri } + if (uris.isNullOrEmpty()) { + urlImageLoader.cancelAllRequests() + return + } + + urlImageLoader.loadImages(uris) { invalidateSelf() } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt new file mode 100644 index 00000000000000..ea928930e70af3 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageURLLoader.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.drawable + +import android.graphics.Bitmap +import android.net.Uri +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.logging.FLog +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequestBuilder +import com.facebook.react.bridge.UiThreadUtil +import java.util.concurrent.ConcurrentHashMap + +internal class BackgroundImageURLLoader { + private companion object { + private const val TAG = "BackgroundImageURLLoader" + } + + private val pendingRequests = mutableMapOf>>() + private val loadedBitmaps = ConcurrentHashMap() + private var onComplete: (() -> Unit)? = null + + fun loadImages( + uris: List, + onComplete: () -> Unit + ) { + cancelAllRequests() + + if (uris.isEmpty()) { + onComplete() + return + } + + this.onComplete = onComplete + for (uri in uris) { + val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri)).build() + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) + + pendingRequests[uri] = dataSource + + dataSource.subscribe( + object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(bitmap: Bitmap?) { + if (bitmap != null) { + loadedBitmaps[uri] = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) + } + onRequestComplete(uri) + } + + override fun onFailureImpl(dataSource: DataSource>) { + FLog.w(TAG, "Failed to load background image: $uri-${dataSource.failureCause}") + onRequestComplete(uri) + } + }, + CallerThreadExecutor.getInstance() + ) + } + } + + fun loadedBitmapForUri(uri: String): Bitmap? = loadedBitmaps[uri] + + private fun onRequestComplete(uri: String) { + pendingRequests.remove(uri) + if (pendingRequests.isEmpty()) { + UiThreadUtil.runOnUiThread { onComplete?.invoke() } + } + } + + fun cancelAllRequests() { + for (dataSource in pendingRequests.values) { + dataSource.close() + } + pendingRequests.clear() + loadedBitmaps.clear() + onComplete = null + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt index 1b1cd5d74017b4..2c0c8e661f166a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt @@ -12,34 +12,38 @@ import android.graphics.Shader import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType -public class BackgroundImageLayer() { - private lateinit var gradient: Gradient - - private constructor(gradient: Gradient) : this() { - this.gradient = gradient +public sealed class BackgroundImageLayer { + public class GradientLayer internal constructor(private val gradient: Gradient) : BackgroundImageLayer() { + public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height) } + public class URLImageLayer(public val uri: String) : BackgroundImageLayer() + public companion object { - public fun parse(gradientMap: ReadableMap?, context: Context): BackgroundImageLayer? { - if (gradientMap == null) { + public fun parse(backgroundImageMap: ReadableMap?, context: Context): BackgroundImageLayer? { + if (backgroundImageMap == null) { return null } - val gradient = parseGradient(gradientMap, context) ?: return null - return BackgroundImageLayer(gradient) - } - private fun parseGradient(gradientMap: ReadableMap, context: Context): Gradient? { - if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) { + if (!backgroundImageMap.hasKey("type") || backgroundImageMap.getType("type") != ReadableType.String) { return null } - return when (gradientMap.getString("type")) { - "linear-gradient" -> LinearGradient.parse(gradientMap, context) - "radial-gradient" -> RadialGradient.parse(gradientMap, context) + return when (backgroundImageMap.getString("type")) { + "linear-gradient" -> { + val gradient = LinearGradient.parse(backgroundImageMap, context) ?: return null + GradientLayer(gradient) + } + "radial-gradient" -> { + val gradient = RadialGradient.parse(backgroundImageMap, context) ?: return null + GradientLayer(gradient) + } + "url" -> { + val uri = backgroundImageMap.getString("uri") ?: return null + URLImageLayer(uri) + } else -> null } } } - - public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height) } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt index 48d46166995e0a..741d5c6da0cf3f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt @@ -22,7 +22,10 @@ add_library(rrc_view OBJECT ${rrc_view_SRC}) react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ ${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/) -target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR}) +react_native_android_selector(imagemanager_platform_DIR + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/android/ + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/cxx/) +target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${imagemanager_platform_DIR}) target_link_libraries(rrc_view folly_runtime