Welcome screen polish.

This commit is contained in:
Alex Hart
2026-04-09 16:05:24 -03:00
committed by jeffrey-signal
parent 969635d942
commit faa6a1d3f0
3 changed files with 95 additions and 142 deletions
@@ -1,74 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.ui.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.tooling.preview.Preview
/**
* Lays out [primary] and [secondary] side by side in a row. [primaryProportion] controls what
* fraction of the total width [primary] receives (e.g. 0.5f = equal split).
*/
@Composable
fun SideBySideLayout(
primary: @Composable () -> Unit,
secondary: @Composable () -> Unit,
modifier: Modifier = Modifier,
primaryProportion: Float = 0.5f
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val primaryWidth = (constraints.maxWidth * primaryProportion).toInt()
val secondaryWidth = constraints.maxWidth - primaryWidth
val primaryPlaceables = subcompose("primary", primary).map { m ->
m.measure(constraints.copy(minWidth = primaryWidth, maxWidth = primaryWidth, minHeight = 0))
}
val primaryHeight = primaryPlaceables.maxOfOrNull { it.height } ?: 0
val secondaryMeasurables = subcompose("secondary", secondary)
val secondaryMinHeight = secondaryMeasurables.maxOfOrNull { it.minIntrinsicHeight(secondaryWidth) } ?: 0
val layoutHeight = maxOf(primaryHeight, secondaryMinHeight)
val secondaryPlaceables = secondaryMeasurables.map { m ->
m.measure(constraints.copy(minWidth = secondaryWidth, maxWidth = secondaryWidth, minHeight = layoutHeight, maxHeight = layoutHeight))
}
layout(constraints.maxWidth, layoutHeight) {
primaryPlaceables.forEach { it.placeRelative(0, 0) }
secondaryPlaceables.forEach { it.placeRelative(primaryWidth, 0) }
}
}
}
@Preview
@Composable
private fun SideBySideLayoutPreview() {
Previews.Preview {
SideBySideLayout(
primary = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
)
},
secondary = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Blue)
)
},
modifier = Modifier.fillMaxSize()
)
}
}
@@ -30,7 +30,7 @@ fun RegistrationScreen(
) {
SubcomposeLayout(modifier = modifier.imePadding()) { constraints ->
val footerPlaceables = footer?.let {
subcompose("footer", it).map { m -> m.measure(constraints.copy(minHeight = 0)) }
subcompose("footer", it).map { m -> m.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
} ?: emptyList()
val footerHeight = footerPlaceables.maxOfOrNull { it.height } ?: 0
@@ -10,11 +10,14 @@ package org.signal.registration.screens.welcome
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.SpaceAround
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -29,6 +32,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -53,11 +57,11 @@ import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SideBySideLayout
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.dismissWithAnimation
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.isWidthExpanded
import org.signal.core.ui.rememberWindowBreakpoint
import org.signal.registration.R
import org.signal.registration.screens.RegistrationScreen
@@ -149,20 +153,27 @@ private fun CompactLayout(
}
},
footer = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
contentAlignment = Alignment.Center
) {
TermsAndPrivacy(onTermsAndPrivacyClick = onTermsAndPrivacyClick)
Column(
modifier = Modifier.widthIn(max = 320.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TermsAndPrivacy(onTermsAndPrivacyClick = onTermsAndPrivacyClick)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(24.dp))
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
Spacer(modifier = Modifier.height(48.dp))
Spacer(modifier = Modifier.height(48.dp))
}
}
}
)
@@ -180,29 +191,45 @@ private fun MediumLayout(
.fillMaxSize()
.padding(bottom = 56.dp),
content = {
SideBySideLayout(
modifier = Modifier.fillMaxSize(),
primary = {
HeroImage()
},
secondary = {
Box {
Row(modifier = Modifier.fillMaxWidth()) {
HeroImage(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.padding(horizontal = 24.dp)
)
Headline(
modifier = Modifier.padding(top = 88.dp)
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
.padding(horizontal = 24.dp)
)
}
)
TermsAndPrivacy(
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
)
}
},
footer = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TermsAndPrivacy(onTermsAndPrivacyClick = onTermsAndPrivacyClick)
val isWidthExpanded = currentWindowAdaptiveInfo().windowSizeClass.isWidthExpanded
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Column(
modifier = Modifier
.widthIn(max = if (isWidthExpanded) 412.dp else 320.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
}
}
}
)
@@ -218,47 +245,49 @@ private fun LargeLayout(
RegistrationScreen(
modifier = modifier.fillMaxSize(),
content = {
SideBySideLayout(
modifier = Modifier.fillMaxSize(),
primary = {
HeroImage(
modifier = Modifier.padding(vertical = 10.dp)
)
},
secondary = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth()
Row(
horizontalArrangement = SpaceAround,
modifier = Modifier.padding(vertical = 56.dp)
) {
HeroImage(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.weight(1f)
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxHeight()
.widthIn(max = 320.dp)
.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.Start,
Headline(
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(77.dp))
TermsAndPrivacy(
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
modifier = Modifier
.widthIn(max = 380.dp)
.fillMaxWidth()
.padding(top = 98.dp)
) {
Headline(
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(start = 10.dp)
)
.align(Alignment.CenterHorizontally)
.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.weight(1f))
TermsAndPrivacy(
onTermsAndPrivacyClick = onTermsAndPrivacyClick,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 8.dp)
)
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
}
PrimaryDeviceCallToActionButtons(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick
)
}
}
)
}
}
)
}
@@ -310,7 +339,7 @@ private fun TermsAndPrivacy(
}
@Composable
private fun ColumnScope.PrimaryDeviceCallToActionButtons(
private fun PrimaryDeviceCallToActionButtons(
onEvent: (WelcomeScreenEvents) -> Unit,
onRestoreOrTransferClick: () -> Unit
) {
@@ -318,7 +347,6 @@ private fun ColumnScope.PrimaryDeviceCallToActionButtons(
onClick = { onEvent(WelcomeScreenEvents.Continue) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
.testTag(TestTags.WELCOME_GET_STARTED_BUTTON)
) {
Text(stringResource(R.string.RegistrationActivity_continue))
@@ -333,7 +361,6 @@ private fun ColumnScope.PrimaryDeviceCallToActionButtons(
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
.testTag(TestTags.WELCOME_RESTORE_OR_TRANSFER_BUTTON)
) {
Text(stringResource(R.string.registration_activity__restore_or_transfer))