Skip to content

Commit

Permalink
Add CompositeBackgroundDrawable and BackgroundStyleApplicator (#45688)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #45688

Box shadows are handled as part of different drawables. We have other cases where we want to show multiple drawables at once, such as for ripple feedback, or more commonly, for app-wide TextInput styles (which adds padding).

With more multi-background scenarios in the future, and CSSBackgroundDrawable already way overloaded, the arch here I want to go towards is less drawables, as hidden implementation details, with single responsibilities, more often switched out. Once path logic is extracted, this would also allow for better fast-paths, like not needing to create a (heavy) CSSBackgroundDrawable, for simple views with a color background.

`CompositeBackgroundDrawable` is then a more structured LayerDrawable, which also lets us mutate or retrieve information from specific layers, and enforces the different types of layers are correctly z-ordered.

`BackgroundStyleApplicator` is the public API for manipulating these styles, inspired by the existing `ReactViewBackgroundManager`. There are some important design differences.

1. The only per-view state is the publicly accessible background drawable. This means the applicator can be used on arbitrary views, and eventually used in BaseViewManager for all views (once all the QEs settle)
2. We have reliable accessors for every setter, which seem to be what folks use externally for animation
3. We work consistently in CSS device independent pixels (for the most part...)
4. More structure/safety in how we refer to edges vs uniform
5. Overflow state is not kept on the applicator, so views can set/keep their own defaults

Overflow clipping must still be implemented per-view, during drawing unfortunately.

Changelog:
[Android][Added] - Add BackgroundStyleApplicator for managing view backgrounds

Reviewed By: joevilches

Differential Revision: D60252279

fbshipit-source-id: 4c6da3e128d4da94f35d50c30c7c412cb513cc12
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Jul 29, 2024
1 parent 838d26d commit 1a78477
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 0 deletions.
16 changes: 16 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3995,6 +3995,22 @@ public abstract interface class com/facebook/react/turbomodule/core/interfaces/T
public abstract fun getBindingsInstaller ()Lcom/facebook/react/turbomodule/core/interfaces/BindingsInstallerHolder;
}

public final class com/facebook/react/uimanager/BackgroundStyleApplicator {
public static final field INSTANCE Lcom/facebook/react/uimanager/BackgroundStyleApplicator;
public static final fun clipToPaddingBox (Landroid/view/View;Landroid/graphics/Canvas;)V
public static final fun getBackgroundColor (Landroid/view/View;)Ljava/lang/Integer;
public static final fun getBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Integer;
public static final fun getBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Lcom/facebook/react/uimanager/LengthPercentage;
public static final fun getBorderStyle (Landroid/view/View;)Lcom/facebook/react/uimanager/style/BorderStyle;
public static final fun getBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Float;
public static final fun setBackgroundColor (Landroid/view/View;Ljava/lang/Integer;)V
public static final fun setBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Integer;)V
public static final fun setBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V
public static final fun setBorderStyle (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderStyle;)V
public static final fun setBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Float;)V
public static final fun setBoxShadow (Landroid/view/View;Ljava/util/List;)V
}

public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener, com/facebook/react/uimanager/BaseViewManagerInterface {
public fun <init> ()V
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.RequiresApi
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable
import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable
import com.facebook.react.uimanager.style.BorderRadiusProp
import com.facebook.react.uimanager.style.BorderStyle
import com.facebook.react.uimanager.style.BoxShadow
import com.facebook.react.uimanager.style.LogicalEdge

/**
* BackgroundStyleApplicator is responsible for applying backgrounds, borders, and related effects,
* to an Android view
*/
@OptIn(UnstableReactNativeAPI::class)
public object BackgroundStyleApplicator {

@JvmStatic
public fun setBackgroundColor(view: View, @ColorInt color: Int?): Unit {
// No color to set, and no color already set
if ((color == null || color == Color.TRANSPARENT) &&
view.background !is CompositeBackgroundDrawable) {
return
}

ensureCSSBackground(view).color = color ?: Color.TRANSPARENT
}

@JvmStatic
@ColorInt
public fun getBackgroundColor(view: View): Int? = getCSSBackground(view)?.color

@JvmStatic
public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit =
ensureCSSBackground(view)
.setBorderWidth(edge.toSpacingType(), PixelUtil.toPixelFromDIP(width ?: Float.NaN))

@JvmStatic
public fun getBorderWidth(view: View, edge: LogicalEdge): Float? {
val width = getCSSBackground(view)?.getBorderWidth(edge.toSpacingType())
return if (width == null || width.isNaN()) null else PixelUtil.toDIPFromPixel((width))
}

@JvmStatic
public fun setBorderColor(view: View, edge: LogicalEdge, @ColorInt color: Int?): Unit =
ensureCSSBackground(view).setBorderColor(edge.toSpacingType(), color)

@JvmStatic
@ColorInt
public fun getBorderColor(view: View, edge: LogicalEdge): Int? =
getCSSBackground(view)?.getBorderColor(edge.toSpacingType())

@JvmStatic
public fun setBorderRadius(
view: View,
corner: BorderRadiusProp,
// TODO: LengthPercentage silently converts from pixels to DIPs before here already
radius: LengthPercentage?
): Unit = ensureCSSBackground(view).setBorderRadius(corner, radius)

@JvmStatic
public fun getBorderRadius(view: View, corner: BorderRadiusProp): LengthPercentage? =
getCSSBackground(view)?.borderRadius?.get(corner)

@JvmStatic
public fun setBorderStyle(view: View, borderStyle: BorderStyle?): Unit {
ensureCSSBackground(view).borderStyle = borderStyle
}

@JvmStatic
public fun getBorderStyle(view: View): BorderStyle? = getCSSBackground(view)?.borderStyle

@JvmStatic
@RequiresApi(31)
public fun setBoxShadow(view: View, shadows: List<BoxShadow>): Unit {
val shadowDrawables =
shadows.map { boxShadow ->
val offsetX = boxShadow.offsetX
val offsetY = boxShadow.offsetY
val color = boxShadow.color ?: Color.BLACK
val blurRadius = boxShadow.blurRadius ?: 0f
val spreadDistance = boxShadow.spreadDistance ?: 0f
val inset = boxShadow.inset ?: false

if (inset) {
InsetBoxShadowDrawable(
context = view.context,
background = ensureCSSBackground(view),
shadowColor = color,
offsetX = offsetX,
offsetY = offsetY,
blurRadius = blurRadius,
spread = spreadDistance)
} else {
OutsetBoxShadowDrawable(
context = view.context,
background = ensureCSSBackground(view),
shadowColor = color,
offsetX = offsetX,
offsetY = offsetY,
blurRadius = blurRadius,
spread = spreadDistance)
}
}

view.background = ensureCompositeBackgroundDrawable(view).withNewShadows(shadowDrawables)
}

@JvmStatic
public fun clipToPaddingBox(view: View, canvas: Canvas): Unit {
// The canvas may be scrolled, so we need to offset
val drawingRect = Rect()
view.getDrawingRect(drawingRect)

val cssBackground = getCSSBackground(view)
if (cssBackground == null) {
canvas.clipRect(drawingRect)
return
}

val paddingBoxPath = cssBackground.paddingBoxPath
if (paddingBoxPath != null) {
paddingBoxPath.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
canvas.clipPath(paddingBoxPath)
} else {
val paddingBoxRect = cssBackground.paddingBoxRect
paddingBoxRect.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
canvas.clipRect(paddingBoxRect)
}
}

private fun ensureCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable {
if (view.background is CompositeBackgroundDrawable) {
return view.background as CompositeBackgroundDrawable
}

val compositeDrawable = CompositeBackgroundDrawable(view.background, null, emptyList(), null)
view.background = compositeDrawable
return compositeDrawable
}

private fun ensureCSSBackground(view: View): CSSBackgroundDrawable {
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
if (compositeBackgroundDrawable.cssBackground != null) {
return compositeBackgroundDrawable.cssBackground
} else {
val cssBackground = CSSBackgroundDrawable(view.context)
view.background = compositeBackgroundDrawable.withNewCssBackground(cssBackground)
return cssBackground
}
}

private fun getCSSBackground(view: View): CSSBackgroundDrawable? {
if (view.background is CompositeBackgroundDrawable) {
return (view.background as CompositeBackgroundDrawable).cssBackground
}
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import com.facebook.react.common.annotations.UnstableReactNativeAPI

/**
* CompositeBackgroundDrawable can overlay multiple different layers, shadows, and native effects
* such as ripple, into an Android View's background drawable.
*/
@OptIn(UnstableReactNativeAPI::class)
internal class CompositeBackgroundDrawable(
/**
* Any non-react-managed background already part of the view, like one set as Android style on a
* TextInput
*/
public val originalBackground: Drawable? = null,

/**
* CSS background layer and border rendering
*
* TODO: we should extract path logic from here, and fast-path to using simpler drawables like
* ColorDrawable in the common cases
*/
public val cssBackground: CSSBackgroundDrawable? = null,

/** Inner and outer box shadows */
public val shadows: List<Drawable> = emptyList(),

/** Native riplple effect (e.g. used by TouchableNativeFeedback) */
public val nativeRipple: Drawable? = null
) :
LayerDrawable(
listOfNotNull(
originalBackground,
cssBackground,
// z-ordering of user-provided shadow-list is opposite direction of LayerDrawable
// z-ordering
// https://drafts.csswg.org/css-backgrounds/#shadow-layers
*shadows.asReversed().toTypedArray(),
nativeRipple)
.toTypedArray()) {

init {
// We want to overlay drawables, instead of placing future drawables within the content area of
// previous ones. E.g. an EditText style may set padding on a TextInput, but we don't want to
// constrain background color to the area inside of the padding.
setPaddingMode(LayerDrawable.PADDING_MODE_STACK)
}

public fun withNewCssBackground(
cssBackground: CSSBackgroundDrawable?
): CompositeBackgroundDrawable {
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, nativeRipple)
}

public fun withNewShadows(newShadows: List<Drawable>): CompositeBackgroundDrawable {
return CompositeBackgroundDrawable(originalBackground, cssBackground, newShadows, nativeRipple)
}

public fun withNewNativeRipple(newRipple: Drawable?): CompositeBackgroundDrawable {
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, newRipple)
}
}

0 comments on commit 1a78477

Please sign in to comment.