Adaptive welcome screen with compact, medium, and large layouts.

This commit is contained in:
Alex Hart
2026-04-08 14:49:56 -03:00
committed by Greyson Parrelli
parent 38baf17938
commit d90e9919ae
10 changed files with 483 additions and 65 deletions

View File

@@ -6,6 +6,10 @@
package org.signal.core.ui
import android.content.res.Resources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
@@ -31,6 +35,16 @@ fun Resources.getWindowSizeClass(): WindowSizeClass {
)
}
@Composable
fun rememberWindowBreakpoint(): WindowBreakpoint {
val resources = LocalResources.current
val configuration = LocalConfiguration.current
return remember(resources, configuration) {
resources.getWindowBreakpoint()
}
}
/**
* Determines the device's form factor (PHONE, FOLDABLE, or TABLET) based on the current
* [Resources] and window size class.
@@ -39,7 +53,7 @@ fun Resources.getWindowSizeClass(): WindowSizeClass {
* - Returns [WindowBreakpoint.SMALL] if the width or height is compact.
* - Returns [WindowBreakpoint.LARGE] if the height is at least the expanded lower bound.
* - Returns [WindowBreakpoint.MEDIUM] if the width is at least the medium lower bound.
* - Otherwise, falls back to aspect ratio heuristics: wider (≥ 1.6) is [WindowBreakpoint.LARGE], else [WindowBreakpoint.MEDIUM].
* - Otherwise, falls back to aspect ratio heuristics: wider (≥ 1.5) is [WindowBreakpoint.LARGE], else [WindowBreakpoint.MEDIUM].
*
* @return the inferred [WindowBreakpoint] for the current device.
*/
@@ -58,7 +72,7 @@ fun Resources.getWindowBreakpoint(): WindowBreakpoint {
val denominator = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
val aspectRatio = numerator.toFloat() / denominator
return if (aspectRatio >= 1.6f) {
return if (aspectRatio >= 1.5f) {
WindowBreakpoint.LARGE
} else {
WindowBreakpoint.MEDIUM

View File

@@ -0,0 +1,74 @@
/*
* 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()
)
}
}

View File

@@ -100,10 +100,10 @@ annotation class TabletLandscapeDayPreview
@TabletLandscapeDayPreview
annotation class TabletDayPreviews
@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=portrait")
@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=portrait")
annotation class TabletPortraitNightPreview
@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=landscape")
@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=landscape")
annotation class TabletLandscapeNightPreview
@TabletPortraitNightPreview