Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ android {
applicationId = "com.hedvig"

versionCode = 43
versionName = "14.4.2"
versionName = "14.4.3"

resourceConfigurations.addAll(listOf("en", "sv-rSE"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.hedvig.android.design.system.hedvig

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
*/
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<AutoScrollingMarqueeNode>() {
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<Float>(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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +38,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
Expand Down Expand Up @@ -183,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))
Expand Down Expand Up @@ -231,15 +234,19 @@ 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,
style = HedvigTheme.typography.label,
color = HedvigTheme.colorScheme.textSecondary,
maxLines = 1,
softWrap = false,
modifier = Modifier.autoScrollingMarquee(),
)
}
Spacer(Modifier.width(16.dp))
HedvigButton(
text = stringResource(Res.string.ONBOARDING_SEE_PRICE_BUTTON),
onClick = { openUrl(crossSell.storeUrl) },
Expand All @@ -251,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(
Expand Down Expand Up @@ -280,19 +309,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,
),
),
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 9.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (9.3.1)" variant="all" version="9.3.1">
</issues>
Loading