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.Crossfade
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
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.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.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.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.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.NavigationItemIconPosition
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.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
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.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
@@ -82,8 +102,13 @@ fun AppScreen(
val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val systemBarsInsets = WindowInsets.systemBars.asPaddingValues()
val cutoutInsets = WindowInsets.displayCutout.asPaddingValues()
val backStack = rememberNavBackStack(Screen.Timer)
val toolbarScrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(
FloatingToolbarExitDirection.Bottom
)
if (uiState.alarmRinging)
AlarmDialog {
@@ -99,46 +124,92 @@ fun AppScreen(
bottomBar = {
AnimatedVisibility(
backStack.last() !is Screen.AOD,
enter = fadeIn(),
exit = fadeOut()
enter = slideInVertically(motionScheme.slowSpatialSpec()) { it },
exit = slideOutVertically(motionScheme.slowSpatialSpec()) { it }
) {
val wide = remember {
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
}
ShortNavigationBar(
arrangement =
if (wide) ShortNavigationBarArrangement.Centered
else ShortNavigationBarArrangement.EqualWeight
Box(
Modifier
.fillMaxWidth()
.padding(
start = cutoutInsets.calculateStartPadding(layoutDirection),
end = cutoutInsets.calculateEndPadding(layoutDirection)
),
Alignment.Center
) {
mainScreens.forEach {
val selected = backStack.last() == it.route
ShortNavigationBarItem(
selected = selected,
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
{
if (backStack.size < 2) backStack.add(it.route)
else backStack[1] = it.route
HorizontalFloatingToolbar(
expanded = true,
scrollBehavior = toolbarScrollBehavior,
colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(
toolbarContainerColor = colorScheme.primary,
toolbarContentColor = colorScheme.onPrimary
),
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 ->
SharedTransitionLayout {
NavDisplay(
@@ -161,20 +232,12 @@ fun AppScreen(
TimerScreen(
timerState = uiState,
isPlus = isPlus,
contentPadding = contentPadding,
progress = { progress },
onAction = timerViewModel::onAction,
modifier = modifier
.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
.then(
if (isAODEnabled) Modifier.clickable {
if (backStack.size < 2) backStack.add(Screen.AOD)
}
else Modifier
),
modifier = if (isAODEnabled) Modifier.clickable {
if (backStack.size < 2) backStack.add(Screen.AOD)
} else Modifier
)
}
@@ -183,36 +246,21 @@ fun AppScreen(
timerState = uiState,
progress = { progress },
setTimerFrequency = setTimerFrequency,
modifier = Modifier
.then(
if (isAODEnabled) Modifier.clickable {
if (backStack.size > 1) backStack.removeLastOrNull()
}
else Modifier
)
modifier = if (isAODEnabled) Modifier.clickable {
if (backStack.size > 1) backStack.removeLastOrNull()
} else Modifier
)
}
entry<Screen.Settings.Main> {
SettingsScreenRoot(
setShowPaywall = { showPaywall = it },
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
contentPadding = contentPadding
)
}
entry<Screen.Stats> {
StatsScreenRoot(
contentPadding = contentPadding,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
)
StatsScreenRoot(contentPadding = contentPadding)
}
}
)

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -53,6 +56,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.material3.Switch
@@ -71,6 +75,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -98,6 +103,7 @@ fun TimerSettings(
isPlus: Boolean,
serviceRunning: Boolean,
settingsState: SettingsState,
contentPadding: PaddingValues,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
@@ -109,6 +115,7 @@ fun TimerSettings(
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
val appName = stringResource(R.string.app_name)
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
@@ -141,32 +148,42 @@ fun TimerSettings(
)
)
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text(stringResource(R.string.settings))
},
navigationIcon = {
FilledTonalIconButton(
onClick = onBack,
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor)
) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
},
colors = topBarColors,
scrollBehavior = scrollBehavior
Scaffold(
topBar = {
LargeFlexibleTopAppBar(
title = {
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text(stringResource(R.string.settings))
},
navigationIcon = {
FilledTonalIconButton(
onClick = onBack,
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = listItemColors.containerColor)
) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
},
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(
verticalArrangement = Arrangement.spacedBy(2.dp),
contentPadding = insets,
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
@@ -428,6 +445,7 @@ private fun TimerSettingsPreview() {
isPlus = false,
serviceRunning = true,
settingsState = remember { SettingsState() },
contentPadding = PaddingValues(),
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
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.Spacer
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.height
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.motionScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -61,6 +64,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -129,6 +133,7 @@ fun StatsScreen(
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val layoutDirection = LocalLayoutDirection.current
val hoursFormat = stringResource(R.string.hours_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 markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
TopAppBar(
title = {
Text(
stringResource(R.string.stats),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
),
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.padding(vertical = 14.dp)
)
},
actions = if (BuildConfig.DEBUG) {
{
IconButton(
onClick = generateSampleData
) {
Spacer(Modifier.size(24.dp))
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.stats),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
),
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.padding(vertical = 14.dp)
)
},
actions = if (BuildConfig.DEBUG) {
{
IconButton(
onClick = generateSampleData
) {
Spacer(Modifier.size(24.dp))
}
}
}
} else {
{}
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior,
windowInsets = WindowInsets()
} else {
{}
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior,
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(
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = insets,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
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.fillMaxWidth
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.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ButtonGroup
import androidx.compose.material3.ButtonGroupDefaults
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.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -107,12 +111,14 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
fun SharedTransitionScope.TimerScreen(
timerState: TimerState,
isPlus: Boolean,
contentPadding: PaddingValues,
progress: () -> Float,
onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier
) {
val motionScheme = motionScheme
val haptic = LocalHapticFeedback.current
val layoutDirection = LocalLayoutDirection.current
val color by animateColorAsState(
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
@@ -137,407 +143,424 @@ fun SharedTransitionScope.TimerScreen(
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Column(modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
title = {
AnimatedContent(
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
Scaffold(
topBar = {
TopAppBar(
title = {
AnimatedContent(
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
transitionSpec = {
slideInVertically(
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,
modifier = Modifier.fillMaxWidth(.9f)
) {
when (it) {
TimerMode.BRAND ->
Text(
if (!isPlus) stringResource(R.string.app_name)
else stringResource(R.string.app_name_plus),
},
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth(.9f)
) {
when (it) {
TimerMode.BRAND ->
Text(
if (!isPlus) stringResource(R.string.app_name)
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(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.error
color = colorScheme.tertiary
),
textAlign = TextAlign.Center
)
TimerMode.FOCUS ->
Text(
stringResource(R.string.focus),
TimerMode.LONG_BREAK -> Text(
stringResource(R.string.long_break),
style = TextStyle(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.primary
color = colorScheme.tertiary
),
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 = {},
titleHorizontalAlignment = CenterHorizontally,
scrollBehavior = scrollBehavior
},
subtitle = {},
titleHorizontalAlignment = CenterHorizontally,
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)
)
Column(
LazyColumn(
verticalArrangement = Arrangement.Center,
horizontalAlignment = CenterHorizontally,
contentPadding = insets,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Column(horizontalAlignment = CenterHorizontally) {
Box(contentAlignment = Alignment.Center) {
if (timerState.timerMode == TimerMode.FOCUS) {
CircularProgressIndicator(
progress = progress,
modifier = Modifier
.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],
item {
Column(horizontalAlignment = CenterHorizontally) {
Box(contentAlignment = Alignment.Center) {
if (timerState.timerMode == TimerMode.FOCUS) {
CircularProgressIndicator(
progress = progress,
modifier = Modifier
.size(width = 128.dp, height = 96.dp)
.animateWidth(interactionSources[0])
) {
if (timerState.timerRunning) {
Icon(
painterResource(R.drawable.pause_large),
contentDescription = stringResource(R.string.pause),
modifier = Modifier.size(32.dp)
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"focus progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
} else {
Icon(
painterResource(R.drawable.play_large),
contentDescription = stringResource(R.string.play),
modifier = Modifier.size(32.dp)
.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)
)
},
{ state ->
DropdownMenuItem(
leadingIcon = {
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
.size(width = 128.dp, height = 96.dp)
.animateWidth(interactionSources[0])
) {
if (timerState.timerRunning) {
Icon(
painterResource(R.drawable.pause),
contentDescription = stringResource(R.string.pause)
painterResource(R.drawable.pause_large),
contentDescription = stringResource(R.string.pause),
modifier = Modifier.size(32.dp)
)
} else {
Icon(
painterResource(R.drawable.play),
contentDescription = stringResource(R.string.play)
painterResource(R.drawable.play_large),
contentDescription = stringResource(R.string.play),
modifier = Modifier.size(32.dp)
)
}
},
text = {
Text(
if (timerState.timerRunning) stringResource(R.string.pause) else stringResource(
R.string.play
}
},
{ state ->
DropdownMenuItem(
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 = {
onAction(TimerAction.ToggleTimer)
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)
},
onClick = {
onAction(TimerAction.ToggleTimer)
state.dismiss()
}
)
}
},
{ 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(
{
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(
painterResource(R.drawable.skip_next_large),
contentDescription = stringResource(R.string.skip_to_next),
modifier = Modifier.size(32.dp)
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()
}
)
}
},
{ state ->
DropdownMenuItem(
leadingIcon = {
)
customItem(
{
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(
painterResource(R.drawable.skip_next),
stringResource(R.string.skip_to_next)
painterResource(R.drawable.skip_next_large),
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) {
Text(stringResource(R.string.up_next), style = typography.titleSmall)
AnimatedContent(
timerState.nextTimeStr,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
item {
Column(horizontalAlignment = CenterHorizontally) {
Text(stringResource(R.string.up_next), style = typography.titleSmall)
AnimatedContent(
timerState.nextTimeStr,
transitionSpec = {
slideInVertically(
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)
)
}
) {
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(
timerState.nextTimerMode,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.defaultSpatialSpec(),
initialOffsetY = { (-it * 1.25).toInt() }
).togetherWith(
slideOutVertically(
AnimatedContent(
timerState.nextTimerMode,
transitionSpec = {
slideInVertically(
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(
showSystemUi = true,
device = Devices.PIXEL_9_PRO
@@ -553,6 +576,7 @@ fun TimerScreenPreview() {
TimerScreen(
timerState,
isPlus = true,
contentPadding = PaddingValues(),
{ 0.3f },
{}
)