Skip to content
Draft
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
19 changes: 15 additions & 4 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -4834,17 +4834,28 @@ public final class com/facebook/react/uimanager/events/TouchEventType$Companion
public final fun getJSEventName (Lcom/facebook/react/uimanager/events/TouchEventType;)Ljava/lang/String;
}

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

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

public final class com/facebook/react/uimanager/style/BackgroundImageLayer$GradientLayer : com/facebook/react/uimanager/style/BackgroundImageLayer {
public final fun getShader (FF)Landroid/graphics/Shader;
}

public final class com/facebook/react/uimanager/style/BackgroundImageLayer$URLImageLayer : com/facebook/react/uimanager/style/BackgroundImageLayer {
public fun <init> (Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Float;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getIntrinsicHeight ()Ljava/lang/Float;
public final fun getIntrinsicWidth ()Ljava/lang/Float;
public final fun getUri ()Ljava/lang/String;
}

public final class com/facebook/react/uimanager/style/BorderRadiusProp : java/lang/Enum {
public static final field BORDER_BOTTOM_END_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
public static final field BORDER_BOTTOM_LEFT_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(context)

var backgroundImageLayers: List<BackgroundImageLayer>? = null
set(value) {
if (field != value) {
field = value
loadUrlImages(value)
invalidateSelf()
}
}
Expand Down Expand Up @@ -111,7 +114,7 @@ internal class BackgroundImageDrawable(
}

override fun draw(canvas: Canvas) {
if (backgroundImageLayers == null || backgroundImageLayers?.isEmpty() == true) {
if (backgroundImageLayers.isNullOrEmpty()) {
return
}

Expand Down Expand Up @@ -140,13 +143,40 @@ internal class BackgroundImageDrawable(
val position =
backgroundPosition?.takeIf { it.isNotEmpty() }?.let { it.getOrNull(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
// Asset (local require() assets) pass intrinsicWidth/Height in DIPs
// So use those dimensions and convert from dp to physical pixels.
val assetWidth = backgroundImageLayer.intrinsicWidth
val assetHeight = backgroundImageLayer.intrinsicHeight
if (assetWidth != null && assetHeight != null) {
assetWidth.dpToPx() to assetHeight.dpToPx()
} else {
// Plain url() images don't pass intrinsic width/height
// so take the decoded pixels as DIPs and convert
bitmap.width.toFloat().dpToPx() to bitmap.height.toFloat().dpToPx()
}
}
}

// 2. Calculate the size of a single tile.
val (tileWidth, tileHeight) =
calculateBackgroundImageSize(
backgroundPositioningArea.width(),
backgroundPositioningArea.height(),
backgroundPositioningArea.width(),
backgroundPositioningArea.height(),
intrinsicWidth,
intrinsicHeight,
size,
repeat,
)
Expand All @@ -155,8 +185,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)
Expand Down Expand Up @@ -255,7 +289,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
}
Expand Down Expand Up @@ -414,4 +454,14 @@ internal class BackgroundImageDrawable(

return translateX to translateY
}

private fun loadUrlImages(layers: List<BackgroundImageLayer>?) {
val uris = layers?.filterIsInstance<BackgroundImageLayer.URLImageLayer>()?.map { it.uri }
if (uris.isNullOrEmpty()) {
urlImageLoader.cancelAllRequests()
return
}

urlImageLoader.loadImages(uris) { invalidateSelf() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.content.Context
import android.graphics.Bitmap
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 com.facebook.react.common.ReactConstants
import com.facebook.react.views.imagehelper.ImageSource
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

internal class BackgroundImageURLLoader(private val context: Context) {

private val pendingRequests =
ConcurrentHashMap<String, DataSource<CloseableReference<CloseableImage>>>()
private val loadedBitmaps = ConcurrentHashMap<String, Bitmap>()
private val outstandingRequests = AtomicInteger(0)
private var onComplete: (() -> Unit)? = null
private var requestedUris: List<String> = emptyList()

fun loadImages(
uris: List<String>,
onComplete: () -> Unit
) {
val distinctUris = uris.distinct()
if (distinctUris == requestedUris) {
return
}

cancelAllRequests()

if (distinctUris.isEmpty()) {
onComplete()
return
}

requestedUris = distinctUris
this.onComplete = onComplete
outstandingRequests.set(distinctUris.size)
for (uri in distinctUris) {
val imageRequest =
ImageRequestBuilder.newBuilderWithSource(ImageSource(context, uri).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) {
val copiedBitmap = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false)
if (copiedBitmap != null) {
loadedBitmaps[uri] = copiedBitmap
} else {
FLog.w(ReactConstants.TAG, "Could not copy bitmap for background image: %s", uri)
}
}
onRequestComplete(uri)
}

override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
FLog.w(
ReactConstants.TAG,
dataSource.failureCause,
"Failed to load background image: %s",
uri)
onRequestComplete(uri)
}
},
CallerThreadExecutor.getInstance()
)
}
}

fun loadedBitmapForUri(uri: String): Bitmap? = loadedBitmaps[uri]

private fun onRequestComplete(uri: String) {
pendingRequests.remove(uri)
if (outstandingRequests.decrementAndGet() == 0) {
UiThreadUtil.runOnUiThread { onComplete?.invoke() }
}
}

fun cancelAllRequests() {
for (dataSource in pendingRequests.values) {
dataSource.close()
}
pendingRequests.clear()
loadedBitmaps.clear()
outstandingRequests.set(0)
onComplete = null
requestedUris = emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,95 @@ import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType

/**
* Represents a single layer of a background image, typically containing a gradient.
* Represents a single layer of a background image, either a gradient or an image loaded from a URL.
*
* This class encapsulates gradient definitions (linear or radial) that can be applied as background
* layers to React Native views. It provides parsing from React Native bridge data and shader
* generation for rendering.
* This class encapsulates the background image definitions (linear gradient, radial gradient, or
* `url()`) that can be applied as background layers to React Native views. It provides parsing from
* React Native bridge data and shader generation for rendering.
*
* @see LinearGradient
* @see RadialGradient
*/
public class BackgroundImageLayer() {
private lateinit var gradient: Gradient
public sealed class BackgroundImageLayer {
/** A layer rendered from a linear or radial gradient. */
public class GradientLayer internal constructor(private val gradient: Gradient) :
BackgroundImageLayer() {
/**
* Creates a shader for rendering this background layer.
*
* @param width The width of the area to fill
* @param height The height of the area to fill
* @return A Shader instance for rendering the gradient
*/
public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height)
}

private constructor(gradient: Gradient) : this() {
this.gradient = gradient
/**
* A layer rendered from an image fetched from [uri].
*
* @param uri The source URI of the image to draw
* @param intrinsicWidth Natural width in DIPs, or null
* @param intrinsicHeight Natural height in DIPs, or null
*/
public class URLImageLayer(
public val uri: String,
public val intrinsicWidth: Float? = null,
public val intrinsicHeight: Float? = null,
) : BackgroundImageLayer() {
override fun equals(other: Any?): Boolean =
other is URLImageLayer &&
uri == other.uri &&
intrinsicWidth == other.intrinsicWidth &&
intrinsicHeight == other.intrinsicHeight
}

public companion object {
/**
* Parses a ReadableMap into a BackgroundImageLayer.
*
* The map should contain gradient configuration including a "type" key specifying either
* "linear-gradient" or "radial-gradient".
* The map should contain a "type" key specifying either "linear-gradient", "radial-gradient",
* or "url".
*
* @param gradientMap The map containing gradient configuration
* @param backgroundImageMap The map containing the background image configuration
* @param context Android context for resource resolution
* @return A BackgroundImageLayer instance, or null if parsing fails
*/
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,
readDimension(backgroundImageMap, "intrinsicWidth"),
readDimension(backgroundImageMap, "intrinsicHeight"),
)
}
else -> null
}
}
}

/**
* Creates a shader for rendering this background layer.
*
* @param width The width of the area to fill
* @param height The height of the area to fill
* @return A Shader instance for rendering the gradient
*/
public fun getShader(width: Float, height: Float): Shader = gradient.getShader(width, height)
private fun readDimension(map: ReadableMap, key: String): Float? =
if (map.hasKey(key) && map.getType(key) == ReadableType.Number) {
map.getDouble(key).toFloat()
} else {
null
}
}
}
Loading