-
Notifications
You must be signed in to change notification settings - Fork 24.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
838d26d
commit 1a78477
Showing
3 changed files
with
261 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
...tive/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
...ndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |