From 38b605ee6462abdd6679c783b96c6ff0b069a33e Mon Sep 17 00:00:00 2001 From: JayShortway <29483617+JayShortway@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:14:25 +0100 Subject: [PATCH] [Paywalls V2] Adds support for shadows (#1952) --- .../paywalls/components/properties/Shadow.kt | 4 +- .../components/composable/WithShadow.kt | 266 ++++++++++++++++++ .../components/modifier/Background.kt | 2 +- .../components/{ => properties}/ColorStyle.kt | 5 +- .../components/properties/ShadowStyle.kt | 28 ++ .../components/text/TextComponentStyle.kt | 4 +- .../components/text/TextComponentView.kt | 3 +- 7 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/composable/WithShadow.kt rename ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/{ => properties}/ColorStyle.kt (97%) create mode 100644 ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Shadow.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Shadow.kt index 16c691c11d..4078475b05 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Shadow.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/properties/Shadow.kt @@ -1,11 +1,13 @@ package com.revenuecat.purchases.paywalls.components.properties import com.revenuecat.purchases.InternalRevenueCatAPI +import dev.drewhamilton.poko.Poko import kotlinx.serialization.Serializable @InternalRevenueCatAPI +@Poko @Serializable -internal data class Shadow( +class Shadow( @get:JvmSynthetic val color: ColorScheme, @get:JvmSynthetic val radius: Double, @get:JvmSynthetic val x: Double, diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/composable/WithShadow.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/composable/WithShadow.kt new file mode 100644 index 0000000000..95c7ede3d1 --- /dev/null +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/composable/WithShadow.kt @@ -0,0 +1,266 @@ +@file:JvmSynthetic + +package com.revenuecat.purchases.ui.revenuecatui.components.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.paywalls.components.properties.ColorInfo +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.Shadow +import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background +import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.toShadowStyle + +/** + * Adds a [shadow] in the provided [shape] to the [content]. + * + * @param margin If your [content] has a margin, provide that here. Otherwise the transparent pixels of the margin will + * participate in the shadow. + */ +@Composable +internal fun WithShadow( + shadow: ShadowStyle, + shape: Shape, + margin: PaddingValues = PaddingValues(all = 0.dp), + content: @Composable () -> Unit, +) { + // We cannot use the built-in .shadow() modifier, as it takes very different parameters from what we have in our + // ShadowStyle. WithShadow also can't be a modifier itself, because the modifiers we use to draw the shadow + // (offset, blur and background) cannot be part of the same modifier chain as those applied to the content. If they + // are, they will be combined in ways we don't want. + // We might be able to achieve a similar result using a .drawBehind() modifier, but we would need to figure out how + // to draw gradients on a canvas. + Layout( + content = { + // Content + content() + + // Shadow + Box( + Modifier + .offset { IntOffset(x = shadow.x.roundToPx(), y = shadow.y.roundToPx()) } + .blur(shadow.radius, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(shadow.color, shape), + ) + }, + ) { measurables, constraints -> + val contentMeasurable = measurables[0] + val shadowMeasurable = measurables[1] + + val startMarginPx = margin.calculateStartPadding(layoutDirection).roundToPx() + val topMarginPx = margin.calculateTopPadding().roundToPx() + val endMarginPx = margin.calculateEndPadding(layoutDirection).roundToPx() + val bottomMarginPx = margin.calculateBottomPadding().roundToPx() + + // We measure the content first, then make the shadow exactly as big, minus any margins. + val contentPlaceable = contentMeasurable.measure(constraints) + val shadowPlaceable = shadowMeasurable.measure( + constraints = Constraints( + minWidth = contentPlaceable.measuredWidth - startMarginPx - endMarginPx, + maxWidth = contentPlaceable.measuredWidth - startMarginPx - endMarginPx, + minHeight = contentPlaceable.measuredHeight - topMarginPx - bottomMarginPx, + maxHeight = contentPlaceable.measuredHeight - topMarginPx - bottomMarginPx, + ), + ) + + layout(width = contentPlaceable.measuredWidth, height = contentPlaceable.measuredHeight) { + // We place the shadow and the content on top of each other. + shadowPlaceable.placeRelative(x = startMarginPx, y = topMarginPx) + contentPlaceable.placeRelative(x = 0, y = 0) + } + } +} + +@Suppress("MagicNumber") +@Preview("Circle") +@Composable +private fun Shadow_Preview_Circle() { + val shape = CircleShape + Box( + modifier = Modifier + .requiredSize(200.dp), + contentAlignment = Alignment.Center, + ) { + WithShadow( + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 5.0, + y = 5.0, + radius = 0.0, + ).toShadowStyle(), + shape = shape, + ) { + Box( + modifier = Modifier + .requiredSize(100.dp) + .background(Color.Red, shape = shape), + ) + } + } +} + +@Suppress("MagicNumber") +@Preview("Square") +@Composable +private fun Shadow_Preview_Square() { + val shape = RectangleShape + Box( + modifier = Modifier + .requiredSize(200.dp), + contentAlignment = Alignment.Center, + ) { + WithShadow( + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 10.0, + y = 5.0, + radius = 20.0, + ).toShadowStyle(), + shape = shape, + ) { + Box( + modifier = Modifier + .requiredSize(100.dp) + .background(Color.Red, shape = shape), + ) + } + } +} + +@Suppress("MagicNumber") +@Preview("Gradient+CustomShape") +@Composable +private fun Shadow_Preview_Gradient_CustomShape() { + val shape = RoundedCornerShape(50) + Box( + modifier = Modifier + .requiredSize(200.dp), + contentAlignment = Alignment.Center, + ) { + WithShadow( + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Gradient.Linear( + degrees = 0f, + points = listOf( + ColorInfo.Gradient.Point( + color = Color.Red.toArgb(), + percent = 0.1f, + ), + ColorInfo.Gradient.Point( + color = Color.Green.toArgb(), + percent = 0.5f, + ), + ColorInfo.Gradient.Point( + color = Color.Blue.toArgb(), + percent = 0.9f, + ), + ), + ), + ), + x = 0.0, + y = 5.0, + radius = 10.0, + ).toShadowStyle(), + shape = shape, + ) { + Text( + text = "GET UNLIMITED RGB", + modifier = Modifier + .background(Color.Black, shape = shape) + .padding(horizontal = 24.dp, vertical = 16.dp), + color = Color.White, + style = MaterialTheme.typography.titleSmall, + ) + } + } +} + +@Suppress("MagicNumber") +@Preview("Margin") +@Composable +private fun Shadow_Preview_Margin() { + val margin = PaddingValues(start = 8.dp, top = 16.dp, end = 4.dp, bottom = 24.dp) + val shape = RectangleShape + Column( + modifier = Modifier + .requiredSize(width = 100.dp, height = 200.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + WithShadow( + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 0.0, + y = 5.0, + radius = 20.0, + ).toShadowStyle(), + shape = shape, + margin = margin, + ) { + Box( + modifier = Modifier + .padding(margin) + .requiredSize(width = 50.dp, height = 50.dp) + .background(Color.Red, shape) + .border(width = 2.dp, Color.Blue, shape) + .padding(all = 16.dp), + ) + } + + WithShadow( + shadow = Shadow( + color = ColorScheme( + light = ColorInfo.Hex(Color.Black.toArgb()), + ), + x = 0.0, + y = 5.0, + radius = 20.0, + ).toShadowStyle(), + shape = shape, + margin = margin, + ) { + Box( + modifier = Modifier + .padding(margin) + .requiredSize(width = 50.dp, height = 50.dp) + .background(Color.Red, shape) + .border(width = 2.dp, Color.Blue, shape) + .padding(all = 16.dp), + ) + } + } +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Background.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Background.kt index 743de7bfad..24b6329c72 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Background.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/modifier/Background.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape -import com.revenuecat.purchases.ui.revenuecatui.components.ColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle @JvmSynthetic @Stable diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ColorStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ColorStyle.kt similarity index 97% rename from ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ColorStyle.kt rename to ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ColorStyle.kt index 85119bd5c8..c9e9b4cdf2 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/ColorStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ColorStyle.kt @@ -1,4 +1,4 @@ -package com.revenuecat.purchases.ui.revenuecatui.components +package com.revenuecat.purchases.ui.revenuecatui.components.properties import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -26,6 +26,9 @@ import kotlin.math.cos import kotlin.math.max import kotlin.math.sin +/** + * Ready to use color properties for the current theme. + */ internal sealed interface ColorStyle { @JvmInline diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt new file mode 100644 index 0000000000..5343aabda5 --- /dev/null +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/properties/ShadowStyle.kt @@ -0,0 +1,28 @@ +package com.revenuecat.purchases.ui.revenuecatui.components.properties + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.revenuecat.purchases.paywalls.components.properties.Shadow + +/** + * Ready to use shadow properties for the current theme. + */ +@Immutable +internal data class ShadowStyle( + @get:JvmSynthetic val color: ColorStyle, + @get:JvmSynthetic val radius: Dp, + @get:JvmSynthetic val x: Dp, + @get:JvmSynthetic val y: Dp, +) + +@JvmSynthetic +@Composable +internal fun Shadow.toShadowStyle(): ShadowStyle = + ShadowStyle( + color = color.toColorStyle(), + radius = radius.dp, + x = x.dp, + y = y.dp, + ) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentStyle.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentStyle.kt index 6a6575e849..b28f78c0f0 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentStyle.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentStyle.kt @@ -15,13 +15,13 @@ import com.revenuecat.purchases.paywalls.components.properties.FontSize import com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment import com.revenuecat.purchases.paywalls.components.properties.Padding import com.revenuecat.purchases.paywalls.components.properties.Size -import com.revenuecat.purchases.ui.revenuecatui.components.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextUnit -import com.revenuecat.purchases.ui.revenuecatui.components.toColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle +import com.revenuecat.purchases.ui.revenuecatui.components.properties.toColorStyle import com.revenuecat.purchases.paywalls.components.properties.FontWeight as RcFontWeight @Suppress("LongParameterList") diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt index 976672cdd4..d17ea77ff7 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/text/TextComponentView.kt @@ -2,7 +2,6 @@ package com.revenuecat.purchases.ui.revenuecatui.components.text -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.LocalTextStyle @@ -23,9 +22,9 @@ import com.revenuecat.purchases.paywalls.components.properties.Size import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit -import com.revenuecat.purchases.ui.revenuecatui.components.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.components.modifier.background import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size +import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle import com.revenuecat.purchases.ui.revenuecatui.composables.Markdown import com.revenuecat.purchases.ui.revenuecatui.extensions.conditional