Skip to content

Commit

Permalink
Add BackgroundDrawable to replace background logic on `CSSBackgroun…
Browse files Browse the repository at this point in the history
…dDrawable` (facebook#46709)

Summary:
Pull Request resolved: facebook#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
  • Loading branch information
jorge-cab authored and facebook-github-bot committed Oct 2, 2024
1 parent b3bc6ea commit 88f09ca
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6151,6 +6151,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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
* 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.PorterDuff
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.Drawable
import androidx.core.graphics.ColorUtils
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.properties.ObservableProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

internal class BackgroundDrawable(
private val context: Context,
var borderRadius: BorderRadiusStyle? = null,
var borderInsets: BorderInsets? = null,
) : Drawable() {
private fun <T> invalidatingChange(initialValue: T): ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
if (oldValue != newValue) {
invalidateSelf()
}
}
}

/**
* 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 backgroundAlpha: Int = 255
private var computedBorderInsets: RectF? = null
private var computedBorderRadius: ComputedBorderRadius? = null
private var needUpdatePath = true

var backgroundColor: Int by invalidatingChange(Color.TRANSPARENT)
var paddingBoxRect: RectF = RectF()
private set

var paddingBoxRenderPath: Path? = null
private set

var backgroundImageLayers: List<BackgroundImageLayer>? by invalidatingChange(null)

private val backgroundPaint: Paint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = backgroundColor
alpha = 255
}

override fun invalidateSelf() {
needUpdatePath = true
super.invalidateSelf()
}

override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
needUpdatePath = true
}

override fun setAlpha(alpha: Int) {
this.backgroundAlpha = alpha
invalidateSelf()
}

override fun setColorFilter(colorFilter: ColorFilter?) {
// do nothing
}

override fun getOpacity(): Int = (Color.alpha(backgroundColor) * backgroundAlpha) ushr 8

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)
val useColor = ColorUtils.setAlphaComponent(backgroundColor, opacity)
if (Color.alpha(useColor) != 0) {
backgroundPaint.color = useColor
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(paddingBoxRect, 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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit 88f09ca

Please sign in to comment.