Add BackgroundDrawable to replace background logic on CSSBackgroundDrawable (#46709)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/46709

**Note:** This diff still does nothing yet, it will be enabled on a diff further up the stack. This split is just to simplify reviewing

`CSSBackgroundDrawable` holds the drawing logic for both **Borders** and **Background**. This is not ideal since it results in a huge file which does 2 things.

We now have `CompositeBackgroundDrawable` which allows us to set a different drawable per "layer" on our view.

By splitting up **Border** and **Background** logic we get better modularity and it'll make it easier to implement more `backgroundImage` features without further bloating the `CSSBackgroundDrawable` file

Also, this helps  with the kotlinification efforts

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D63137921

fbshipit-source-id: fc459beadfda1832e146e36d74c3d424a28ca905
This commit is contained in:
Jorge Cabiedes Acosta
2024-10-21 21:24:19 -07:00
committed by Facebook GitHub Bot
parent 6205aad81e
commit f41c59da96
3 changed files with 266 additions and 1 deletions
@@ -6149,6 +6149,7 @@ public final class com/facebook/react/uimanager/style/ComputedBorderRadius {
public final fun getTopRight ()Lcom/facebook/react/uimanager/style/CornerRadii;
public final fun hasRoundedBorders ()Z
public fun hashCode ()I
public final fun isUniform ()Z
public fun toString ()Ljava/lang/String;
}
@@ -0,0 +1,260 @@
/*
* 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.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.ComposeShader
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.Drawable
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel
import com.facebook.react.uimanager.style.BackgroundImageLayer
import com.facebook.react.uimanager.style.BorderInsets
import com.facebook.react.uimanager.style.BorderRadiusStyle
import com.facebook.react.uimanager.style.ComputedBorderRadius
import kotlin.math.roundToInt
internal class BackgroundDrawable(
private val context: Context,
/*
* We assume borderRadius & borderInsets to be shared across multiple drawables
* therefore we should manually invalidate this drawable when changing either of them
*/
var borderRadius: BorderRadiusStyle? = null,
var borderInsets: BorderInsets? = null,
) : Drawable() {
/**
* There is a small gap between the edges of adjacent paths, such as between its Border and its
* Outline. The smallest amount (found to be 0.8f) is used to shrink outline's path, overlapping
* them and closing the visible gap.
*/
private val gapBetweenPaths = 0.8f
private var computedBorderInsets: RectF? = null
private var computedBorderRadius: ComputedBorderRadius? = null
private var needUpdatePath = true
var backgroundColor: Int = Color.TRANSPARENT
set(value) {
if (field != value) {
field = value
backgroundPaint.color = value
invalidateSelf()
}
}
private var paddingBoxRect: RectF = RectF()
private var paddingBoxRenderPath: Path? = null
var backgroundImageLayers: List<BackgroundImageLayer>? = null
set(value) {
if (field != value) {
field = value
invalidateSelf()
}
}
private val backgroundPaint: Paint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = backgroundColor
}
override fun invalidateSelf() {
needUpdatePath = true
super.invalidateSelf()
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
needUpdatePath = true
}
override fun setAlpha(alpha: Int) {
backgroundPaint.alpha =
(((alpha / 255f) * (Color.alpha(backgroundColor) / 255f)) * 255f).roundToInt()
invalidateSelf()
}
override fun setColorFilter(colorFilter: ColorFilter?) {
// do nothing
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
val alpha = backgroundPaint.alpha
return when (alpha) {
255 -> PixelFormat.OPAQUE
in 1..254 -> PixelFormat.TRANSLUCENT
else -> PixelFormat.TRANSPARENT
}
}
override fun draw(canvas: Canvas) {
updatePath()
canvas.save()
var innerRadiusX = 0f
var innerRadiusY = 0f
// Draws the View without its border first (with background color fill)
if (backgroundPaint.alpha != 0) {
if (computedBorderRadius?.isUniform() == true && borderRadius?.hasRoundedBorders() == true) {
innerRadiusX =
getInnerBorderRadius(
computedBorderRadius?.topLeft?.horizontal?.dpToPx(), computedBorderInsets?.left)
innerRadiusY =
getInnerBorderRadius(
computedBorderRadius?.topLeft?.vertical?.dpToPx(), computedBorderInsets?.top)
canvas.drawRoundRect(paddingBoxRect, innerRadiusX, innerRadiusY, backgroundPaint)
} else if (borderRadius?.hasRoundedBorders() != true) {
canvas.drawRect(bounds, backgroundPaint)
} else {
canvas.drawPath(checkNotNull(paddingBoxRenderPath), backgroundPaint)
}
}
if (backgroundImageLayers != null && backgroundImageLayers?.isNotEmpty() == true) {
backgroundPaint.setShader(getBackgroundImageShader())
if (computedBorderRadius?.isUniform() == true && borderRadius?.hasRoundedBorders() == true) {
canvas.drawRoundRect(paddingBoxRect, innerRadiusX, innerRadiusY, backgroundPaint)
} else if (borderRadius?.hasRoundedBorders() != true) {
canvas.drawRect(paddingBoxRect, backgroundPaint)
} else {
canvas.drawPath(checkNotNull(paddingBoxRenderPath), backgroundPaint)
}
backgroundPaint.setShader(null)
}
canvas.restore()
}
private fun computeBorderInsets(): RectF =
borderInsets?.resolve(layoutDirection, context).let {
RectF(
it?.left?.dpToPx() ?: 0f,
it?.top?.dpToPx() ?: 0f,
it?.right?.dpToPx() ?: 0f,
it?.bottom?.dpToPx() ?: 0f)
}
private fun getBackgroundImageShader(): Shader? {
backgroundImageLayers?.let { layers ->
var compositeShader: Shader? = null
for (backgroundImageLayer in layers) {
val currentShader = backgroundImageLayer.getShader(bounds) ?: continue
compositeShader =
if (compositeShader == null) {
currentShader
} else {
ComposeShader(currentShader, compositeShader, PorterDuff.Mode.SRC_OVER)
}
}
return compositeShader
}
return null
}
/**
* Here, "inner" refers to the border radius on the inside of the border. So it ends up being the
* "outer" border radius inset by the respective width.
*/
private fun getInnerBorderRadius(computedRadius: Float?, borderWidth: Float?): Float {
return ((computedRadius ?: 0f) - (borderWidth ?: 0f)).coerceAtLeast(0f)
}
private fun updatePath() {
if (!needUpdatePath) {
return
}
needUpdatePath = false
computedBorderInsets = computeBorderInsets()
computedBorderRadius =
borderRadius?.resolve(
layoutDirection,
context,
toDIPFromPixel(bounds.width().toFloat()),
toDIPFromPixel(bounds.height().toFloat()))
if (computedBorderRadius?.hasRoundedBorders() == true &&
computedBorderRadius?.isUniform() == false) {
paddingBoxRenderPath = paddingBoxRenderPath ?: Path()
paddingBoxRenderPath?.reset()
}
// only close gap between border and background if we draw the border, otherwise
// we wind up pixelating small pixel-radius curves
var pathAdjustment = 0f
if (computedBorderInsets != null &&
(computedBorderInsets?.left != 0f ||
computedBorderInsets?.top != 0f ||
computedBorderInsets?.right != 0f ||
computedBorderInsets?.bottom != 0f)) {
pathAdjustment = gapBetweenPaths
}
// There is a small gap between backgroundDrawable and
// borderDrawable. pathAdjustment is used to slightly enlarge the rectangle
// (paddingBoxRect), ensuring the border can be
// drawn on top without the gap.
paddingBoxRect.left = bounds.left + (computedBorderInsets?.left ?: 0f) - pathAdjustment
paddingBoxRect.top = bounds.top + (computedBorderInsets?.top ?: 0f) - pathAdjustment
paddingBoxRect.right = bounds.right - (computedBorderInsets?.right ?: 0f) + pathAdjustment
paddingBoxRect.bottom = bounds.bottom - (computedBorderInsets?.bottom ?: 0f) + pathAdjustment
if (borderRadius?.hasRoundedBorders() == true && computedBorderRadius?.isUniform() != true) {
val innerTopLeftRadiusX =
getInnerBorderRadius(
computedBorderRadius?.topLeft?.horizontal?.dpToPx(), computedBorderInsets?.left)
val innerTopLeftRadiusY =
getInnerBorderRadius(
computedBorderRadius?.topLeft?.vertical?.dpToPx(), computedBorderInsets?.top)
val innerTopRightRadiusX =
getInnerBorderRadius(
computedBorderRadius?.topRight?.horizontal?.dpToPx(), computedBorderInsets?.right)
val innerTopRightRadiusY =
getInnerBorderRadius(
computedBorderRadius?.topRight?.vertical?.dpToPx(), computedBorderInsets?.top)
val innerBottomRightRadiusX =
getInnerBorderRadius(
computedBorderRadius?.bottomRight?.horizontal?.dpToPx(), computedBorderInsets?.right)
val innerBottomRightRadiusY =
getInnerBorderRadius(
computedBorderRadius?.bottomRight?.vertical?.dpToPx(), computedBorderInsets?.bottom)
val innerBottomLeftRadiusX =
getInnerBorderRadius(
computedBorderRadius?.bottomLeft?.horizontal?.dpToPx(), computedBorderInsets?.left)
val innerBottomLeftRadiusY =
getInnerBorderRadius(
computedBorderRadius?.bottomLeft?.vertical?.dpToPx(), computedBorderInsets?.bottom)
paddingBoxRenderPath?.addRoundRect(
paddingBoxRect,
floatArrayOf(
innerTopLeftRadiusX,
innerTopLeftRadiusY,
innerTopRightRadiusX,
innerTopRightRadiusY,
innerBottomRightRadiusX,
innerBottomRightRadiusY,
innerBottomLeftRadiusX,
innerBottomLeftRadiusY,
),
Path.Direction.CW)
}
}
}
@@ -15,7 +15,7 @@ public enum class ComputedBorderRadiusProp {
COMPUTED_BORDER_BOTTOM_LEFT_RADIUS,
}
/** Phsysical edge lengths (in DIPs) for a border-radius. */
/** Physical edge lengths (in DIPs) for a border-radius. */
public data class ComputedBorderRadius(
val topLeft: CornerRadii,
val topRight: CornerRadii,
@@ -32,6 +32,10 @@ public data class ComputedBorderRadius(
bottomRight.horizontal > 0f
}
public fun isUniform(): Boolean {
return topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight
}
public fun get(property: ComputedBorderRadiusProp): CornerRadii {
return when (property) {
ComputedBorderRadiusProp.COMPUTED_BORDER_TOP_LEFT_RADIUS -> topLeft