From 3998f06cb45474d62114c604fefd06bc9054f247 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Mon, 11 May 2026 07:33:16 -0700 Subject: [PATCH] Scaffold / Appbar composable Custom composable that switched between compact and medium size app bar depending on a combination of orientation and screen size. (Small app bar when horizontal on small devices, medium app bar otherwise. Medium app bar supports collapse on scroll. --- .../features/netplay/ui/NetplayScreen.kt | 42 +++++++++-------- .../features/netplay/ui/NetplaySetupScreen.kt | 25 ++++------ .../dolphinemu/ui/theme/DolphinTheme.kt | 46 +++++++++++++++++++ 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index 54ea403eeb..ad454ec1a5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Intent import android.content.res.Configuration +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -44,6 +45,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExtendedFloatingActionButton @@ -53,11 +55,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -103,6 +102,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox @@ -140,19 +140,19 @@ fun NetplayScreen( gameDigestProgress: GameDigestProgress?, joinAddresses: Map, ) { - Scaffold( - topBar = { - MediumTopAppBar( - title = { Text(stringResource(R.string.netplay_title)) }, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) + val scrollState = rememberScrollState() + + DolphinScaffold( + title = { + Text(stringResource(R.string.netplay_title)) + }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } }, floatingActionButton = { if (isHosting) { @@ -202,6 +202,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + scrollState = scrollState, contentPadding = innerPadding, modifier = modifier ) @@ -228,6 +229,7 @@ fun NetplayScreen( joinAddresses = joinAddresses, selectedJoinInfoType = selectedJoinInfoType, onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it }, + scrollState = scrollState, contentPadding = innerPadding, modifier = modifier ) @@ -346,12 +348,13 @@ private fun PortraitContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + scrollState: ScrollState, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) .padding(contentPadding) ) { Chat( @@ -418,6 +421,7 @@ private fun LandscapeContent( joinAddresses: Map, selectedJoinInfoType: JoinInfoType, onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit, + scrollState: ScrollState, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { @@ -843,7 +847,7 @@ private fun JoinInfoDropdown( label = { Text(stringResource(R.string.netplay_host_address_label)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth() ) @@ -1001,7 +1005,7 @@ private fun NetworkModeDropdown( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), ) ExposedDropdownMenu( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index f7f660638d..ae222e8722 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -30,11 +30,9 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text @@ -57,6 +55,7 @@ import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType +import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer @@ -88,19 +87,15 @@ fun NetplaySetupScreen( onHostClicked: () -> Unit, onConnectClicked: () -> Unit, ) { - Scaffold( - topBar = { - MediumTopAppBar( - title = { Text(stringResource(R.string.netplay_setup_title)) }, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - ) + DolphinScaffold( + title = { Text(stringResource(R.string.netplay_setup_title)) }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } }, floatingActionButton = { ExtendedFloatingActionButton( diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt index c142a44d73..4aa7e2055a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt @@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.ui.theme import android.content.Context +import android.content.res.Configuration import androidx.annotation.AttrRes import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.ScrollState @@ -21,12 +22,16 @@ import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue.Hidden import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -41,6 +46,8 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle @@ -147,6 +154,45 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme { } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DolphinScaffold( + title: @Composable () -> Unit, + navigationIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, + floatingActionButton: @Composable () -> Unit = {}, + content: @Composable (PaddingValues) -> Unit, +) { + val isCompactLandscape = with(LocalConfiguration.current) { + orientation == Configuration.ORIENTATION_LANDSCAPE && smallestScreenWidthDp < 600 + } + + val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + Scaffold( + modifier = modifier.then( + if (isCompactLandscape) Modifier + else Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) + ), + topBar = { + if (isCompactLandscape) { + TopAppBar( + title = title, + navigationIcon = navigationIcon, + ) + } else { + MediumTopAppBar( + title = title, + navigationIcon = navigationIcon, + scrollBehavior = topAppBarScrollBehavior, + ) + } + }, + floatingActionButton = floatingActionButton, + content = content, + ) +} + @Composable fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp))