Skip to content

Commit 5455ef6

Browse files
url() function support in background on android
copy bitmap on background thread
1 parent c6b137c commit 5455ef6

4 files changed

Lines changed: 228 additions & 41 deletions

File tree

‎packages/react-native/ReactAndroid/api/ReactAndroid.api‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4834,17 +4834,27 @@ public final class com/facebook/react/uimanager/events/TouchEventType$Companion
48344834
public final fun getJSEventName (Lcom/facebook/react/uimanager/events/TouchEventType;)Ljava/lang/String;
48354835
}
48364836

4837-
public final class com/facebook/react/uimanager/style/BackgroundImageLayer {
4837+
public abstract class com/facebook/react/uimanager/style/BackgroundImageLayer {
48384838
public static final field Companion Lcom/facebook/react/uimanager/style/BackgroundImageLayer$Companion;
4839-
public fun <init> ()V
4840-
public synthetic fun <init> (Lcom/facebook/react/uimanager/style/Gradient;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
4841-
public final fun getShader (FF)Landroid/graphics/Shader;
4839+
public synthetic fun <init> (Lkotlin/jvm/internal/DefaultConstructorMarker;)V
48424840
}
48434841

48444842
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$Companion {
48454843
public final fun parse (Lcom/facebook/react/bridge/ReadableMap;Landroid/content/Context;)Lcom/facebook/react/uimanager/style/BackgroundImageLayer;
48464844
}
48474845

4846+
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$GradientLayer : com/facebook/react/uimanager/style/BackgroundImageLayer {
4847+
public final fun getShader (FF)Landroid/graphics/Shader;
4848+
}
4849+
4850+
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$URLImageLayer : com/facebook/react/uimanager/style/BackgroundImageLayer {
4851+
public final fun getIntrinsicHeight ()Ljava/lang/Float;
4852+
public final fun getIntrinsicWidth ()Ljava/lang/Float;
4853+
public final fun getUri ()Ljava/lang/String;
4854+
public fun <init> (Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Float;)V
4855+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
4856+
}
4857+
48484858
public final class com/facebook/react/uimanager/style/BorderRadiusProp : java/lang/Enum {
48494859
public static final field BORDER_BOTTOM_END_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
48504860
public static final field BORDER_BOTTOM_LEFT_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundImageDrawable.kt‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.facebook.react.uimanager.drawable
99

1010
import android.content.Context
11+
import android.graphics.Bitmap
1112
import android.graphics.Canvas
1213
import android.graphics.ColorFilter
1314
import android.graphics.Paint
@@ -45,11 +46,13 @@ internal class BackgroundImageDrawable(
4546
private var backgroundImageClipPath: Path? = null
4647
private var backgroundPositioningArea: RectF? = null
4748
private var backgroundPaintingArea: RectF? = null
49+
private val urlImageLoader = BackgroundImageURLLoader(context)
4850

4951
var backgroundImageLayers: List<BackgroundImageLayer>? = null
5052
set(value) {
5153
if (field != value) {
5254
field = value
55+
loadUrlImages(value)
5356
invalidateSelf()
5457
}
5558
}
@@ -111,7 +114,7 @@ internal class BackgroundImageDrawable(
111114
}
112115

113116
override fun draw(canvas: Canvas) {
114-
if (backgroundImageLayers == null || backgroundImageLayers?.isEmpty() == true) {
117+
if (backgroundImageLayers.isNullOrEmpty()) {
115118
return
116119
}
117120

@@ -140,13 +143,40 @@ internal class BackgroundImageDrawable(
140143
val position =
141144
backgroundPosition?.takeIf { it.isNotEmpty() }?.let { it.getOrNull(index % it.size) }
142145

146+
val urlBitmap: Bitmap?
147+
val (intrinsicWidth, intrinsicHeight) =
148+
when (backgroundImageLayer) {
149+
is BackgroundImageLayer.GradientLayer -> {
150+
urlBitmap = null
151+
backgroundPositioningArea.width() to backgroundPositioningArea.height()
152+
}
153+
is BackgroundImageLayer.URLImageLayer -> {
154+
val bitmap = urlImageLoader.loadedBitmapForUri(backgroundImageLayer.uri)
155+
if (bitmap == null) {
156+
continue
157+
}
158+
urlBitmap = bitmap
159+
// Asset (local require() assets) pass intrinsicWidth/Height in DIPs
160+
// So use those dimensions and convert from dp to physical pixels.
161+
val assetWidth = backgroundImageLayer.intrinsicWidth
162+
val assetHeight = backgroundImageLayer.intrinsicHeight
163+
if (assetWidth != null && assetHeight != null) {
164+
assetWidth.dpToPx() to assetHeight.dpToPx()
165+
} else {
166+
// Plain url() images don't pass intrinsic width/height
167+
// so take the decoded pixels as DIPs and convert
168+
bitmap.width.toFloat().dpToPx() to bitmap.height.toFloat().dpToPx()
169+
}
170+
}
171+
}
172+
143173
// 2. Calculate the size of a single tile.
144174
val (tileWidth, tileHeight) =
145175
calculateBackgroundImageSize(
146176
backgroundPositioningArea.width(),
147177
backgroundPositioningArea.height(),
148-
backgroundPositioningArea.width(),
149-
backgroundPositioningArea.height(),
178+
intrinsicWidth,
179+
intrinsicHeight,
150180
size,
151181
repeat,
152182
)
@@ -155,8 +185,12 @@ internal class BackgroundImageDrawable(
155185
continue
156186
}
157187

158-
// 3. Set paint shader
159-
backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight))
188+
// 3. Set paint shader for gradients (URL images don't use shaders)
189+
if (backgroundImageLayer is BackgroundImageLayer.GradientLayer) {
190+
backgroundPaint.setShader(backgroundImageLayer.getShader(tileWidth, tileHeight))
191+
} else {
192+
backgroundPaint.setShader(null)
193+
}
160194

161195
// 4. Calculate spacing, x and y tiles count and position for tiles
162196
var (initialX, initialY) = calculateBackgroundPosition(tileWidth, tileHeight, position)
@@ -255,7 +289,13 @@ internal class BackgroundImageDrawable(
255289
repeat(yTilesCount) {
256290
canvas.save()
257291
canvas.translate(translateX, translateY)
258-
canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint)
292+
if (urlBitmap != null) {
293+
val srcRect = Rect(0, 0, urlBitmap.width, urlBitmap.height)
294+
val dstRect = RectF(0f, 0f, tileWidth, tileHeight)
295+
canvas.drawBitmap(urlBitmap, srcRect, dstRect, backgroundPaint)
296+
} else {
297+
canvas.drawRect(0f, 0f, tileWidth, tileHeight, backgroundPaint)
298+
}
259299
canvas.restore()
260300
translateY += tileHeight + ySpacing
261301
}
@@ -414,4 +454,14 @@ internal class BackgroundImageDrawable(
414454

415455
return translateX to translateY
416456
}
457+
458+
private fun loadUrlImages(layers: List<BackgroundImageLayer>?) {
459+
val uris = layers?.filterIsInstance<BackgroundImageLayer.URLImageLayer>()?.map { it.uri }
460+
if (uris.isNullOrEmpty()) {
461+
urlImageLoader.cancelAllRequests()
462+
return
463+
}
464+
465+
urlImageLoader.loadImages(uris) { invalidateSelf() }
466+
}
417467
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.content.Context
11+
import android.graphics.Bitmap
12+
import android.net.Uri
13+
import com.facebook.common.executors.CallerThreadExecutor
14+
import com.facebook.common.references.CloseableReference
15+
import com.facebook.datasource.DataSource
16+
import com.facebook.drawee.backends.pipeline.Fresco
17+
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
18+
import com.facebook.imagepipeline.image.CloseableImage
19+
import com.facebook.imagepipeline.request.ImageRequestBuilder
20+
import com.facebook.react.bridge.ReactContext
21+
import com.facebook.react.bridge.UiThreadUtil
22+
import com.facebook.react.util.RNLog
23+
import java.util.concurrent.ConcurrentHashMap
24+
import java.util.concurrent.atomic.AtomicInteger
25+
26+
internal class BackgroundImageURLLoader(context: Context) {
27+
private val reactContext: ReactContext? = context as? ReactContext
28+
29+
private val pendingRequests =
30+
ConcurrentHashMap<String, DataSource<CloseableReference<CloseableImage>>>()
31+
private val loadedBitmaps = ConcurrentHashMap<String, Bitmap>()
32+
private val outstandingRequests = AtomicInteger(0)
33+
private var onComplete: (() -> Unit)? = null
34+
35+
fun loadImages(
36+
uris: List<String>,
37+
onComplete: () -> Unit
38+
) {
39+
cancelAllRequests()
40+
41+
val distinctUris = uris.distinct()
42+
if (distinctUris.isEmpty()) {
43+
onComplete()
44+
return
45+
}
46+
47+
this.onComplete = onComplete
48+
outstandingRequests.set(distinctUris.size)
49+
for (uri in distinctUris) {
50+
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri)).build()
51+
val imagePipeline = Fresco.getImagePipeline()
52+
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
53+
54+
pendingRequests[uri] = dataSource
55+
56+
dataSource.subscribe(
57+
object : BaseBitmapDataSubscriber() {
58+
override fun onNewResultImpl(bitmap: Bitmap?) {
59+
if (bitmap != null) {
60+
val copiedBitmap = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false)
61+
if (copiedBitmap != null) {
62+
loadedBitmaps[uri] = copiedBitmap
63+
} else {
64+
RNLog.w(reactContext, "Could not copy bitmap for background image: $uri")
65+
}
66+
}
67+
onRequestComplete(uri)
68+
}
69+
70+
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
71+
RNLog.w(reactContext, "Failed to load background image: $uri - ${dataSource.failureCause}")
72+
onRequestComplete(uri)
73+
}
74+
},
75+
CallerThreadExecutor.getInstance()
76+
)
77+
}
78+
}
79+
80+
fun loadedBitmapForUri(uri: String): Bitmap? = loadedBitmaps[uri]
81+
82+
private fun onRequestComplete(uri: String) {
83+
pendingRequests.remove(uri)
84+
if (outstandingRequests.decrementAndGet() == 0) {
85+
UiThreadUtil.runOnUiThread { onComplete?.invoke() }
86+
}
87+
}
88+
89+
fun cancelAllRequests() {
90+
for (dataSource in pendingRequests.values) {
91+
dataSource.close()
92+
}
93+
pendingRequests.clear()
94+
loadedBitmaps.clear()
95+
outstandingRequests.set(0)
96+
onComplete = null
97+
}
98+
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt‎

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,60 +13,89 @@ import com.facebook.react.bridge.ReadableMap
1313
import com.facebook.react.bridge.ReadableType
1414

1515
/**
16-
* Represents a single layer of a background image, typically containing a gradient.
16+
* Represents a single layer of a background image, either a gradient or an image loaded from a URL.
1717
*
18-
* This class encapsulates gradient definitions (linear or radial) that can be applied as background
19-
* layers to React Native views. It provides parsing from React Native bridge data and shader
20-
* generation for rendering.
18+
* This class encapsulates the background image definitions (linear gradient, radial gradient, or
19+
* `url()`) that can be applied as background layers to React Native views. It provides parsing from
20+
* React Native bridge data and shader generation for rendering.
2121
*
2222
* @see LinearGradient
2323
* @see RadialGradient
2424
*/
25-
public class BackgroundImageLayer() {
26-
private lateinit var gradient: Gradient
27-
28-
private constructor(gradient: Gradient) : this() {
29-
this.gradient = gradient
25+
public sealed class BackgroundImageLayer {
26+
/** A layer rendered from a linear or radial gradient. */
27+
public class GradientLayer internal constructor(private val gradient: Gradient) :
28+
BackgroundImageLayer() {
29+
/**
30+
* Creates a shader for rendering this background layer.
31+
*
32+
* @param width The width of the area to fill
33+
* @param height The height of the area to fill
34+
* @return A Shader instance for rendering the gradient
35+
*/
36+
public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height)
3037
}
3138

39+
/**
40+
* A layer rendered from an image fetched from [uri].
41+
*
42+
* @param uri The source URI of the image to draw
43+
* @param intrinsicWidth Natural width in DIPs, or null
44+
* @param intrinsicHeight Natural height in DIPs, or null
45+
*/
46+
public class URLImageLayer(
47+
public val uri: String,
48+
public val intrinsicWidth: Float? = null,
49+
public val intrinsicHeight: Float? = null,
50+
) : BackgroundImageLayer()
51+
3252
public companion object {
3353
/**
3454
* Parses a ReadableMap into a BackgroundImageLayer.
3555
*
36-
* The map should contain gradient configuration including a "type" key specifying either
37-
* "linear-gradient" or "radial-gradient".
56+
* The map should contain a "type" key specifying either "linear-gradient", "radial-gradient",
57+
* or "url".
3858
*
39-
* @param gradientMap The map containing gradient configuration
59+
* @param backgroundImageMap The map containing the background image configuration
4060
* @param context Android context for resource resolution
4161
* @return A BackgroundImageLayer instance, or null if parsing fails
4262
*/
43-
public fun parse(gradientMap: ReadableMap?, context: Context): BackgroundImageLayer? {
44-
if (gradientMap == null) {
63+
public fun parse(backgroundImageMap: ReadableMap?, context: Context): BackgroundImageLayer? {
64+
if (backgroundImageMap == null) {
4565
return null
4666
}
47-
val gradient = parseGradient(gradientMap, context) ?: return null
48-
return BackgroundImageLayer(gradient)
49-
}
5067

51-
private fun parseGradient(gradientMap: ReadableMap, context: Context): Gradient? {
52-
if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) {
68+
if (!backgroundImageMap.hasKey("type") ||
69+
backgroundImageMap.getType("type") != ReadableType.String) {
5370
return null
5471
}
5572

56-
return when (gradientMap.getString("type")) {
57-
"linear-gradient" -> LinearGradient.parse(gradientMap, context)
58-
"radial-gradient" -> RadialGradient.parse(gradientMap, context)
73+
return when (backgroundImageMap.getString("type")) {
74+
"linear-gradient" -> {
75+
val gradient = LinearGradient.parse(backgroundImageMap, context) ?: return null
76+
GradientLayer(gradient)
77+
}
78+
"radial-gradient" -> {
79+
val gradient = RadialGradient.parse(backgroundImageMap, context) ?: return null
80+
GradientLayer(gradient)
81+
}
82+
"url" -> {
83+
val uri = backgroundImageMap.getString("uri") ?: return null
84+
URLImageLayer(
85+
uri,
86+
readDimension(backgroundImageMap, "intrinsicWidth"),
87+
readDimension(backgroundImageMap, "intrinsicHeight"),
88+
)
89+
}
5990
else -> null
6091
}
6192
}
62-
}
6393

64-
/**
65-
* Creates a shader for rendering this background layer.
66-
*
67-
* @param width The width of the area to fill
68-
* @param height The height of the area to fill
69-
* @return A Shader instance for rendering the gradient
70-
*/
71-
public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height)
94+
private fun readDimension(map: ReadableMap, key: String): Float? =
95+
if (map.hasKey(key) && map.getType(key) == ReadableType.Number) {
96+
map.getDouble(key).toFloat()
97+
} else {
98+
null
99+
}
100+
}
72101
}

0 commit comments

Comments
 (0)