Skip to content

Commit

Permalink
Optimize CompositeBackgroundDrawable
Browse files Browse the repository at this point in the history
Summary:
Its not optimal to reconstruct CompositeBackgroundDrawable everytime we add a layer to it. 

With this change I'm adding an optimization to modify the underlying `LayerDrawable` in place instead of reconstructing everything. `LayerDrawable` has a pretty constrained API and some weirdish behaviors. 

- `addLayer` - This API doesnt set the callback for the recently added layer so after adding the layer we also need to manually set the callback
- Mutating `LayerDrawable` in-place is not straightforward, I had to add a function that will figure out where each layer should be inserted
- `LayerDrawable` doesn't allow deleting an element which means that for this case in particular we do need to re-create `CompositeBackgroundDrawable`
- Newer Android versions allow `null` on `LayerDrawable` layers, but older versions do not, this implementation is mostly done this way to accommodate older versions. But also, even though newer versions can have `null` set on a layer `LayerDrawable` still doesn't handle it well and we get some bugs when removing and inserting a layer

This is all feature flagged. since it will only be enabled with the new drawables

Changelog: [Internal]

Differential Revision: D65907786
  • Loading branch information
jorge-cab authored and facebook-github-bot committed Nov 14, 2024
1 parent 0ea75b3 commit 3bae7a3
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ public object BackgroundStyleApplicator {
ensureCSSBackground(view).setBorderWidth(edge.toSpacingType(), width?.dpToPx() ?: Float.NaN)
}

val composite = ensureCompositeBackgroundDrawable(view)
composite.borderInsets = composite.borderInsets ?: BorderInsets()
composite.borderInsets?.setBorderWidth(edge, width)
if (Build.VERSION.SDK_INT >= MIN_INSET_BOX_SHADOW_SDK_VERSION) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.uimanager.style.BorderInsets
import com.facebook.react.uimanager.style.BorderRadiusStyle

Expand All @@ -32,45 +33,31 @@ internal class CompositeBackgroundDrawable(
public val originalBackground: Drawable? = null,

/** Non-inset box shadows */
public val outerShadows: LayerDrawable? = null,
public var outerShadows: LayerDrawable? = 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,
public var cssBackground: CSSBackgroundDrawable? = null,

/** Background rendering Layer */
public val background: BackgroundDrawable? = null,
public var background: BackgroundDrawable? = null,

/** Border rendering Layer */
public val border: BorderDrawable? = null,
public var border: BorderDrawable? = null,

/** TouchableNativeFeeback set selection background, like "SelectableBackground" */
public val feedbackUnderlay: Drawable? = null,
public var feedbackUnderlay: Drawable? = null,

/** Inset box-shadows */
public val innerShadows: LayerDrawable? = null,
public var innerShadows: LayerDrawable? = null,

/** Outline */
public val outline: OutlineDrawable? = null,
) :
LayerDrawable(
listOf(
originalBackground,
// z-ordering of user-provided shadow-list is opposite direction of LayerDrawable
// z-ordering
// https://drafts.csswg.org/css-backgrounds/#shadow-layers
outerShadows,
cssBackground,
background,
border,
feedbackUnderlay,
innerShadows,
outline)
.toTypedArray()) {
public var outline: OutlineDrawable? = null,
) : LayerDrawable(emptyArray()) {
// Holder value for currently set insets
public var borderInsets: BorderInsets? = null
// Holder value for currently set border radius
Expand All @@ -81,6 +68,15 @@ internal class CompositeBackgroundDrawable(
// 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)

addLayer(originalBackground, 0)
addLayer(outerShadows, 1)
addLayer(cssBackground, 2)
addLayer(background, 3)
addLayer(border, 4)
addLayer(feedbackUnderlay, 5)
addLayer(innerShadows, 6)
addLayer(outline, 7)
}

public fun withNewCssBackground(
Expand All @@ -103,6 +99,14 @@ internal class CompositeBackgroundDrawable(
}

public fun withNewBackground(background: BackgroundDrawable?): CompositeBackgroundDrawable {
if (ReactNativeFeatureFlags.enableNewBackgroundAndBorderDrawables()) {
this.background = background

if (updateLayer(background, 3)) {
return this
}
}

return CompositeBackgroundDrawable(
context,
originalBackground,
Expand All @@ -123,6 +127,15 @@ internal class CompositeBackgroundDrawable(
outerShadows: LayerDrawable?,
innerShadows: LayerDrawable?
): CompositeBackgroundDrawable {
if (ReactNativeFeatureFlags.enableNewBackgroundAndBorderDrawables()) {
this.outerShadows = outerShadows
this.innerShadows = innerShadows

if (updateLayer(outerShadows, 1) && updateLayer(innerShadows, 6)) {
return this
}
}

return CompositeBackgroundDrawable(
context,
originalBackground,
Expand All @@ -139,7 +152,15 @@ internal class CompositeBackgroundDrawable(
}
}

public fun withNewBorder(border: BorderDrawable): CompositeBackgroundDrawable {
public fun withNewBorder(border: BorderDrawable?): CompositeBackgroundDrawable {
if (ReactNativeFeatureFlags.enableNewBackgroundAndBorderDrawables()) {
this.border = border

if (updateLayer(border, 4)) {
return this
}
}

return CompositeBackgroundDrawable(
context,
originalBackground,
Expand All @@ -156,7 +177,15 @@ internal class CompositeBackgroundDrawable(
}
}

public fun withNewOutline(outline: OutlineDrawable): CompositeBackgroundDrawable {
public fun withNewOutline(outline: OutlineDrawable?): CompositeBackgroundDrawable {
if (ReactNativeFeatureFlags.enableNewBackgroundAndBorderDrawables()) {
this.outline = outline

if (updateLayer(outline, 7)) {
return this
}
}

return CompositeBackgroundDrawable(
context,
originalBackground,
Expand All @@ -174,6 +203,14 @@ internal class CompositeBackgroundDrawable(
}

public fun withNewFeedbackUnderlay(newUnderlay: Drawable?): CompositeBackgroundDrawable {
if (ReactNativeFeatureFlags.enableNewBackgroundAndBorderDrawables()) {
this.feedbackUnderlay = newUnderlay

if (updateLayer(newUnderlay, 5)) {
return this
}
}

return CompositeBackgroundDrawable(
context,
originalBackground,
Expand All @@ -190,6 +227,59 @@ internal class CompositeBackgroundDrawable(
}
}

private fun updateLayer(layer: Drawable?, id: Int): Boolean {
if (layer == null && findDrawableByLayerId(id) == null) {
return true
}

if (layer == null && findDrawableByLayerId(id) != null) {
return false
}

if (findDrawableByLayerId(id) == null) {
insertNewLayer(layer, id)
} else {
setDrawableByLayerId(id, layer)
}
invalidateSelf()
return true
}

private fun insertNewLayer(layer: Drawable?, id: Int) {
if (layer == null) {
return
}

if (numberOfLayers == 0) {
addLayer(layer, id)
return
}

for (i in 0..<numberOfLayers) {
if (id < getId(i)) {
val tempDrawable: Drawable = getDrawable(i)
val tempId = getId(i)
setDrawable(i, layer)
setId(i, id)
insertNewLayer(tempDrawable, tempId)
} else if (i == numberOfLayers - 1) {
addLayer(layer, id)
return
}
}
}

private fun addLayer(layer: Drawable?, id: Int) {
if (layer == null) {
return
}

addLayer(layer)
layer.callback = this
setId(numberOfLayers - 1, id)
invalidateSelf()
}

/* Android's elevation implementation requires this to be implemented to know where to draw the
elevation shadow. */
override fun getOutline(outline: Outline) {
Expand Down

0 comments on commit 3bae7a3

Please sign in to comment.