feat(ui): use toolbar instead of navigation bar for navigation

Closes: #111
This commit is contained in:
Nishant Mishra
2025-11-30 11:30:56 +05:30
parent 1a52bf53b9
commit 0dcaddf4fe
7 changed files with 682 additions and 525 deletions

View File

@@ -21,36 +21,56 @@ import android.content.Intent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.FloatingToolbarExitDirection
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.NavigationItemIconPosition
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ShortNavigationBar
import androidx.compose.material3.ShortNavigationBarArrangement
import androidx.compose.material3.ShortNavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
@@ -82,8 +102,13 @@ fun AppScreen(
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme val motionScheme = motionScheme
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val systemBarsInsets = WindowInsets.systemBars.asPaddingValues()
val cutoutInsets = WindowInsets.displayCutout.asPaddingValues()
val backStack = rememberNavBackStack(Screen.Timer) val backStack = rememberNavBackStack(Screen.Timer)
val toolbarScrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(
FloatingToolbarExitDirection.Bottom
)
if (uiState.alarmRinging) if (uiState.alarmRinging)
AlarmDialog { AlarmDialog {
@@ -99,46 +124,92 @@ fun AppScreen(
bottomBar = { bottomBar = {
AnimatedVisibility( AnimatedVisibility(
backStack.last() !is Screen.AOD, backStack.last() !is Screen.AOD,
enter = fadeIn(), enter = slideInVertically(motionScheme.slowSpatialSpec()) { it },
exit = fadeOut() exit = slideOutVertically(motionScheme.slowSpatialSpec()) { it }
) { ) {
val wide = remember { val wide = remember {
windowSizeClass.isWidthAtLeastBreakpoint( windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
) )
} }
ShortNavigationBar( Box(
arrangement = Modifier
if (wide) ShortNavigationBarArrangement.Centered .fillMaxWidth()
else ShortNavigationBarArrangement.EqualWeight .padding(
start = cutoutInsets.calculateStartPadding(layoutDirection),
end = cutoutInsets.calculateEndPadding(layoutDirection)
),
Alignment.Center
) { ) {
mainScreens.forEach { HorizontalFloatingToolbar(
val selected = backStack.last() == it.route expanded = true,
ShortNavigationBarItem( scrollBehavior = toolbarScrollBehavior,
selected = selected, colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens toolbarContainerColor = colorScheme.primary,
{ toolbarContentColor = colorScheme.onPrimary
if (backStack.size < 2) backStack.add(it.route) ),
else backStack[1] = it.route modifier = Modifier
.padding(
top = ScreenOffset,
bottom = systemBarsInsets.calculateBottomPadding()
+ ScreenOffset
)
.zIndex(1f)
) {
mainScreens.forEach { item ->
val selected = backStack.last() == item.route
ToggleButton(
checked = selected,
onCheckedChange = if (item.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
{
if (backStack.size < 2) backStack.add(item.route)
else backStack[1] = item.route
}
} else {
{ if (backStack.size > 1) backStack.removeAt(1) }
},
colors = ToggleButtonDefaults.toggleButtonColors(
containerColor = colorScheme.primary,
contentColor = colorScheme.onPrimary,
checkedContainerColor = colorScheme.primaryContainer,
checkedContentColor = colorScheme.onPrimaryContainer
),
shapes = ToggleButtonDefaults.shapes(
CircleShape,
CircleShape,
CircleShape
),
modifier = Modifier.height(56.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Crossfade(selected) { selected ->
if (selected) Icon(painterResource(item.selectedIcon), null)
else Icon(painterResource(item.unselectedIcon), null)
}
AnimatedVisibility(
selected || wide,
enter = fadeIn(motionScheme.defaultEffectsSpec()) + expandHorizontally(
motionScheme.defaultSpatialSpec()
),
exit = fadeOut(motionScheme.defaultEffectsSpec()) + shrinkHorizontally(
motionScheme.defaultSpatialSpec()
),
) {
Row {
Spacer(Modifier.width(ButtonDefaults.MediumIconSpacing))
Text(stringResource(item.label))
}
}
} }
} else { }
{ if (backStack.size > 1) backStack.removeAt(1) } }
},
icon = {
Crossfade(selected) { selected ->
if (selected) Icon(painterResource(it.selectedIcon), null)
else Icon(painterResource(it.unselectedIcon), null)
}
},
iconPosition =
if (wide) NavigationItemIconPosition.Start
else NavigationItemIconPosition.Top,
label = { Text(stringResource(it.label)) }
)
} }
} }
} }
} },
modifier = modifier
) { contentPadding -> ) { contentPadding ->
SharedTransitionLayout { SharedTransitionLayout {
NavDisplay( NavDisplay(
@@ -161,20 +232,12 @@ fun AppScreen(
TimerScreen( TimerScreen(
timerState = uiState, timerState = uiState,
isPlus = isPlus, isPlus = isPlus,
contentPadding = contentPadding,
progress = { progress }, progress = { progress },
onAction = timerViewModel::onAction, onAction = timerViewModel::onAction,
modifier = modifier modifier = if (isAODEnabled) Modifier.clickable {
.padding( if (backStack.size < 2) backStack.add(Screen.AOD)
start = contentPadding.calculateStartPadding(layoutDirection), } else Modifier
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
.then(
if (isAODEnabled) Modifier.clickable {
if (backStack.size < 2) backStack.add(Screen.AOD)
}
else Modifier
),
) )
} }
@@ -183,36 +246,21 @@ fun AppScreen(
timerState = uiState, timerState = uiState,
progress = { progress }, progress = { progress },
setTimerFrequency = setTimerFrequency, setTimerFrequency = setTimerFrequency,
modifier = Modifier modifier = if (isAODEnabled) Modifier.clickable {
.then( if (backStack.size > 1) backStack.removeLastOrNull()
if (isAODEnabled) Modifier.clickable { } else Modifier
if (backStack.size > 1) backStack.removeLastOrNull()
}
else Modifier
)
) )
} }
entry<Screen.Settings.Main> { entry<Screen.Settings.Main> {
SettingsScreenRoot( SettingsScreenRoot(
setShowPaywall = { showPaywall = it }, setShowPaywall = { showPaywall = it },
modifier = modifier.padding( contentPadding = contentPadding
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
) )
} }
entry<Screen.Stats> { entry<Screen.Stats> {
StatsScreenRoot( StatsScreenRoot(contentPadding = contentPadding)
contentPadding = contentPadding,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
)
} }
} }
) )

View File

@@ -27,8 +27,10 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -39,6 +41,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -55,6 +58,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -86,6 +90,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
@Composable @Composable
fun SettingsScreenRoot( fun SettingsScreenRoot(
setShowPaywall: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) { ) {
@@ -119,6 +124,7 @@ fun SettingsScreenRoot(
serviceRunning = serviceRunning, serviceRunning = serviceRunning,
settingsState = settingsState, settingsState = settingsState,
backStack = backStack, backStack = backStack,
contentPadding = contentPadding,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
@@ -137,6 +143,7 @@ private fun SettingsScreen(
serviceRunning: Boolean, serviceRunning: Boolean,
settingsState: SettingsState, settingsState: SettingsState,
backStack: SnapshotStateList<Screen.Settings>, backStack: SnapshotStateList<Screen.Settings>,
contentPadding: PaddingValues,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
@@ -146,6 +153,7 @@ private fun SettingsScreen(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val currentLocales = val currentLocales =
@@ -181,26 +189,36 @@ private fun SettingsScreen(
}, },
entryProvider = entryProvider { entryProvider = entryProvider {
entry<Screen.Settings.Main> { entry<Screen.Settings.Main> {
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Scaffold(
TopAppBar( topBar = {
title = { TopAppBar(
Text( title = {
stringResource(R.string.settings), Text(
style = LocalTextStyle.current.copy( stringResource(R.string.settings),
fontFamily = robotoFlexTopBar, style = LocalTextStyle.current.copy(
fontSize = 32.sp, fontFamily = robotoFlexTopBar,
lineHeight = 32.sp fontSize = 32.sp,
lineHeight = 32.sp
)
) )
) },
}, subtitle = {},
subtitle = {}, colors = topBarColors,
colors = topBarColors, titleHorizontalAlignment = Alignment.CenterHorizontally,
titleHorizontalAlignment = Alignment.CenterHorizontally, scrollBehavior = scrollBehavior
scrollBehavior = scrollBehavior )
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
contentPadding = insets,
modifier = Modifier modifier = Modifier
.background(topBarColors.containerColor) .background(topBarColors.containerColor)
.fillMaxSize() .fillMaxSize()
@@ -279,6 +297,7 @@ private fun SettingsScreen(
entry<Screen.Settings.Alarm> { entry<Screen.Settings.Alarm> {
AlarmSettings( AlarmSettings(
settingsState = settingsState, settingsState = settingsState,
contentPadding = contentPadding,
onAction = onAction, onAction = onAction,
onBack = backStack::removeLastOrNull, onBack = backStack::removeLastOrNull,
modifier = modifier, modifier = modifier,
@@ -287,6 +306,7 @@ private fun SettingsScreen(
entry<Screen.Settings.Appearance> { entry<Screen.Settings.Appearance> {
AppearanceSettings( AppearanceSettings(
settingsState = settingsState, settingsState = settingsState,
contentPadding = contentPadding,
isPlus = isPlus, isPlus = isPlus,
onAction = onAction, onAction = onAction,
setShowPaywall = setShowPaywall, setShowPaywall = setShowPaywall,
@@ -299,6 +319,7 @@ private fun SettingsScreen(
isPlus = isPlus, isPlus = isPlus,
serviceRunning = serviceRunning, serviceRunning = serviceRunning,
settingsState = settingsState, settingsState = settingsState,
contentPadding = contentPadding,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,

View File

@@ -28,8 +28,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -43,6 +45,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -57,6 +60,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -80,12 +84,14 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable @Composable
fun AlarmSettings( fun AlarmSettings(
settingsState: SettingsState, settingsState: SettingsState,
contentPadding: PaddingValues,
onAction: (SettingsAction) -> Unit, onAction: (SettingsAction) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current val context = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
var alarmName by remember { mutableStateOf("...") } var alarmName by remember { mutableStateOf("...") }
@@ -148,32 +154,42 @@ fun AlarmSettings(
) )
} }
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Scaffold(
LargeFlexibleTopAppBar( topBar = {
title = { LargeFlexibleTopAppBar(
Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar) title = {
}, Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar)
subtitle = { },
Text(stringResource(R.string.settings)) subtitle = {
}, Text(stringResource(R.string.settings))
navigationIcon = { },
FilledTonalIconButton( navigationIcon = {
onClick = onBack, FilledTonalIconButton(
shapes = IconButtonDefaults.shapes(), onClick = onBack,
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor) shapes = IconButtonDefaults.shapes(),
) { colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor)
Icon( ) {
painterResource(R.drawable.arrow_back), Icon(
null painterResource(R.drawable.arrow_back),
) null
} )
}, }
colors = topBarColors, },
scrollBehavior = scrollBehavior colors = topBarColors,
scrollBehavior = scrollBehavior
)
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
contentPadding = insets,
modifier = Modifier modifier = Modifier
.background(topBarColors.containerColor) .background(topBarColors.containerColor)
.fillMaxSize() .fillMaxSize()
@@ -248,6 +264,7 @@ fun AlarmSettingsPreview() {
val settingsState = SettingsState() val settingsState = SettingsState()
AlarmSettings( AlarmSettings(
settingsState = settingsState, settingsState = settingsState,
contentPadding = PaddingValues(),
onAction = {}, onAction = {},
onBack = {} onBack = {}
) )

View File

@@ -19,8 +19,10 @@ package org.nsh07.pomodoro.ui.settingsScreen.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -33,6 +35,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -41,6 +44,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -64,6 +68,7 @@ import org.nsh07.pomodoro.utils.toColor
@Composable @Composable
fun AppearanceSettings( fun AppearanceSettings(
settingsState: SettingsState, settingsState: SettingsState,
contentPadding: PaddingValues,
isPlus: Boolean, isPlus: Boolean,
onAction: (SettingsAction) -> Unit, onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
@@ -71,33 +76,44 @@ fun AppearanceSettings(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val layoutDirection = LocalLayoutDirection.current
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Scaffold(
LargeFlexibleTopAppBar( topBar = {
title = { LargeFlexibleTopAppBar(
Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar) title = {
}, Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar)
subtitle = { },
Text(stringResource(R.string.settings)) subtitle = {
}, Text(stringResource(R.string.settings))
navigationIcon = { },
FilledTonalIconButton( navigationIcon = {
onClick = onBack, FilledTonalIconButton(
shapes = IconButtonDefaults.shapes(), onClick = onBack,
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor) shapes = IconButtonDefaults.shapes(),
) { colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor)
Icon( ) {
painterResource(R.drawable.arrow_back), Icon(
null painterResource(R.drawable.arrow_back),
) null
} )
}, }
colors = topBarColors, },
scrollBehavior = scrollBehavior colors = topBarColors,
scrollBehavior = scrollBehavior
)
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
contentPadding = insets,
modifier = Modifier modifier = Modifier
.background(topBarColors.containerColor) .background(topBarColors.containerColor)
.fillMaxSize() .fillMaxSize()
@@ -182,6 +198,7 @@ fun AppearanceSettingsPreview() {
TomatoTheme(dynamicColor = false) { TomatoTheme(dynamicColor = false) {
AppearanceSettings( AppearanceSettings(
settingsState = settingsState, settingsState = settingsState,
contentPadding = PaddingValues(),
isPlus = false, isPlus = false,
onAction = {}, onAction = {},
setShowPaywall = {}, setShowPaywall = {},

View File

@@ -27,8 +27,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -53,6 +56,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -71,6 +75,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -98,6 +103,7 @@ fun TimerSettings(
isPlus: Boolean, isPlus: Boolean,
serviceRunning: Boolean, serviceRunning: Boolean,
settingsState: SettingsState, settingsState: SettingsState,
contentPadding: PaddingValues,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
@@ -109,6 +115,7 @@ fun TimerSettings(
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current val context = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
val appName = stringResource(R.string.app_name) val appName = stringResource(R.string.app_name)
val notificationManagerService = val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
@@ -141,32 +148,42 @@ fun TimerSettings(
) )
) )
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Scaffold(
LargeFlexibleTopAppBar( topBar = {
title = { LargeFlexibleTopAppBar(
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar) title = {
}, Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
subtitle = { },
Text(stringResource(R.string.settings)) subtitle = {
}, Text(stringResource(R.string.settings))
navigationIcon = { },
FilledTonalIconButton( navigationIcon = {
onClick = onBack, FilledTonalIconButton(
shapes = IconButtonDefaults.shapes(), onClick = onBack,
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor) shapes = IconButtonDefaults.shapes(),
) { colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor)
Icon( ) {
painterResource(R.drawable.arrow_back), Icon(
null painterResource(R.drawable.arrow_back),
) null
} )
}, }
colors = topBarColors, },
scrollBehavior = scrollBehavior colors = topBarColors,
scrollBehavior = scrollBehavior
)
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
contentPadding = insets,
modifier = Modifier modifier = Modifier
.background(topBarColors.containerColor) .background(topBarColors.containerColor)
.fillMaxSize() .fillMaxSize()
@@ -428,6 +445,7 @@ private fun TimerSettingsPreview() {
isPlus = false, isPlus = false,
serviceRunning = true, serviceRunning = true,
settingsState = remember { SettingsState() }, settingsState = remember { SettingsState() },
contentPadding = PaddingValues(),
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,

View File

@@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -45,6 +47,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -61,6 +64,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -129,6 +133,7 @@ fun StatsScreen(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val layoutDirection = LocalLayoutDirection.current
val hoursFormat = stringResource(R.string.hours_format) val hoursFormat = stringResource(R.string.hours_format)
val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format) val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format)
@@ -160,43 +165,50 @@ fun StatsScreen(
val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface } val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface }
val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface } val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
Column( Scaffold(
horizontalAlignment = Alignment.CenterHorizontally, topBar = {
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) TopAppBar(
) { title = {
TopAppBar( Text(
title = { stringResource(R.string.stats),
Text( style = LocalTextStyle.current.copy(
stringResource(R.string.stats), fontFamily = robotoFlexTopBar,
style = LocalTextStyle.current.copy( fontSize = 32.sp,
fontFamily = robotoFlexTopBar, lineHeight = 32.sp
fontSize = 32.sp, ),
lineHeight = 32.sp modifier = Modifier
), .padding(top = contentPadding.calculateTopPadding())
modifier = Modifier .padding(vertical = 14.dp)
.padding(top = contentPadding.calculateTopPadding()) )
.padding(vertical = 14.dp) },
) actions = if (BuildConfig.DEBUG) {
}, {
actions = if (BuildConfig.DEBUG) { IconButton(
{ onClick = generateSampleData
IconButton( ) {
onClick = generateSampleData Spacer(Modifier.size(24.dp))
) { }
Spacer(Modifier.size(24.dp))
} }
} } else {
} else { {}
{} },
}, subtitle = {},
subtitle = {}, titleHorizontalAlignment = Alignment.CenterHorizontally,
titleHorizontalAlignment = Alignment.CenterHorizontally, scrollBehavior = scrollBehavior,
scrollBehavior = scrollBehavior, windowInsets = WindowInsets()
windowInsets = WindowInsets() )
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = insets,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
item { Spacer(Modifier) } item { Spacer(Modifier) }

View File

@@ -38,8 +38,11 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -47,8 +50,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ButtonGroup
import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@@ -64,6 +66,7 @@ import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.shapes import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -84,6 +87,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -107,12 +111,14 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
fun SharedTransitionScope.TimerScreen( fun SharedTransitionScope.TimerScreen(
timerState: TimerState, timerState: TimerState,
isPlus: Boolean, isPlus: Boolean,
contentPadding: PaddingValues,
progress: () -> Float, progress: () -> Float,
onAction: (TimerAction) -> Unit, onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val motionScheme = motionScheme val motionScheme = motionScheme
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val layoutDirection = LocalLayoutDirection.current
val color by animateColorAsState( val color by animateColorAsState(
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
@@ -137,407 +143,424 @@ fun SharedTransitionScope.TimerScreen(
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Column(modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Scaffold(
TopAppBar( topBar = {
title = { TopAppBar(
AnimatedContent( title = {
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND, AnimatedContent(
transitionSpec = { if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
slideInVertically( transitionSpec = {
animationSpec = motionScheme.defaultSpatialSpec(), slideInVertically(
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(), animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() } initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() }
)
) )
) },
}, contentAlignment = Alignment.Center,
contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth(.9f)
modifier = Modifier.fillMaxWidth(.9f) ) {
) { when (it) {
when (it) { TimerMode.BRAND ->
TimerMode.BRAND -> Text(
Text( if (!isPlus) stringResource(R.string.app_name)
if (!isPlus) stringResource(R.string.app_name) else stringResource(R.string.app_name_plus),
else stringResource(R.string.app_name_plus), style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.error
),
textAlign = TextAlign.Center
)
TimerMode.FOCUS ->
Text(
stringResource(R.string.focus),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.primary
),
textAlign = TextAlign.Center
)
TimerMode.SHORT_BREAK -> Text(
stringResource(R.string.short_break),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
color = colorScheme.error color = colorScheme.tertiary
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
TimerMode.FOCUS -> TimerMode.LONG_BREAK -> Text(
Text( stringResource(R.string.long_break),
stringResource(R.string.focus),
style = TextStyle( style = TextStyle(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
color = colorScheme.primary color = colorScheme.tertiary
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
}
TimerMode.SHORT_BREAK -> Text(
stringResource(R.string.short_break),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.tertiary
),
textAlign = TextAlign.Center
)
TimerMode.LONG_BREAK -> Text(
stringResource(R.string.long_break),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.tertiary
),
textAlign = TextAlign.Center
)
} }
} },
}, subtitle = {},
subtitle = {}, titleHorizontalAlignment = CenterHorizontally,
titleHorizontalAlignment = CenterHorizontally, scrollBehavior = scrollBehavior
scrollBehavior = scrollBehavior )
},
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = PaddingValues(
bottom = contentPadding.calculateBottomPadding(),
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
) )
LazyColumn(
Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = CenterHorizontally, horizontalAlignment = CenterHorizontally,
contentPadding = insets,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState())
) { ) {
Column(horizontalAlignment = CenterHorizontally) { item {
Box(contentAlignment = Alignment.Center) { Column(horizontalAlignment = CenterHorizontally) {
if (timerState.timerMode == TimerMode.FOCUS) { Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator( if (timerState.timerMode == TimerMode.FOCUS) {
progress = progress, CircularProgressIndicator(
modifier = Modifier progress = progress,
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"focus progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
color = color,
trackColor = colorContainer,
strokeWidth = 16.dp,
gapSize = 8.dp
)
} else {
CircularWavyProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"break progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
color = color,
trackColor = colorContainer,
stroke = Stroke(
width = with(LocalDensity.current) {
16.dp.toPx()
},
cap = StrokeCap.Round,
),
trackStroke = Stroke(
width = with(LocalDensity.current) {
16.dp.toPx()
},
cap = StrokeCap.Round,
),
wavelength = 60.dp,
gapSize = 8.dp
)
}
var expanded by remember { mutableStateOf(timerState.showBrandTitle) }
Column(
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.clip(shapes.largeIncreased)
.clickable(onClick = { expanded = !expanded })
) {
LaunchedEffect(timerState.showBrandTitle) {
expanded = timerState.showBrandTitle
}
Text(
text = timerState.timeStr,
style = TextStyle(
fontFamily = googleFlex600,
fontSize = 72.sp,
letterSpacing = (-2.6).sp,
fontFeatureSettings = "tnum"
),
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
)
AnimatedVisibility(
expanded,
enter = fadeIn(motionScheme.defaultEffectsSpec()) +
expandVertically(motionScheme.defaultSpatialSpec()),
exit = fadeOut(motionScheme.defaultEffectsSpec()) +
shrinkVertically(motionScheme.defaultSpatialSpec())
) {
Text(
stringResource(
R.string.timer_session_count,
timerState.currentFocusCount,
timerState.totalFocusCount
),
fontFamily = googleFlex600,
style = typography.titleLarge,
color = colorScheme.outline
)
}
}
}
val interactionSources = remember { List(3) { MutableInteractionSource() } }
ButtonGroup(
overflowIndicator = { state ->
ButtonGroupDefaults.OverflowIndicator(
state,
colors = IconButtonDefaults.filledTonalIconButtonColors(),
modifier = Modifier.size(64.dp, 96.dp)
)
},
modifier = Modifier.padding(16.dp)
) {
customItem(
{
FilledIconToggleButton(
onCheckedChange = { checked ->
onAction(TimerAction.ToggleTimer)
if (checked) haptic.performHapticFeedback(HapticFeedbackType.ToggleOn)
else haptic.performHapticFeedback(HapticFeedbackType.ToggleOff)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
},
checked = timerState.timerRunning,
colors = IconButtonDefaults.filledIconToggleButtonColors(
checkedContainerColor = color,
checkedContentColor = onColor
),
shapes = IconButtonDefaults.toggleableShapes(),
interactionSource = interactionSources[0],
modifier = Modifier modifier = Modifier
.size(width = 128.dp, height = 96.dp) .sharedBounds(
.animateWidth(interactionSources[0]) sharedContentState = this@TimerScreen.rememberSharedContentState(
) { "focus progress"
if (timerState.timerRunning) { ),
Icon( animatedVisibilityScope = LocalNavAnimatedContentScope.current
painterResource(R.drawable.pause_large),
contentDescription = stringResource(R.string.pause),
modifier = Modifier.size(32.dp)
) )
} else { .widthIn(max = 350.dp)
Icon( .fillMaxWidth(0.9f)
painterResource(R.drawable.play_large), .aspectRatio(1f),
contentDescription = stringResource(R.string.play), color = color,
modifier = Modifier.size(32.dp) trackColor = colorContainer,
strokeWidth = 16.dp,
gapSize = 8.dp
)
} else {
CircularWavyProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"break progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
) )
} .widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
color = color,
trackColor = colorContainer,
stroke = Stroke(
width = with(LocalDensity.current) {
16.dp.toPx()
},
cap = StrokeCap.Round,
),
trackStroke = Stroke(
width = with(LocalDensity.current) {
16.dp.toPx()
},
cap = StrokeCap.Round,
),
wavelength = 60.dp,
gapSize = 8.dp
)
}
var expanded by remember { mutableStateOf(timerState.showBrandTitle) }
Column(
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.clip(shapes.largeIncreased)
.clickable(onClick = { expanded = !expanded })
) {
LaunchedEffect(timerState.showBrandTitle) {
expanded = timerState.showBrandTitle
} }
Text(
text = timerState.timeStr,
style = TextStyle(
fontFamily = googleFlex600,
fontSize = 72.sp,
letterSpacing = (-2.6).sp,
fontFeatureSettings = "tnum"
),
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"clock"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
)
AnimatedVisibility(
expanded,
enter = fadeIn(motionScheme.defaultEffectsSpec()) +
expandVertically(motionScheme.defaultSpatialSpec()),
exit = fadeOut(motionScheme.defaultEffectsSpec()) +
shrinkVertically(motionScheme.defaultSpatialSpec())
) {
Text(
stringResource(
R.string.timer_session_count,
timerState.currentFocusCount,
timerState.totalFocusCount
),
fontFamily = googleFlex600,
style = typography.titleLarge,
color = colorScheme.outline
)
}
}
}
val interactionSources = remember { List(3) { MutableInteractionSource() } }
ButtonGroup(
overflowIndicator = { state ->
ButtonGroupDefaults.OverflowIndicator(
state,
colors = IconButtonDefaults.filledTonalIconButtonColors(),
modifier = Modifier.size(64.dp, 96.dp)
)
}, },
{ state -> modifier = Modifier.padding(16.dp)
DropdownMenuItem( ) {
leadingIcon = { customItem(
{
FilledIconToggleButton(
onCheckedChange = { checked ->
onAction(TimerAction.ToggleTimer)
if (checked) haptic.performHapticFeedback(HapticFeedbackType.ToggleOn)
else haptic.performHapticFeedback(HapticFeedbackType.ToggleOff)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
},
checked = timerState.timerRunning,
colors = IconButtonDefaults.filledIconToggleButtonColors(
checkedContainerColor = color,
checkedContentColor = onColor
),
shapes = IconButtonDefaults.toggleableShapes(),
interactionSource = interactionSources[0],
modifier = Modifier
.size(width = 128.dp, height = 96.dp)
.animateWidth(interactionSources[0])
) {
if (timerState.timerRunning) { if (timerState.timerRunning) {
Icon( Icon(
painterResource(R.drawable.pause), painterResource(R.drawable.pause_large),
contentDescription = stringResource(R.string.pause) contentDescription = stringResource(R.string.pause),
modifier = Modifier.size(32.dp)
) )
} else { } else {
Icon( Icon(
painterResource(R.drawable.play), painterResource(R.drawable.play_large),
contentDescription = stringResource(R.string.play) contentDescription = stringResource(R.string.play),
modifier = Modifier.size(32.dp)
) )
} }
}, }
text = { },
Text( { state ->
if (timerState.timerRunning) stringResource(R.string.pause) else stringResource( DropdownMenuItem(
R.string.play leadingIcon = {
if (timerState.timerRunning) {
Icon(
painterResource(R.drawable.pause),
contentDescription = stringResource(R.string.pause)
)
} else {
Icon(
painterResource(R.drawable.play),
contentDescription = stringResource(R.string.play)
)
}
},
text = {
Text(
if (timerState.timerRunning) stringResource(R.string.pause) else stringResource(
R.string.play
)
) )
) },
}, onClick = {
onClick = { onAction(TimerAction.ToggleTimer)
onAction(TimerAction.ToggleTimer) state.dismiss()
state.dismiss() }
}
)
}
)
customItem(
{
FilledTonalIconButton(
onClick = {
onAction(TimerAction.ResetTimer)
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
},
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
shapes = IconButtonDefaults.shapes(),
interactionSource = interactionSources[1],
modifier = Modifier
.size(96.dp)
.animateWidth(interactionSources[1])
) {
Icon(
painterResource(R.drawable.restart_large),
contentDescription = stringResource(R.string.restart),
modifier = Modifier.size(32.dp)
) )
} }
}, )
{ state ->
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.restart),
stringResource(R.string.restart)
)
},
text = { Text(stringResource(R.string.restart)) },
onClick = {
onAction(TimerAction.ResetTimer)
state.dismiss()
}
)
}
)
customItem( customItem(
{ {
FilledTonalIconButton( FilledTonalIconButton(
onClick = { onClick = {
onAction(TimerAction.SkipTimer(fromButton = true)) onAction(TimerAction.ResetTimer)
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
}, },
colors = IconButtonDefaults.filledTonalIconButtonColors( colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer containerColor = colorContainer
), ),
shapes = IconButtonDefaults.shapes(), shapes = IconButtonDefaults.shapes(),
interactionSource = interactionSources[2], interactionSource = interactionSources[1],
modifier = Modifier modifier = Modifier
.size(64.dp, 96.dp) .size(96.dp)
.animateWidth(interactionSources[2]) .animateWidth(interactionSources[1])
) { ) {
Icon( Icon(
painterResource(R.drawable.skip_next_large), painterResource(R.drawable.restart_large),
contentDescription = stringResource(R.string.skip_to_next), contentDescription = stringResource(R.string.restart),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
)
}
},
{ state ->
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.restart),
stringResource(R.string.restart)
)
},
text = { Text(stringResource(R.string.restart)) },
onClick = {
onAction(TimerAction.ResetTimer)
state.dismiss()
}
) )
} }
}, )
{ state ->
DropdownMenuItem( customItem(
leadingIcon = { {
FilledTonalIconButton(
onClick = {
onAction(TimerAction.SkipTimer(fromButton = true))
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
},
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
shapes = IconButtonDefaults.shapes(),
interactionSource = interactionSources[2],
modifier = Modifier
.size(64.dp, 96.dp)
.animateWidth(interactionSources[2])
) {
Icon( Icon(
painterResource(R.drawable.skip_next), painterResource(R.drawable.skip_next_large),
stringResource(R.string.skip_to_next) contentDescription = stringResource(R.string.skip_to_next),
modifier = Modifier.size(32.dp)
) )
},
text = { Text(stringResource(R.string.skip_to_next)) },
onClick = {
onAction(TimerAction.SkipTimer(fromButton = true))
state.dismiss()
} }
) },
} { state ->
) DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.skip_next),
stringResource(R.string.skip_to_next)
)
},
text = { Text(stringResource(R.string.skip_to_next)) },
onClick = {
onAction(TimerAction.SkipTimer(fromButton = true))
state.dismiss()
}
)
}
)
}
} }
} }
Spacer(Modifier.height(32.dp)) item { Spacer(Modifier.height(32.dp)) }
Column(horizontalAlignment = CenterHorizontally) { item {
Text(stringResource(R.string.up_next), style = typography.titleSmall) Column(horizontalAlignment = CenterHorizontally) {
AnimatedContent( Text(stringResource(R.string.up_next), style = typography.titleSmall)
timerState.nextTimeStr, AnimatedContent(
transitionSpec = { timerState.nextTimeStr,
slideInVertically( transitionSpec = {
animationSpec = motionScheme.defaultSpatialSpec(), slideInVertically(
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(), animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() } initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() }
)
) )
}
) {
Text(
it,
style = TextStyle(
fontFamily = googleFlex600,
fontSize = 22.sp,
lineHeight = 28.sp,
color = if (timerState.nextTimerMode == TimerMode.FOCUS) colorScheme.primary else colorScheme.tertiary,
textAlign = TextAlign.Center
),
modifier = Modifier.width(200.dp)
) )
} }
) { AnimatedContent(
Text( timerState.nextTimerMode,
it, transitionSpec = {
style = TextStyle( slideInVertically(
fontFamily = googleFlex600,
fontSize = 22.sp,
lineHeight = 28.sp,
color = if (timerState.nextTimerMode == TimerMode.FOCUS) colorScheme.primary else colorScheme.tertiary,
textAlign = TextAlign.Center
),
modifier = Modifier.width(200.dp)
)
}
AnimatedContent(
timerState.nextTimerMode,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(), animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() } initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
targetOffsetY = { (it * 1.25).toInt() }
)
) )
}
) {
Text(
when (it) {
TimerMode.FOCUS -> stringResource(R.string.focus)
TimerMode.SHORT_BREAK -> stringResource(R.string.short_break)
else -> stringResource(R.string.long_break)
},
style = typography.titleMediumEmphasized,
textAlign = TextAlign.Center,
modifier = Modifier.width(200.dp)
) )
} }
) {
Text(
when (it) {
TimerMode.FOCUS -> stringResource(R.string.focus)
TimerMode.SHORT_BREAK -> stringResource(R.string.short_break)
else -> stringResource(R.string.long_break)
},
style = typography.titleMediumEmphasized,
textAlign = TextAlign.Center,
modifier = Modifier.width(200.dp)
)
} }
} }
Spacer(Modifier.height(16.dp)) item { Spacer(Modifier.height(16.dp)) }
} }
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview( @Preview(
showSystemUi = true, showSystemUi = true,
device = Devices.PIXEL_9_PRO device = Devices.PIXEL_9_PRO
@@ -553,6 +576,7 @@ fun TimerScreenPreview() {
TimerScreen( TimerScreen(
timerState, timerState,
isPlus = true, isPlus = true,
contentPadding = PaddingValues(),
{ 0.3f }, { 0.3f },
{} {}
) )