Skip to content
Open
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
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()

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 All @@ -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,
)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -412,4 +441,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,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<String, DataSource<CloseableReference<CloseableImage>>>()
private val loadedBitmaps = ConcurrentHashMap<String, Bitmap>()
private var onComplete: (() -> Unit)? = null

fun loadImages(
uris: List<String>,
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<CloseableReference<CloseableImage>>) {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading