Remove custom WindowSizeClass and just depend on Material Adaptive WindowSizeClass.

Co-authored-by: jeffrey-signal <jeffrey@signal.org>
This commit is contained in:
Alex Hart
2025-10-31 12:50:33 -03:00
committed by jeffrey-signal
parent 95c9776b4d
commit 109f651681
32 changed files with 202 additions and 246 deletions

View File

@@ -5,8 +5,6 @@
package org.thoughtcrime.securesms.window
import android.content.res.Configuration
import android.content.res.Resources
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.background
@@ -16,7 +14,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -42,23 +39,17 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
import org.thoughtcrime.securesms.util.RemoteConfig
import kotlin.math.max
enum class Navigation {
@@ -68,141 +59,15 @@ enum class Navigation {
companion object {
@Composable
fun rememberNavigation(): Navigation {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(windowSizeClass) { windowSizeClass.navigation }
}
}
}
/**
* Describes the size of screen we are displaying, and what components should be displayed.
*
* Screens should utilize this class by convention instead of calling [currentWindowAdaptiveInfo]
* themselves, as this class includes checks with [RemoteConfig] to ensure we're allowed to display
* content in different screen sizes.
*
* https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
*/
enum class WindowSizeClass(
val navigation: Navigation
) {
COMPACT_PORTRAIT(Navigation.BAR),
COMPACT_LANDSCAPE(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.RAIL),
MEDIUM_LANDSCAPE(Navigation.RAIL),
EXTENDED_PORTRAIT(Navigation.RAIL),
EXTENDED_LANDSCAPE(Navigation.RAIL);
val listPaneDefaultPreferredWidth: Dp
get() = if (isExtended()) 416.dp else 316.dp
val detailPaneMaxContentWidth: Dp = 624.dp
val horizontalPartitionDefaultSpacerSize: Dp = 12.dp
fun isCompact(): Boolean = this == COMPACT_PORTRAIT || this == COMPACT_LANDSCAPE
fun isMedium(): Boolean = this == MEDIUM_PORTRAIT || this == MEDIUM_LANDSCAPE
fun isExtended(): Boolean = this == EXTENDED_PORTRAIT || this == EXTENDED_LANDSCAPE
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
fun isPortrait(): Boolean = !isLandscape()
@JvmOverloads
fun isSplitPane(
forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape
): Boolean {
return if (isLargeScreenSupportEnabled() && forceSplitPaneOnCompactLandscape) {
this != COMPACT_PORTRAIT
} else {
this.navigation != Navigation.BAR
}
}
companion object {
@OptIn(ExperimentalWindowCoreApi::class)
fun Resources.getWindowSizeClass(): WindowSizeClass {
val orientation = configuration.orientation
if (isForcedCompact()) {
return getCompactSizeClassForOrientation(orientation)
}
val windowSizeClass = androidx.window.core.layout.WindowSizeClass.compute(
displayMetrics.widthPixels,
displayMetrics.heightPixels,
displayMetrics.density
)
return getSizeClassForOrientationAndSystemSizeClass(orientation, windowSizeClass)
}
fun isLargeScreenSupportEnabled(): Boolean {
return RemoteConfig.largeScreenUi && SignalStore.internal.largeScreenUi
}
fun isForcedCompact(): Boolean {
return !isLargeScreenSupportEnabled()
}
@Composable
fun checkForcedCompact(): Boolean {
return !LocalInspectionMode.current && isForcedCompact()
}
@Composable
fun rememberWindowSizeClass(forceCompact: Boolean = checkForcedCompact()): WindowSizeClass {
val orientation = LocalConfiguration.current.orientation
if (forceCompact) {
return remember(orientation) {
getCompactSizeClassForOrientation(orientation)
return remember(windowSizeClass) {
if (windowSizeClass.isSplitPane() && windowSizeClass.windowHeightSizeClass.isAtLeast(WindowHeightSizeClass.MEDIUM)) {
RAIL
} else {
BAR
}
}
val wsc = currentWindowAdaptiveInfo().windowSizeClass
return remember(orientation, wsc) {
getSizeClassForOrientationAndSystemSizeClass(orientation, wsc)
}
}
private fun getCompactSizeClassForOrientation(orientation: Int): WindowSizeClass {
return when (orientation) {
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
COMPACT_PORTRAIT
}
Configuration.ORIENTATION_LANDSCAPE -> COMPACT_LANDSCAPE
else -> error("Unexpected orientation: $orientation")
}
}
private fun getSizeClassForOrientationAndSystemSizeClass(orientation: Int, windowSizeClass: androidx.window.core.layout.WindowSizeClass): WindowSizeClass {
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT || windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
return getCompactSizeClassForOrientation(orientation)
}
return when (orientation) {
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> COMPACT_PORTRAIT
WindowWidthSizeClass.MEDIUM -> MEDIUM_PORTRAIT
WindowWidthSizeClass.EXPANDED -> EXTENDED_PORTRAIT
else -> error("Unsupported.")
}
}
Configuration.ORIENTATION_LANDSCAPE -> {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> COMPACT_LANDSCAPE
WindowWidthSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
WindowWidthSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
else -> error("Unsupported.")
}
}
else -> error("Unexpected orientation: $orientation")
}
}
}
}
@@ -240,8 +105,8 @@ fun AppScaffold(
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default
) {
val isForcedCompact = WindowSizeClass.checkForcedCompact()
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isForcedCompact = !LocalInspectionMode.current && !isLargeScreenSupportEnabled()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
if (isForcedCompact) {
ListAndNavigation(
@@ -249,7 +114,6 @@ fun AppScaffold(
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass,
contentWindowInsets = contentWindowInsets,
modifier = modifier
)
@@ -314,7 +178,6 @@ fun AppScaffold(
listContent = secondaryContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass,
contentWindowInsets = WindowInsets() // parent scaffold already applies the necessary insets
)
}
@@ -380,10 +243,11 @@ private fun ListAndNavigation(
navRailContent: @Composable () -> Unit,
bottomNavContent: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit = {},
windowSizeClass: WindowSizeClass,
contentWindowInsets: WindowInsets,
modifier: Modifier = Modifier
) {
val navigation = Navigation.rememberNavigation()
Scaffold(
containerColor = Color.Transparent,
topBar = topBarContent,
@@ -394,9 +258,8 @@ private fun ListAndNavigation(
Row(
modifier = Modifier
.padding(paddingValues)
.then(if (windowSizeClass.isLandscape()) Modifier.displayCutoutPadding() else Modifier)
) {
if (windowSizeClass.navigation == Navigation.RAIL) {
if (navigation == Navigation.RAIL) {
navRailContent()
}
@@ -405,7 +268,7 @@ private fun ListAndNavigation(
listContent()
}
if (windowSizeClass.navigation == Navigation.BAR) {
if (navigation == Navigation.BAR) {
bottomNavContent()
}
}
@@ -418,11 +281,11 @@ private fun ListAndNavigation(
@Composable
private fun AppScaffoldPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
AppScaffold(
navigator = rememberAppScaffoldNavigator(
isSplitPane = windowSizeClass.navigation != Navigation.BAR,
isSplitPane = windowSizeClass.isSplitPane(),
defaultPanePreferredWidth = 416.dp,
horizontalPartitionSpacerSize = 16.dp
),

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Dp
import androidx.window.core.layout.WindowSizeClass
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
@@ -94,9 +95,9 @@ open class AppScaffoldNavigator<T> @RememberInComposition constructor(private va
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
windowSizeClass: WindowSizeClass = WindowSizeClass.rememberWindowSizeClass(),
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
isSplitPane: Boolean = windowSizeClass.isSplitPane(
forceSplitPaneOnCompactLandscape = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPaneOnCompactLandscape
forceSplitPane = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPane
),
horizontalPartitionSpacerSize: Dp = windowSizeClass.horizontalPartitionDefaultSpacerSize,
defaultPanePreferredWidth: Dp = windowSizeClass.listPaneDefaultPreferredWidth

View File

@@ -12,6 +12,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -29,7 +30,7 @@ import org.thoughtcrime.securesms.R
@Composable
private fun AppScaffoldWithTopBarPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
AppScaffold(
navigator = rememberAppScaffoldNavigator(),

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import android.content.res.Resources
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
val WindowSizeClass.listPaneDefaultPreferredWidth: Dp get() = if (windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.EXPANDED)) 416.dp else 316.dp
val WindowSizeClass.horizontalPartitionDefaultSpacerSize: Dp get() = 12.dp
val WindowSizeClass.detailPaneMaxContentWidth: Dp get() = 624.dp
fun WindowHeightSizeClass.isAtLeast(other: WindowHeightSizeClass): Boolean {
return hashCode() >= other.hashCode()
}
fun WindowWidthSizeClass.isAtLeast(other: WindowWidthSizeClass): Boolean {
return hashCode() >= other.hashCode()
}
/**
* Global check for large screen support, can be inlined after production release.
*/
fun isLargeScreenSupportEnabled(): Boolean {
return RemoteConfig.largeScreenUi && SignalStore.internal.largeScreenUi
}
@OptIn(ExperimentalWindowCoreApi::class)
fun Resources.getWindowSizeClass(): WindowSizeClass {
return WindowSizeClass.compute(displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.density)
}
/**
* Split Pane is enabled as long as the width size class is MEDIUM or greater
*/
@JvmOverloads
fun WindowSizeClass.isSplitPane(
forceSplitPane: Boolean = SignalStore.internal.forceSplitPane
): Boolean {
if (forceSplitPane) {
return true
}
return windowWidthSizeClass.isAtLeast(WindowWidthSizeClass.MEDIUM)
}