Skip to content

Commit 0336f9b

Browse files
facumenzellavegaro
andauthored
Fix gradient overlay to cover full viewport instead of image bounds (#3017)
## Summary Fixes gradient overlays on background images appearing more aggressive on Android compared to the web builder. ## Problem When a paywall has a background image with a color overlay (gradient), the overlay was being applied via the `underlay()` modifier which draws within the image's bounds. Since images are often aspect-fit and smaller than the viewport, a gradient defined as "66% white from bottom" would compress into that smaller area, making much more of the screen appear white. **Web builder:** Overlay covers 100% of viewport → gradient spread across full screen **Android (before):** Overlay only covered image bounds → gradient compressed into smaller area ## Solution ### `Background.kt` - Removed `colorOverlay` application from the image background modifier - Added `alignment = TopCenter` to position the image at the top ### `ViewWithVideoBackground.kt` - Extended `WithOptionalVideoBackground` to also handle `BackgroundStyle.Image` with overlays - Renders overlay as a separate `Box` layer using `.matchParentSize()` to fill the full container - Overlay renders between background image and content (correct Z-order) | Android | Dashboard | |--------|--------| | <img width="426" height="808" alt="Screenshot 2026-01-15 at 10 37 51" src="https://github.com/user-attachments/assets/49c4f4f6-d78f-4953-a95d-f9adeab9dc12" /> | <img width="369" height="611" alt="Screenshot 2026-01-15 at 10 46 56" src="https://github.com/user-attachments/assets/605af32d-91bf-49da-bc72-4f7dea9a3c43" /> | --------- Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
1 parent bdc4c70 commit 0336f9b

File tree

6 files changed

+77
-35
lines changed

6 files changed

+77
-35
lines changed

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/LoadedPaywallComponents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ internal fun LoadedPaywallComponents(
8080
sheetState = state.sheet,
8181
modifier = modifier.background(background),
8282
) {
83-
WithOptionalVideoBackground(state, background = background) {
83+
WithOptionalBackgroundOverlay(state, background = background) {
8484
Column {
8585
ComponentView(
8686
style = style,

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ViewWithVideoBackground.kt

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,3 @@ internal fun ViewWithVideoBackground(
6363
content()
6464
}
6565
}
66-
67-
@Composable
68-
internal fun WithOptionalVideoBackground(
69-
state: PaywallState.Loaded.Components,
70-
background: BackgroundStyle?,
71-
modifier: Modifier = Modifier,
72-
shape: Shape = RectangleShape,
73-
content: @Composable () -> Unit,
74-
) {
75-
if (background is BackgroundStyle.Video) {
76-
ViewWithVideoBackground(
77-
state = state,
78-
background = background,
79-
shape = shape,
80-
modifier = modifier,
81-
) {
82-
content()
83-
}
84-
} else {
85-
content()
86-
}
87-
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.revenuecat.purchases.ui.revenuecatui.components
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.graphics.RectangleShape
7+
import androidx.compose.ui.graphics.Shape
8+
import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background
9+
import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle
10+
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
11+
12+
@Composable
13+
internal fun WithOptionalBackgroundOverlay(
14+
state: PaywallState.Loaded.Components,
15+
background: BackgroundStyle?,
16+
modifier: Modifier = Modifier,
17+
shape: Shape = RectangleShape,
18+
content: @Composable () -> Unit,
19+
) {
20+
when {
21+
background is BackgroundStyle.Video -> {
22+
ViewWithVideoBackground(
23+
state = state,
24+
background = background,
25+
shape = shape,
26+
modifier = modifier,
27+
) {
28+
content()
29+
}
30+
}
31+
background is BackgroundStyle.Image && background.colorOverlay != null -> {
32+
// Image backgrounds with color overlays need the overlay to cover the full container,
33+
// not just the image bounds. This matches the web builder behavior where overlays
34+
// cover 100% of the viewport.
35+
Box(modifier = modifier) {
36+
// Render overlay BEHIND content but in front of background image (applied via modifier)
37+
Box(
38+
modifier = Modifier
39+
.matchParentSize()
40+
.background(background.colorOverlay, shape),
41+
)
42+
content()
43+
}
44+
}
45+
else -> {
46+
content()
47+
}
48+
}
49+
}

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Background.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ package com.revenuecat.purchases.ui.revenuecatui.components.modifier
55

66
import androidx.compose.foundation.background
77
import androidx.compose.runtime.Stable
8+
import androidx.compose.ui.Alignment
89
import androidx.compose.ui.Modifier
910
import androidx.compose.ui.draw.clip
1011
import androidx.compose.ui.draw.paint
1112
import androidx.compose.ui.graphics.RectangleShape
1213
import androidx.compose.ui.graphics.Shape
1314
import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyle
1415
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle
15-
import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull
1616

1717
@JvmSynthetic
1818
@Stable
@@ -34,12 +34,15 @@ internal fun Modifier.background(
3434
when (background) {
3535
is BackgroundStyle.Color -> this.background(color = background.color, shape = shape)
3636
is BackgroundStyle.Image ->
37+
// The image is applied here via paint(). Color overlays (if present) are rendered
38+
// separately in WithOptionalBackgroundOverlay to ensure the overlay covers the full
39+
// container, not just the image bounds. This matches the web builder behavior.
3740
this.clip(shape)
3841
.paint(
3942
painter = background.painter,
4043
contentScale = background.contentScale,
44+
alignment = Alignment.TopCenter,
4145
)
42-
.applyIfNotNull(background.colorOverlay) { underlay(it, shape) }
4346
is BackgroundStyle.Video ->
4447
// Video backgrounds are handled specially - they need to be rendered
4548
// in a Box behind the content, so we do nothing here

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAli
6161
import com.revenuecat.purchases.paywalls.components.properties.VerticalAlignment
6262
import com.revenuecat.purchases.ui.revenuecatui.components.ComponentView
6363
import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction
64-
import com.revenuecat.purchases.ui.revenuecatui.components.WithOptionalVideoBackground
64+
import com.revenuecat.purchases.ui.revenuecatui.components.WithOptionalBackgroundOverlay
6565
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment
6666
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toHorizontalAlignmentOrNull
6767
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toShape
@@ -621,7 +621,7 @@ private fun MainStackComponent(
621621
if (nestedBadge == null && overlay == null) {
622622
if (backgroundStyle is BackgroundStyle.Video) {
623623
// Video backgrounds require a Box wrapper with explicit sizing
624-
WithOptionalVideoBackground(
624+
WithOptionalBackgroundOverlay(
625625
state = state,
626626
background = backgroundStyle,
627627
shape = composeShape,
@@ -656,7 +656,7 @@ private fun MainStackComponent(
656656
.clip(composeShape)
657657
.then(borderModifier),
658658
) {
659-
WithOptionalVideoBackground(state, background = backgroundStyle) {
659+
WithOptionalBackgroundOverlay(state, background = backgroundStyle) {
660660
stack(Modifier.then(innerShapeModifier))
661661
}
662662

@@ -674,7 +674,7 @@ private fun MainStackComponent(
674674
.then(outerShapeModifier)
675675
.clip(composeShape),
676676
) {
677-
WithOptionalVideoBackground(state, background = backgroundStyle) {
677+
WithOptionalBackgroundOverlay(state, background = backgroundStyle) {
678678
stack(borderModifier.then(innerShapeModifier))
679679
}
680680
overlay()

ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/BackgroundTests.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,30 @@ class BackgroundTests {
172172
val sizeDp = with(LocalDensity.current) { imageSizePx.toDp() }
173173

174174
// Act
175-
Text(
176-
text = "Hello",
175+
// Image backgrounds with overlays are rendered in two layers:
176+
// 1. The image is applied via the background modifier
177+
// 2. The overlay is rendered as a separate Box layer (matching WithOptionalBackgroundOverlay)
178+
val imageStyle = backgroundStyle as BackgroundStyle.Image
179+
Box(
177180
modifier = Modifier
178181
.requiredSize(sizeDp)
179182
.background(backgroundStyle)
180-
.semantics { testTag = "text" },
181-
color = textColor,
182-
)
183+
.semantics { testTag = "box" }
184+
) {
185+
// Overlay layer - rendered behind content but in front of image
186+
imageStyle.colorOverlay?.let { overlay ->
187+
Box(modifier = Modifier.matchParentSize().background(color = overlay))
188+
}
189+
// Content layer
190+
Text(
191+
text = "Hello",
192+
color = textColor,
193+
)
194+
}
183195
}
184196

185197
// Assert
186-
onNodeWithTag("text")
198+
onNodeWithTag("box")
187199
.assertIsDisplayed()
188200
// The overlay should cover the entire background, as it is fully opaque.
189201
.assertNoPixelColorEquals(unexpectedColor.color)

0 commit comments

Comments
 (0)