Skip to content

Commit 1a78477

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Add CompositeBackgroundDrawable and BackgroundStyleApplicator (#45688)
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
1 parent 838d26d commit 1a78477

3 files changed

Lines changed: 261 additions & 0 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3995,6 +3995,22 @@ public abstract interface class com/facebook/react/turbomodule/core/interfaces/T
39953995
public abstract fun getBindingsInstaller ()Lcom/facebook/react/turbomodule/core/interfaces/BindingsInstallerHolder;
39963996
}
39973997

3998+
public final class com/facebook/react/uimanager/BackgroundStyleApplicator {
3999+
public static final field INSTANCE Lcom/facebook/react/uimanager/BackgroundStyleApplicator;
4000+
public static final fun clipToPaddingBox (Landroid/view/View;Landroid/graphics/Canvas;)V
4001+
public static final fun getBackgroundColor (Landroid/view/View;)Ljava/lang/Integer;
4002+
public static final fun getBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Integer;
4003+
public static final fun getBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;)Lcom/facebook/react/uimanager/LengthPercentage;
4004+
public static final fun getBorderStyle (Landroid/view/View;)Lcom/facebook/react/uimanager/style/BorderStyle;
4005+
public static final fun getBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;)Ljava/lang/Float;
4006+
public static final fun setBackgroundColor (Landroid/view/View;Ljava/lang/Integer;)V
4007+
public static final fun setBorderColor (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Integer;)V
4008+
public static final fun setBorderRadius (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderRadiusProp;Lcom/facebook/react/uimanager/LengthPercentage;)V
4009+
public static final fun setBorderStyle (Landroid/view/View;Lcom/facebook/react/uimanager/style/BorderStyle;)V
4010+
public static final fun setBorderWidth (Landroid/view/View;Lcom/facebook/react/uimanager/style/LogicalEdge;Ljava/lang/Float;)V
4011+
public static final fun setBoxShadow (Landroid/view/View;Ljava/util/List;)V
4012+
}
4013+
39984014
public abstract class com/facebook/react/uimanager/BaseViewManager : com/facebook/react/uimanager/ViewManager, android/view/View$OnLayoutChangeListener, com/facebook/react/uimanager/BaseViewManagerInterface {
39994015
public fun <init> ()V
40004016
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager
9+
10+
import android.graphics.Canvas
11+
import android.graphics.Color
12+
import android.graphics.Rect
13+
import android.view.View
14+
import androidx.annotation.ColorInt
15+
import androidx.annotation.RequiresApi
16+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
17+
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable
18+
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable
19+
import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable
20+
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable
21+
import com.facebook.react.uimanager.style.BorderRadiusProp
22+
import com.facebook.react.uimanager.style.BorderStyle
23+
import com.facebook.react.uimanager.style.BoxShadow
24+
import com.facebook.react.uimanager.style.LogicalEdge
25+
26+
/**
27+
* BackgroundStyleApplicator is responsible for applying backgrounds, borders, and related effects,
28+
* to an Android view
29+
*/
30+
@OptIn(UnstableReactNativeAPI::class)
31+
public object BackgroundStyleApplicator {
32+
33+
@JvmStatic
34+
public fun setBackgroundColor(view: View, @ColorInt color: Int?): Unit {
35+
// No color to set, and no color already set
36+
if ((color == null || color == Color.TRANSPARENT) &&
37+
view.background !is CompositeBackgroundDrawable) {
38+
return
39+
}
40+
41+
ensureCSSBackground(view).color = color ?: Color.TRANSPARENT
42+
}
43+
44+
@JvmStatic
45+
@ColorInt
46+
public fun getBackgroundColor(view: View): Int? = getCSSBackground(view)?.color
47+
48+
@JvmStatic
49+
public fun setBorderWidth(view: View, edge: LogicalEdge, width: Float?): Unit =
50+
ensureCSSBackground(view)
51+
.setBorderWidth(edge.toSpacingType(), PixelUtil.toPixelFromDIP(width ?: Float.NaN))
52+
53+
@JvmStatic
54+
public fun getBorderWidth(view: View, edge: LogicalEdge): Float? {
55+
val width = getCSSBackground(view)?.getBorderWidth(edge.toSpacingType())
56+
return if (width == null || width.isNaN()) null else PixelUtil.toDIPFromPixel((width))
57+
}
58+
59+
@JvmStatic
60+
public fun setBorderColor(view: View, edge: LogicalEdge, @ColorInt color: Int?): Unit =
61+
ensureCSSBackground(view).setBorderColor(edge.toSpacingType(), color)
62+
63+
@JvmStatic
64+
@ColorInt
65+
public fun getBorderColor(view: View, edge: LogicalEdge): Int? =
66+
getCSSBackground(view)?.getBorderColor(edge.toSpacingType())
67+
68+
@JvmStatic
69+
public fun setBorderRadius(
70+
view: View,
71+
corner: BorderRadiusProp,
72+
// TODO: LengthPercentage silently converts from pixels to DIPs before here already
73+
radius: LengthPercentage?
74+
): Unit = ensureCSSBackground(view).setBorderRadius(corner, radius)
75+
76+
@JvmStatic
77+
public fun getBorderRadius(view: View, corner: BorderRadiusProp): LengthPercentage? =
78+
getCSSBackground(view)?.borderRadius?.get(corner)
79+
80+
@JvmStatic
81+
public fun setBorderStyle(view: View, borderStyle: BorderStyle?): Unit {
82+
ensureCSSBackground(view).borderStyle = borderStyle
83+
}
84+
85+
@JvmStatic
86+
public fun getBorderStyle(view: View): BorderStyle? = getCSSBackground(view)?.borderStyle
87+
88+
@JvmStatic
89+
@RequiresApi(31)
90+
public fun setBoxShadow(view: View, shadows: List<BoxShadow>): Unit {
91+
val shadowDrawables =
92+
shadows.map { boxShadow ->
93+
val offsetX = boxShadow.offsetX
94+
val offsetY = boxShadow.offsetY
95+
val color = boxShadow.color ?: Color.BLACK
96+
val blurRadius = boxShadow.blurRadius ?: 0f
97+
val spreadDistance = boxShadow.spreadDistance ?: 0f
98+
val inset = boxShadow.inset ?: false
99+
100+
if (inset) {
101+
InsetBoxShadowDrawable(
102+
context = view.context,
103+
background = ensureCSSBackground(view),
104+
shadowColor = color,
105+
offsetX = offsetX,
106+
offsetY = offsetY,
107+
blurRadius = blurRadius,
108+
spread = spreadDistance)
109+
} else {
110+
OutsetBoxShadowDrawable(
111+
context = view.context,
112+
background = ensureCSSBackground(view),
113+
shadowColor = color,
114+
offsetX = offsetX,
115+
offsetY = offsetY,
116+
blurRadius = blurRadius,
117+
spread = spreadDistance)
118+
}
119+
}
120+
121+
view.background = ensureCompositeBackgroundDrawable(view).withNewShadows(shadowDrawables)
122+
}
123+
124+
@JvmStatic
125+
public fun clipToPaddingBox(view: View, canvas: Canvas): Unit {
126+
// The canvas may be scrolled, so we need to offset
127+
val drawingRect = Rect()
128+
view.getDrawingRect(drawingRect)
129+
130+
val cssBackground = getCSSBackground(view)
131+
if (cssBackground == null) {
132+
canvas.clipRect(drawingRect)
133+
return
134+
}
135+
136+
val paddingBoxPath = cssBackground.paddingBoxPath
137+
if (paddingBoxPath != null) {
138+
paddingBoxPath.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
139+
canvas.clipPath(paddingBoxPath)
140+
} else {
141+
val paddingBoxRect = cssBackground.paddingBoxRect
142+
paddingBoxRect.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat())
143+
canvas.clipRect(paddingBoxRect)
144+
}
145+
}
146+
147+
private fun ensureCompositeBackgroundDrawable(view: View): CompositeBackgroundDrawable {
148+
if (view.background is CompositeBackgroundDrawable) {
149+
return view.background as CompositeBackgroundDrawable
150+
}
151+
152+
val compositeDrawable = CompositeBackgroundDrawable(view.background, null, emptyList(), null)
153+
view.background = compositeDrawable
154+
return compositeDrawable
155+
}
156+
157+
private fun ensureCSSBackground(view: View): CSSBackgroundDrawable {
158+
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
159+
if (compositeBackgroundDrawable.cssBackground != null) {
160+
return compositeBackgroundDrawable.cssBackground
161+
} else {
162+
val cssBackground = CSSBackgroundDrawable(view.context)
163+
view.background = compositeBackgroundDrawable.withNewCssBackground(cssBackground)
164+
return cssBackground
165+
}
166+
}
167+
168+
private fun getCSSBackground(view: View): CSSBackgroundDrawable? {
169+
if (view.background is CompositeBackgroundDrawable) {
170+
return (view.background as CompositeBackgroundDrawable).cssBackground
171+
}
172+
return null
173+
}
174+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.drawable
9+
10+
import android.graphics.drawable.Drawable
11+
import android.graphics.drawable.LayerDrawable
12+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
13+
14+
/**
15+
* CompositeBackgroundDrawable can overlay multiple different layers, shadows, and native effects
16+
* such as ripple, into an Android View's background drawable.
17+
*/
18+
@OptIn(UnstableReactNativeAPI::class)
19+
internal class CompositeBackgroundDrawable(
20+
/**
21+
* Any non-react-managed background already part of the view, like one set as Android style on a
22+
* TextInput
23+
*/
24+
public val originalBackground: Drawable? = null,
25+
26+
/**
27+
* CSS background layer and border rendering
28+
*
29+
* TODO: we should extract path logic from here, and fast-path to using simpler drawables like
30+
* ColorDrawable in the common cases
31+
*/
32+
public val cssBackground: CSSBackgroundDrawable? = null,
33+
34+
/** Inner and outer box shadows */
35+
public val shadows: List<Drawable> = emptyList(),
36+
37+
/** Native riplple effect (e.g. used by TouchableNativeFeedback) */
38+
public val nativeRipple: Drawable? = null
39+
) :
40+
LayerDrawable(
41+
listOfNotNull(
42+
originalBackground,
43+
cssBackground,
44+
// z-ordering of user-provided shadow-list is opposite direction of LayerDrawable
45+
// z-ordering
46+
// https://drafts.csswg.org/css-backgrounds/#shadow-layers
47+
*shadows.asReversed().toTypedArray(),
48+
nativeRipple)
49+
.toTypedArray()) {
50+
51+
init {
52+
// We want to overlay drawables, instead of placing future drawables within the content area of
53+
// previous ones. E.g. an EditText style may set padding on a TextInput, but we don't want to
54+
// constrain background color to the area inside of the padding.
55+
setPaddingMode(LayerDrawable.PADDING_MODE_STACK)
56+
}
57+
58+
public fun withNewCssBackground(
59+
cssBackground: CSSBackgroundDrawable?
60+
): CompositeBackgroundDrawable {
61+
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, nativeRipple)
62+
}
63+
64+
public fun withNewShadows(newShadows: List<Drawable>): CompositeBackgroundDrawable {
65+
return CompositeBackgroundDrawable(originalBackground, cssBackground, newShadows, nativeRipple)
66+
}
67+
68+
public fun withNewNativeRipple(newRipple: Drawable?): CompositeBackgroundDrawable {
69+
return CompositeBackgroundDrawable(originalBackground, cssBackground, shadows, newRipple)
70+
}
71+
}

0 commit comments

Comments
 (0)