Skip to content

Commit e859293

Browse files
feat(android): background-size, position and repeat styles (#52282)
Summary: This PR adds support for background size, position and repeat styles. It follows the [CSS](https://www.w3.org/TR/css-backgrounds-3/#backgrounds) spec. Currently we default to `background-origin: padding-box` and `background-clip : border-box` to match the web's behavior. We can introduce these styles later. I have split the PR intro three parts for review. This PR includes android only changes. I wanted to introduce one style at a time, but CSS spec is such that size, position and repeat are intertwined. ## Changelog: [ANDROID][ADDED] - Background size, position and repeat styles. <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #52282 Test Plan: Merge the [JS](#52284) PR, rebuild android app and test RNTester app, it includes `BackgroundImageExample`. I have also added testcases for parsing syntax in JS. https://github.com/user-attachments/assets/b7192fdf-52ba-4eb0-a1be-d47c72d87e92 Reviewed By: joevilches Differential Revision: D82993837 Pulled By: jorge-cab fbshipit-source-id: 52859e51d2c4bab27823d3eb913993fdfbb4c04d
1 parent d8c2f1c commit e859293

File tree

12 files changed

+782
-64
lines changed

12 files changed

+782
-64
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3457,12 +3457,13 @@ public final class com/facebook/react/uimanager/LengthPercentage {
34573457
public final fun getType ()Lcom/facebook/react/uimanager/LengthPercentageType;
34583458
public fun hashCode ()I
34593459
public final fun resolve (F)F
3460-
public static final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;)Lcom/facebook/react/uimanager/LengthPercentage;
3460+
public static final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;Z)Lcom/facebook/react/uimanager/LengthPercentage;
34613461
public fun toString ()Ljava/lang/String;
34623462
}
34633463

34643464
public final class com/facebook/react/uimanager/LengthPercentage$Companion {
3465-
public final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;)Lcom/facebook/react/uimanager/LengthPercentage;
3465+
public final fun setFromDynamic (Lcom/facebook/react/bridge/Dynamic;Z)Lcom/facebook/react/uimanager/LengthPercentage;
3466+
public static synthetic fun setFromDynamic$default (Lcom/facebook/react/uimanager/LengthPercentage$Companion;Lcom/facebook/react/bridge/Dynamic;ZILjava/lang/Object;)Lcom/facebook/react/uimanager/LengthPercentage;
34663467
}
34673468

34683469
public final class com/facebook/react/uimanager/LengthPercentageType : java/lang/Enum {
@@ -4613,6 +4614,9 @@ public final class com/facebook/react/uimanager/ViewProps {
46134614
public static final field AUTO Ljava/lang/String;
46144615
public static final field BACKGROUND_COLOR Ljava/lang/String;
46154616
public static final field BACKGROUND_IMAGE Ljava/lang/String;
4617+
public static final field BACKGROUND_POSITION Ljava/lang/String;
4618+
public static final field BACKGROUND_REPEAT Ljava/lang/String;
4619+
public static final field BACKGROUND_SIZE Ljava/lang/String;
46164620
public static final field BORDER_BLOCK_COLOR Ljava/lang/String;
46174621
public static final field BORDER_BLOCK_END_COLOR Ljava/lang/String;
46184622
public static final field BORDER_BLOCK_START_COLOR Ljava/lang/String;
@@ -4942,7 +4946,7 @@ public final class com/facebook/react/uimanager/style/BackgroundImageLayer {
49424946
public static final field Companion Lcom/facebook/react/uimanager/style/BackgroundImageLayer$Companion;
49434947
public fun <init> ()V
49444948
public synthetic fun <init> (Lcom/facebook/react/uimanager/style/Gradient;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
4945-
public final fun getShader (Landroid/graphics/Rect;)Landroid/graphics/Shader;
4949+
public final fun getShader (FF)Landroid/graphics/Shader;
49464950
}
49474951

49484952
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$Companion {
@@ -6701,6 +6705,9 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
67016705
public fun setAccessible (Lcom/facebook/react/views/view/ReactViewGroup;Z)V
67026706
public fun setBackfaceVisibility (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V
67036707
public fun setBackgroundImage (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
6708+
public fun setBackgroundPosition (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
6709+
public fun setBackgroundRepeat (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
6710+
public fun setBackgroundSize (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
67046711
public fun setBorderColor (Lcom/facebook/react/views/view/ReactViewGroup;ILjava/lang/Integer;)V
67056712
public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;IF)V
67066713
public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;ILcom/facebook/react/bridge/Dynamic;)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.facebook.react.uimanager.PixelUtil.pxToDp
2424
import com.facebook.react.uimanager.common.UIManagerType
2525
import com.facebook.react.uimanager.common.ViewUtil
2626
import com.facebook.react.uimanager.drawable.BackgroundDrawable
27+
import com.facebook.react.uimanager.drawable.BackgroundImageDrawable
2728
import com.facebook.react.uimanager.drawable.BorderDrawable
2829
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable
2930
import com.facebook.react.uimanager.drawable.InsetBoxShadowDrawable
@@ -32,6 +33,9 @@ import com.facebook.react.uimanager.drawable.MIN_OUTSET_BOX_SHADOW_SDK_VERSION
3233
import com.facebook.react.uimanager.drawable.OutlineDrawable
3334
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable
3435
import com.facebook.react.uimanager.style.BackgroundImageLayer
36+
import com.facebook.react.uimanager.style.BackgroundPosition
37+
import com.facebook.react.uimanager.style.BackgroundRepeat
38+
import com.facebook.react.uimanager.style.BackgroundSize
3539
import com.facebook.react.uimanager.style.BorderInsets
3640
import com.facebook.react.uimanager.style.BorderRadiusProp
3741
import com.facebook.react.uimanager.style.BorderRadiusStyle
@@ -65,7 +69,25 @@ public object BackgroundStyleApplicator {
6569
view: View,
6670
backgroundImageLayers: List<BackgroundImageLayer>?,
6771
): Unit {
68-
ensureBackgroundDrawable(view).backgroundImageLayers = backgroundImageLayers
72+
ensureBackgroundImageDrawable(view).backgroundImageLayers = backgroundImageLayers
73+
}
74+
75+
@JvmStatic
76+
internal fun setBackgroundSize(view: View, backgroundSizes: List<BackgroundSize>?): Unit {
77+
ensureBackgroundImageDrawable(view).backgroundSize = backgroundSizes
78+
}
79+
80+
@JvmStatic
81+
internal fun setBackgroundPosition(
82+
view: View,
83+
backgroundPositions: List<BackgroundPosition>?,
84+
): Unit {
85+
ensureBackgroundImageDrawable(view).backgroundPosition = backgroundPositions
86+
}
87+
88+
@JvmStatic
89+
internal fun setBackgroundRepeat(view: View, backgroundRepeats: List<BackgroundRepeat>?): Unit {
90+
ensureBackgroundImageDrawable(view).backgroundRepeat = backgroundRepeats
6991
}
7092

7193
@JvmStatic
@@ -82,9 +104,11 @@ public object BackgroundStyleApplicator {
82104

83105
ensureBorderDrawable(view).setBorderWidth(edge.toSpacingType(), width?.dpToPx() ?: Float.NaN)
84106
composite.background?.borderInsets = composite.borderInsets
107+
composite.backgroundImage?.borderInsets = composite.borderInsets
85108
composite.border?.borderInsets = composite.borderInsets
86109

87110
composite.background?.invalidateSelf()
111+
composite.backgroundImage?.invalidateSelf()
88112
composite.border?.invalidateSelf()
89113

90114
composite.borderInsets = composite.borderInsets ?: BorderInsets()
@@ -133,9 +157,12 @@ public object BackgroundStyleApplicator {
133157
ensureBackgroundDrawable(view)
134158
}
135159
compositeBackgroundDrawable.background?.borderRadius = compositeBackgroundDrawable.borderRadius
160+
compositeBackgroundDrawable.backgroundImage?.borderRadius =
161+
compositeBackgroundDrawable.borderRadius
136162
compositeBackgroundDrawable.border?.borderRadius = compositeBackgroundDrawable.borderRadius
137163

138164
compositeBackgroundDrawable.background?.invalidateSelf()
165+
compositeBackgroundDrawable.backgroundImage?.invalidateSelf()
139166
compositeBackgroundDrawable.border?.invalidateSelf()
140167

141168
if (Build.VERSION.SDK_INT >= MIN_OUTSET_BOX_SHADOW_SDK_VERSION) {
@@ -381,6 +408,27 @@ public object BackgroundStyleApplicator {
381408
private fun getBackground(view: View): BackgroundDrawable? =
382409
getCompositeBackgroundDrawable(view)?.background
383410

411+
private fun ensureBackgroundImageDrawable(view: View): BackgroundImageDrawable {
412+
val compositeBackgroundDrawable = ensureCompositeBackgroundDrawable(view)
413+
var backgroundImage = compositeBackgroundDrawable.backgroundImage
414+
415+
return if (backgroundImage != null) {
416+
backgroundImage
417+
} else {
418+
backgroundImage =
419+
BackgroundImageDrawable(
420+
view.context,
421+
compositeBackgroundDrawable.borderRadius,
422+
compositeBackgroundDrawable.borderInsets,
423+
)
424+
view.background = compositeBackgroundDrawable.withNewBackgroundImage(backgroundImage)
425+
backgroundImage
426+
}
427+
}
428+
429+
private fun getBackgroundImage(view: View): BackgroundImageDrawable? =
430+
getCompositeBackgroundDrawable(view)?.backgroundImage
431+
384432
private fun getBorder(view: View): BorderDrawable? = getCompositeBackgroundDrawable(view)?.border
385433

386434
private fun ensureBorderDrawable(view: View): BorderDrawable {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,24 @@ public data class LengthPercentage(
2424
) {
2525
public companion object {
2626
@JvmStatic
27-
public fun setFromDynamic(dynamic: Dynamic): LengthPercentage? {
27+
public fun setFromDynamic(dynamic: Dynamic, allowNegative: Boolean = false): LengthPercentage? {
2828
return when (dynamic.type) {
2929
ReadableType.Number -> {
3030
val value = dynamic.asDouble()
31-
if (value >= 0f) {
32-
LengthPercentage(value.toFloat(), LengthPercentageType.POINT)
33-
} else {
34-
null
31+
if (value < 0 && !allowNegative) {
32+
return null
3533
}
34+
LengthPercentage(value.toFloat(), LengthPercentageType.POINT)
3635
}
3736
ReadableType.String -> {
3837
val s = dynamic.asString()
3938
if (s != null && s.endsWith("%")) {
4039
try {
4140
val value = s.substring(0, s.length - 1).toFloat()
42-
if (value >= 0f) {
43-
LengthPercentage(value, LengthPercentageType.PERCENT)
44-
} else {
45-
null
41+
if (value < 0 && !allowNegative) {
42+
return null
4643
}
44+
LengthPercentage(value, LengthPercentageType.PERCENT)
4745
} catch (e: NumberFormatException) {
4846
FLog.w(ReactConstants.TAG, "Invalid percentage format: $s")
4947
null

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public object ViewProps {
7373
public const val ENABLED: String = "enabled"
7474
public const val BACKGROUND_COLOR: String = "backgroundColor"
7575
public const val BACKGROUND_IMAGE: String = "experimental_backgroundImage"
76+
public const val BACKGROUND_SIZE: String = "experimental_backgroundSize"
77+
public const val BACKGROUND_POSITION: String = "experimental_backgroundPosition"
78+
public const val BACKGROUND_REPEAT: String = "experimental_backgroundRepeat"
7679
public const val FOREGROUND_COLOR: String = "foregroundColor"
7780
public const val COLOR: String = "color"
7881
public const val FONT_SIZE: String = "fontSize"

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ import android.content.Context
1111
import android.graphics.Canvas
1212
import android.graphics.Color
1313
import android.graphics.ColorFilter
14-
import android.graphics.ComposeShader
1514
import android.graphics.Paint
1615
import android.graphics.Path
1716
import android.graphics.PixelFormat
18-
import android.graphics.PorterDuff
1917
import android.graphics.Rect
2018
import android.graphics.RectF
21-
import android.graphics.Shader
2219
import android.graphics.drawable.Drawable
2320
import com.facebook.react.uimanager.PixelUtil.dpToPx
2421
import com.facebook.react.uimanager.PixelUtil.pxToDp
25-
import com.facebook.react.uimanager.style.BackgroundImageLayer
2622
import com.facebook.react.uimanager.style.BorderInsets
2723
import com.facebook.react.uimanager.style.BorderRadiusStyle
2824
import com.facebook.react.uimanager.style.ComputedBorderRadius
@@ -59,14 +55,6 @@ internal class BackgroundDrawable(
5955
private var backgroundRect: RectF = RectF()
6056
private var backgroundRenderPath: Path? = null
6157

62-
var backgroundImageLayers: List<BackgroundImageLayer>? = null
63-
set(value) {
64-
if (field != value) {
65-
field = value
66-
invalidateSelf()
67-
}
68-
}
69-
7058
private val backgroundPaint: Paint =
7159
Paint(Paint.ANTI_ALIAS_FLAG).apply {
7260
style = Paint.Style.FILL
@@ -123,24 +111,6 @@ internal class BackgroundDrawable(
123111
}
124112
}
125113

126-
backgroundPaint.alpha = 255
127-
if (backgroundImageLayers != null && backgroundImageLayers?.isNotEmpty() == true) {
128-
backgroundPaint.setShader(getBackgroundImageShader())
129-
if (computedBorderRadius?.isUniform() == true && borderRadius?.hasRoundedBorders() == true) {
130-
canvas.drawRoundRect(
131-
backgroundRect,
132-
computedBorderRadius?.topLeft?.horizontal?.dpToPx() ?: 0f,
133-
computedBorderRadius?.topLeft?.vertical?.dpToPx() ?: 0f,
134-
backgroundPaint,
135-
)
136-
} else if (borderRadius?.hasRoundedBorders() != true) {
137-
canvas.drawRect(backgroundRect, backgroundPaint)
138-
} else {
139-
canvas.drawPath(checkNotNull(backgroundRenderPath), backgroundPaint)
140-
}
141-
backgroundPaint.setShader(null)
142-
}
143-
backgroundPaint.alpha = Color.alpha(backgroundColor)
144114
canvas.restore()
145115
}
146116

@@ -154,24 +124,6 @@ internal class BackgroundDrawable(
154124
)
155125
}
156126

157-
private fun getBackgroundImageShader(): Shader? {
158-
backgroundImageLayers?.let { layers ->
159-
var compositeShader: Shader? = null
160-
for (backgroundImageLayer in layers) {
161-
val currentShader = backgroundImageLayer.getShader(bounds)
162-
163-
compositeShader =
164-
if (compositeShader == null) {
165-
currentShader
166-
} else {
167-
ComposeShader(currentShader, compositeShader, PorterDuff.Mode.SRC_OVER)
168-
}
169-
}
170-
return compositeShader
171-
}
172-
return null
173-
}
174-
175127
private fun updatePath() {
176128
if (!needUpdatePath) {
177129
return

0 commit comments

Comments
 (0)