From 3bae7a34796e7111a9359d5c39914ff1535e7fea Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Thu, 14 Nov 2024 12:04:32 -0800 Subject: [PATCH] Optimize CompositeBackgroundDrawable 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 --- .../uimanager/BackgroundStyleApplicator.kt | 1 - .../drawable/CompositeBackgroundDrawable.kt | 138 +++++++++++++++--- 2 files changed, 114 insertions(+), 25 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 381cdf556bdbec..c2b180482c5a1b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -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) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt index 43eff2d4644af8..a8a8b92d827d7c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt @@ -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 @@ -32,7 +33,7 @@ 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 @@ -40,37 +41,23 @@ internal class CompositeBackgroundDrawable( * 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 @@ -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( @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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..