From eb5797015659558837a5c464aa1b13136e2ca56d Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 13:33:03 +0200 Subject: [PATCH 1/8] chore: roll onboarding cross-sell subtitle instead of wrapping Long subtitles (common in English) wrapped to a second line, giving the bundle-discount cross-sell rows varying heights and uneven pillow spacing. Make the subtitle a single marquee line so it scrolls instead. Titles are left free to wrap. Inter-row spacing is unchanged. --- .../ui/bundle/OnboardingBundleDestination.kt | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt index c21ffbad0c..3eb4c99cbb 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt @@ -1,5 +1,6 @@ package com.hedvig.android.feature.onboarding.ui.bundle +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -238,6 +239,8 @@ private fun OnboardingCrossSellRow( crossSell.description, style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, + maxLines = 1, + modifier = Modifier.basicMarquee(), ) } HedvigButton( @@ -280,19 +283,40 @@ private class OnboardingBundleUiStateProvider : CollectionPreviewParameterProvid progress = OnboardingProgress(totalSteps = 5, currentIndex = 1), crossSells = listOf( OnboardingCrossSell( - id = "accident", - title = "Accident Insurance", - description = "Coverage for accidental injuries", - storeUrl = "https://www.hedvig.com/se/forsakringar/olycksfallsforsakring", + id = "home", + title = "Home Insurance", + description = "For you, your family and your home", + storeUrl = "https://www.hedvig.com/se/forsakringar/hemforsakring", pillowImageUrl = null, ), OnboardingCrossSell( id = "pet", title = "Pet Insurance", - description = "Coverage for your pet", + description = "For your dog or cat", storeUrl = "https://www.hedvig.com/se/forsakringar/djurforsakring", pillowImageUrl = null, ), + OnboardingCrossSell( + id = "car", + title = "Car insurance", + description = "For you and your car", + storeUrl = "https://www.hedvig.com/se/forsakringar/bilforsakring", + pillowImageUrl = null, + ), + OnboardingCrossSell( + id = "vacation", + title = "Vacation Home Insurance", + description = "For your cottage or cabin", + storeUrl = "https://www.hedvig.com/se/forsakringar/fritidshusforsakring", + pillowImageUrl = null, + ), + OnboardingCrossSell( + id = "ppi", + title = "Payment Protection Insurance", + description = "For you if you get unemployed", + storeUrl = "https://www.hedvig.com/se/forsakringar/inkomstforsakring", + pillowImageUrl = null, + ), ), ), ), From 7dcf4fd292aa9c9c76ea81499e88656bf5e91b29 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 14:07:38 +0200 Subject: [PATCH 2/8] chore: back-and-forth auto-scroll for cross-sell subtitle Replace basicMarquee with a custom AutoScrollingText that scrolls a too-wide subtitle to its end, pauses, then scrolls back to the start and pauses again, looping. basicMarquee only loops one direction with an arbitrary gap before the text re-enters from the right; this reveals the whole line and returns, matching the iOS behaviour. Scroll duration scales with the hidden overflow so the speed stays constant. Subtitles that already fit do not move. --- .../onboarding/ui/bundle/AutoScrollingText.kt | 77 +++++++++++++++++++ .../ui/bundle/OnboardingBundleDestination.kt | 11 ++- 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt new file mode 100644 index 0000000000..ef8e214f94 --- /dev/null +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt @@ -0,0 +1,77 @@ +package com.hedvig.android.feature.onboarding.ui.bundle + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.HedvigText +import kotlin.math.roundToInt +import kotlinx.coroutines.delay + +/** + * A single line of text that, when it is wider than the space it is given, scrolls left to reveal + * its end, pauses, then scrolls back to the start and pauses again, looping. Text that already fits + * stays still. Scroll duration is proportional to the hidden overflow, so longer text scrolls for + * longer at a constant reading speed rather than faster. + */ +@Composable +internal fun AutoScrollingText(text: String, style: TextStyle, color: Color, modifier: Modifier = Modifier) { + var containerWidth by remember { mutableIntStateOf(0) } + var contentWidth by remember { mutableIntStateOf(0) } + val offset = remember { Animatable(0f) } + val density = LocalDensity.current + + LaunchedEffect(containerWidth, contentWidth) { + val overflow = (contentWidth - containerWidth).toFloat() + if (overflow <= 0f) { + offset.snapTo(0f) + return@LaunchedEffect + } + val durationMillis = with(density) { (overflow / SCROLL_VELOCITY_PER_SECOND.toPx() * 1000f).roundToInt() } + val scrollSpec = tween(durationMillis, easing = LinearEasing) + while (true) { + delay(PAUSE_MILLIS) + offset.animateTo(-overflow, scrollSpec) + delay(PAUSE_MILLIS) + offset.animateTo(0f, scrollSpec) + } + } + + Box( + modifier = modifier + .clipToBounds() + .onSizeChanged { containerWidth = it.width }, + ) { + HedvigText( + text = text, + style = style, + color = color, + maxLines = 1, + softWrap = false, + modifier = Modifier + .wrapContentWidth(align = Alignment.Start, unbounded = true) + .onSizeChanged { contentWidth = it.width } + .offset { IntOffset(offset.value.roundToInt(), 0) }, + ) + } +} + +private val SCROLL_VELOCITY_PER_SECOND = 30.dp +private const val PAUSE_MILLIS = 1500L diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt index 3eb4c99cbb..51c0e94e06 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt @@ -1,6 +1,5 @@ package com.hedvig.android.feature.onboarding.ui.bundle -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -232,17 +231,17 @@ private fun OnboardingCrossSellRow( } else { Spacer(Modifier.size(48.dp)) } - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(16.dp)) Column(Modifier.weight(1f)) { HedvigText(crossSell.title, style = HedvigTheme.typography.bodySmall) - HedvigText( - crossSell.description, + AutoScrollingText( + text = crossSell.description, style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, - maxLines = 1, - modifier = Modifier.basicMarquee(), + modifier = Modifier.fillMaxWidth(), ) } + Spacer(Modifier.width(16.dp)) HedvigButton( text = stringResource(Res.string.ONBOARDING_SEE_PRICE_BUTTON), onClick = { openUrl(crossSell.storeUrl) }, From 3dc7af0e1157d6579267dce331e5b0a3e7ae2638 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 12 Aug 2026 14:12:51 +0200 Subject: [PATCH 3/8] bump version to 14.4.3 --- app/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 43bcc4e36c..e112bd29e9 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -26,7 +26,7 @@ android { applicationId = "com.hedvig" versionCode = 43 - versionName = "14.4.2" + versionName = "14.4.3" resourceConfigurations.addAll(listOf("en", "sv-rSE")) } From 1bc9a95a1bba06a21e6d3f613994ba8ef1ddde21 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 14:19:28 +0200 Subject: [PATCH 4/8] chore: use typed Duration for auto-scroll pause delay --- .../feature/onboarding/ui/bundle/AutoScrollingText.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt index ef8e214f94..cdcc06027a 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.hedvig.android.design.system.hedvig.HedvigText import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay /** @@ -47,9 +48,9 @@ internal fun AutoScrollingText(text: String, style: TextStyle, color: Color, mod val durationMillis = with(density) { (overflow / SCROLL_VELOCITY_PER_SECOND.toPx() * 1000f).roundToInt() } val scrollSpec = tween(durationMillis, easing = LinearEasing) while (true) { - delay(PAUSE_MILLIS) + delay(PAUSE_MILLIS.milliseconds) offset.animateTo(-overflow, scrollSpec) - delay(PAUSE_MILLIS) + delay(PAUSE_MILLIS.milliseconds) offset.animateTo(0f, scrollSpec) } } From b3e580733c5e0157f6445f978f2368de8c82f62c Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 14:46:58 +0200 Subject: [PATCH 5/8] chore: expose cross-sell auto-scroll as a Modifier Repackage the ping-pong auto-scroll as Modifier.autoScrollingMarquee(), a LayoutModifierNode that measures its content unbounded, reports the container width, clips, and animates the placement offset. Applies directly to HedvigText like basicMarquee, dropping the wrapping Box and the style/color pass-through. --- .../ui/bundle/AutoScrollingMarquee.kt | 103 ++++++++++++++++++ .../onboarding/ui/bundle/AutoScrollingText.kt | 78 ------------- .../ui/bundle/OnboardingBundleDestination.kt | 8 +- 3 files changed, 108 insertions(+), 81 deletions(-) create mode 100644 app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt delete mode 100644 app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt new file mode 100644 index 0000000000..cabb1243a5 --- /dev/null +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt @@ -0,0 +1,103 @@ +package com.hedvig.android.feature.onboarding.ui.bundle + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.requireDensity +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +/** + * Scrolls the content horizontally when it is wider than the space it is given: left to reveal its + * end, a pause, then back to the start and another pause, looping. Content that already fits stays + * still. Scroll duration is proportional to the hidden overflow, so the speed stays constant rather + * than long content whipping past. Intended for a single, non-wrapping line of text. + * + * @param velocity how far the content travels per second while scrolling + * @param pause how long to hold at each end before scrolling the other way + */ +internal fun Modifier.autoScrollingMarquee( + velocity: Dp = DEFAULT_VELOCITY_PER_SECOND, + pause: Duration = DEFAULT_PAUSE, +): Modifier = clipToBounds() then AutoScrollingMarqueeElement(velocity, pause) + +private data class AutoScrollingMarqueeElement( + val velocity: Dp, + val pause: Duration, +) : ModifierNodeElement() { + override fun create() = AutoScrollingMarqueeNode(velocity, pause) + + override fun update(node: AutoScrollingMarqueeNode) = node.update(velocity, pause) + + override fun InspectorInfo.inspectableProperties() { + name = "autoScrollingMarquee" + properties["velocity"] = velocity + properties["pause"] = pause + } +} + +private class AutoScrollingMarqueeNode( + private var velocity: Dp, + private var pause: Duration, +) : Modifier.Node(), LayoutModifierNode { + private val offset = Animatable(0f) + private var overflow = 0f + private var animation: Job? = null + + fun update(velocity: Dp, pause: Duration) { + this.velocity = velocity + this.pause = pause + restart() + } + + override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult { + val placeable = measurable.measure(constraints.copy(maxWidth = Constraints.Infinity)) + val width = placeable.width.coerceAtMost(constraints.maxWidth) + val newOverflow = (placeable.width - width).toFloat() + if (newOverflow != overflow) { + overflow = newOverflow + restart() + } + return layout(width, placeable.height) { + placeable.place(offset.value.roundToInt(), 0) + } + } + + private fun restart() { + animation?.cancel() + animation = coroutineScope.launch { + if (overflow <= 0f) { + offset.snapTo(0f) + return@launch + } + val durationMillis = (overflow / with(requireDensity()) { velocity.toPx() } * 1000f).roundToInt() + val scrollSpec = tween(durationMillis, easing = LinearEasing) + offset.snapTo(0f) + while (isActive) { + delay(pause) + offset.animateTo(-overflow, scrollSpec) + delay(pause) + offset.animateTo(0f, scrollSpec) + } + } + } +} + +private val DEFAULT_VELOCITY_PER_SECOND = 30.dp +private val DEFAULT_PAUSE = 1500.milliseconds diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt deleted file mode 100644 index cdcc06027a..0000000000 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingText.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.hedvig.android.feature.onboarding.ui.bundle - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import com.hedvig.android.design.system.hedvig.HedvigText -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.milliseconds -import kotlinx.coroutines.delay - -/** - * A single line of text that, when it is wider than the space it is given, scrolls left to reveal - * its end, pauses, then scrolls back to the start and pauses again, looping. Text that already fits - * stays still. Scroll duration is proportional to the hidden overflow, so longer text scrolls for - * longer at a constant reading speed rather than faster. - */ -@Composable -internal fun AutoScrollingText(text: String, style: TextStyle, color: Color, modifier: Modifier = Modifier) { - var containerWidth by remember { mutableIntStateOf(0) } - var contentWidth by remember { mutableIntStateOf(0) } - val offset = remember { Animatable(0f) } - val density = LocalDensity.current - - LaunchedEffect(containerWidth, contentWidth) { - val overflow = (contentWidth - containerWidth).toFloat() - if (overflow <= 0f) { - offset.snapTo(0f) - return@LaunchedEffect - } - val durationMillis = with(density) { (overflow / SCROLL_VELOCITY_PER_SECOND.toPx() * 1000f).roundToInt() } - val scrollSpec = tween(durationMillis, easing = LinearEasing) - while (true) { - delay(PAUSE_MILLIS.milliseconds) - offset.animateTo(-overflow, scrollSpec) - delay(PAUSE_MILLIS.milliseconds) - offset.animateTo(0f, scrollSpec) - } - } - - Box( - modifier = modifier - .clipToBounds() - .onSizeChanged { containerWidth = it.width }, - ) { - HedvigText( - text = text, - style = style, - color = color, - maxLines = 1, - softWrap = false, - modifier = Modifier - .wrapContentWidth(align = Alignment.Start, unbounded = true) - .onSizeChanged { contentWidth = it.width } - .offset { IntOffset(offset.value.roundToInt(), 0) }, - ) - } -} - -private val SCROLL_VELOCITY_PER_SECOND = 30.dp -private const val PAUSE_MILLIS = 1500L diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt index 51c0e94e06..42fb9a9904 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt @@ -234,11 +234,13 @@ private fun OnboardingCrossSellRow( Spacer(Modifier.width(16.dp)) Column(Modifier.weight(1f)) { HedvigText(crossSell.title, style = HedvigTheme.typography.bodySmall) - AutoScrollingText( - text = crossSell.description, + HedvigText( + crossSell.description, style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, - modifier = Modifier.fillMaxWidth(), + maxLines = 1, + softWrap = false, + modifier = Modifier.autoScrollingMarquee(), ) } Spacer(Modifier.width(16.dp)) From 6d77c74f69445d37a8939622c65fba94575307aa Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 14:56:51 +0200 Subject: [PATCH 6/8] chore: move autoScrollingMarquee into design-system-hedvig Promote Modifier.autoScrollingMarquee() from feature-onboarding to the shared design-system-hedvig commonMain so any surface can reuse it. Now public and KMP-common (available to iOS too). --- .../android/design/system/hedvig}/AutoScrollingMarquee.kt | 4 ++-- .../onboarding/ui/bundle/OnboardingBundleDestination.kt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) rename app/{feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle => design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig}/AutoScrollingMarquee.kt (97%) diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/AutoScrollingMarquee.kt similarity index 97% rename from app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt rename to app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/AutoScrollingMarquee.kt index cabb1243a5..176ae3c9bc 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/AutoScrollingMarquee.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/AutoScrollingMarquee.kt @@ -1,4 +1,4 @@ -package com.hedvig.android.feature.onboarding.ui.bundle +package com.hedvig.android.design.system.hedvig import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing @@ -32,7 +32,7 @@ import kotlinx.coroutines.launch * @param velocity how far the content travels per second while scrolling * @param pause how long to hold at each end before scrolling the other way */ -internal fun Modifier.autoScrollingMarquee( +fun Modifier.autoScrollingMarquee( velocity: Dp = DEFAULT_VELOCITY_PER_SECOND, pause: Duration = DEFAULT_PAUSE, ): Modifier = clipToBounds() then AutoScrollingMarqueeElement(velocity, pause) diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt index 42fb9a9904..dfa0cf390e 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt @@ -35,6 +35,7 @@ import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.autoScrollingMarquee import com.hedvig.android.design.system.hedvig.placeholder.crossSellPainterFallback import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader import com.hedvig.android.feature.onboarding.data.OnboardingCrossSell From bf530f5a6aabb495314473810ecf304c5b6f8f4b Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 15:14:28 +0200 Subject: [PATCH 7/8] chore: give onboarding cross-sell rows a uniform height Lay the cross-sell rows out with a small custom Layout that measures every row once and sizes them all to the tallest, centring each row's content in its cell. A single two-line title lifts every row to the same height, so the pillows and See price buttons keep an even vertical rhythm instead of drifting with each row's text. One measure pass, so heights are uniform on the first frame with no relayout. When no title wraps, the tallest equals the rest and nothing changes. --- .../ui/bundle/OnboardingBundleDestination.kt | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt index dfa0cf390e..46eb161bb9 100644 --- a/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt +++ b/app/feature/feature-onboarding/src/main/kotlin/com/hedvig/android/feature/onboarding/ui/bundle/OnboardingBundleDestination.kt @@ -19,8 +19,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.ImageLoader @@ -184,15 +187,14 @@ private fun OnboardingBundleScreen( ) Spacer(Modifier.weight(1f)) Spacer(Modifier.height(24.dp)) - for ((index, crossSell) in state.crossSells.withIndex()) { - if (index > 0) { - Spacer(Modifier.height(8.dp)) + EqualHeightColumn(rowSpacing = 8.dp) { + for (crossSell in state.crossSells) { + OnboardingCrossSellRow( + crossSell = crossSell, + imageLoader = imageLoader, + openUrl = openUrl, + ) } - OnboardingCrossSellRow( - crossSell = crossSell, - imageLoader = imageLoader, - openUrl = openUrl, - ) } Spacer(Modifier.height(8.dp)) Spacer(Modifier.weight(1f)) @@ -256,6 +258,28 @@ private fun OnboardingCrossSellRow( } } +/** + * Lays its children out in a vertical column where every child is given the height of the tallest + * child, with each child's own content centred within its cell. One measure pass, so the heights are + * uniform on the first frame. [rowSpacing] is inserted between children. + */ +@Composable +private fun EqualHeightColumn(rowSpacing: Dp, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Layout(content = content, modifier = modifier) { measurables, constraints -> + val placeables = measurables.map { it.measure(constraints.copy(minHeight = 0)) } + val rowHeight = placeables.maxOfOrNull(Placeable::height) ?: 0 + val spacingPx = rowSpacing.roundToPx() + val totalHeight = rowHeight * placeables.size + spacingPx * (placeables.size - 1).coerceAtLeast(0) + layout(constraints.maxWidth, totalHeight) { + var y = 0 + placeables.forEach { placeable -> + placeable.place(0, y + (rowHeight - placeable.height) / 2) + y += rowHeight + spacingPx + } + } + } +} + @HedvigPreview @Composable private fun PreviewOnboardingBundleScreen( From c1e1df9158c52dc9534320382445e2d2bc99f8af Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 12 Aug 2026 17:18:38 +0200 Subject: [PATCH 8/8] Run lint --- hedvig-lint/lint-baseline/lint-baseline-feature-onboarding.xml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 hedvig-lint/lint-baseline/lint-baseline-feature-onboarding.xml diff --git a/hedvig-lint/lint-baseline/lint-baseline-feature-onboarding.xml b/hedvig-lint/lint-baseline/lint-baseline-feature-onboarding.xml new file mode 100644 index 0000000000..699da1bff0 --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-feature-onboarding.xml @@ -0,0 +1,3 @@ + + +