feat(ui): implement an AOD screen and transition animations
This commit is contained in:
196
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
196
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SharedTransitionScope.AlwaysOnDisplay(
|
||||||
|
timerState: TimerState,
|
||||||
|
progress: () -> Float,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var sharedElementTransitionComplete by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val view = LocalView.current
|
||||||
|
val window = remember { (view.context as Activity).window }
|
||||||
|
val insetsController = remember { WindowCompat.getInsetsController(window, view) }
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
insetsController.apply {
|
||||||
|
hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
insetsController.apply {
|
||||||
|
show(WindowInsetsCompat.Type.statusBars())
|
||||||
|
show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(300)
|
||||||
|
sharedElementTransitionComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val primary by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFFA2A2A2)
|
||||||
|
else {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
||||||
|
else colorScheme.tertiary
|
||||||
|
},
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val secondaryContainer by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFF1D1D1D)
|
||||||
|
else {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
|
||||||
|
else colorScheme.tertiaryContainer
|
||||||
|
},
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val surface by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color.Black
|
||||||
|
else colorScheme.surface,
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val onSurface by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFFE3E3E3)
|
||||||
|
else colorScheme.onSurface,
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(surface)
|
||||||
|
) {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
.size(250.dp),
|
||||||
|
color = primary,
|
||||||
|
trackColor = secondaryContainer,
|
||||||
|
strokeWidth = 12.dp,
|
||||||
|
gapSize = 8.dp,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CircularWavyProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
.size(250.dp),
|
||||||
|
color = primary,
|
||||||
|
trackColor = secondaryContainer,
|
||||||
|
stroke = Stroke(
|
||||||
|
width = with(LocalDensity.current) {
|
||||||
|
12.dp.toPx()
|
||||||
|
},
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
),
|
||||||
|
trackStroke = Stroke(
|
||||||
|
width = with(LocalDensity.current) {
|
||||||
|
12.dp.toPx()
|
||||||
|
},
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
),
|
||||||
|
wavelength = 94.dp,
|
||||||
|
gapSize = 8.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = timerState.timeStr,
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = openRundeClock,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 56.sp,
|
||||||
|
letterSpacing = (-2).sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AlwaysOnDisplayPreview() {
|
||||||
|
val timerState = TimerState()
|
||||||
|
val progress = { 0.5f }
|
||||||
|
TomatoTheme {
|
||||||
|
SharedTransitionLayout {
|
||||||
|
AlwaysOnDisplay(
|
||||||
|
timerState = timerState,
|
||||||
|
progress = progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,17 +8,16 @@
|
|||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
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.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -29,17 +28,13 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.ShortNavigationBar
|
import androidx.compose.material3.ShortNavigationBar
|
||||||
import androidx.compose.material3.ShortNavigationBarArrangement
|
import androidx.compose.material3.ShortNavigationBarArrangement
|
||||||
import androidx.compose.material3.ShortNavigationBarItem
|
import androidx.compose.material3.ShortNavigationBarItem
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
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.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
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
|
||||||
@@ -78,7 +73,6 @@ fun AppScreen(
|
|||||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
|
||||||
val backStack = rememberNavBackStack(Screen.Timer)
|
val backStack = rememberNavBackStack(Screen.Timer)
|
||||||
var showAOD by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (uiState.alarmRinging)
|
if (uiState.alarmRinging)
|
||||||
AlarmDialog {
|
AlarmDialog {
|
||||||
@@ -88,147 +82,159 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedContent(
|
|
||||||
showAOD,
|
Scaffold(
|
||||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) }
|
bottomBar = {
|
||||||
) { aod ->
|
AnimatedVisibility(
|
||||||
if (!aod) {
|
backStack.last() !is Screen.AOD,
|
||||||
Scaffold(
|
enter = fadeIn(),
|
||||||
bottomBar = {
|
exit = fadeOut()
|
||||||
val wide = remember {
|
) {
|
||||||
windowSizeClass.isWidthAtLeastBreakpoint(
|
val wide = remember {
|
||||||
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
windowSizeClass.isWidthAtLeastBreakpoint(
|
||||||
)
|
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||||
}
|
|
||||||
ShortNavigationBar(
|
|
||||||
arrangement =
|
|
||||||
if (wide) ShortNavigationBarArrangement.Centered
|
|
||||||
else ShortNavigationBarArrangement.EqualWeight
|
|
||||||
) {
|
|
||||||
screens.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
|
|
||||||
}
|
|
||||||
} 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
|
|
||||||
.then(
|
|
||||||
if (isAODEnabled) Modifier.clickable { showAOD = !showAOD }
|
|
||||||
else Modifier
|
|
||||||
)
|
)
|
||||||
) { contentPadding ->
|
}
|
||||||
NavDisplay(
|
ShortNavigationBar(
|
||||||
backStack = backStack,
|
arrangement =
|
||||||
onBack = { backStack.removeLastOrNull() },
|
if (wide) ShortNavigationBarArrangement.Centered
|
||||||
transitionSpec = {
|
else ShortNavigationBarArrangement.EqualWeight
|
||||||
ContentTransform(
|
) {
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
screens.forEach {
|
||||||
fadeOut(motionScheme.defaultEffectsSpec())
|
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
|
||||||
|
}
|
||||||
|
} 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)) }
|
||||||
)
|
)
|
||||||
},
|
|
||||||
popTransitionSpec = {
|
|
||||||
ContentTransform(
|
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
|
||||||
fadeOut(motionScheme.defaultEffectsSpec())
|
|
||||||
)
|
|
||||||
},
|
|
||||||
predictivePopTransitionSpec = {
|
|
||||||
ContentTransform(
|
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
|
||||||
fadeOut(motionScheme.defaultEffectsSpec()) +
|
|
||||||
scaleOut(targetScale = 0.7f),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
entryProvider = entryProvider {
|
|
||||||
entry<Screen.Timer> {
|
|
||||||
TimerScreen(
|
|
||||||
timerState = uiState,
|
|
||||||
progress = { progress },
|
|
||||||
onAction = { action ->
|
|
||||||
when (action) {
|
|
||||||
TimerAction.ResetTimer ->
|
|
||||||
Intent(context, TimerService::class.java).also {
|
|
||||||
it.action = TimerService.Actions.RESET.toString()
|
|
||||||
context.startService(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
is TimerAction.SkipTimer ->
|
|
||||||
Intent(context, TimerService::class.java).also {
|
|
||||||
it.action = TimerService.Actions.SKIP.toString()
|
|
||||||
context.startService(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimerAction.StopAlarm ->
|
|
||||||
Intent(context, TimerService::class.java).also {
|
|
||||||
it.action =
|
|
||||||
TimerService.Actions.STOP_ALARM.toString()
|
|
||||||
context.startService(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimerAction.ToggleTimer ->
|
|
||||||
Intent(context, TimerService::class.java).also {
|
|
||||||
it.action = TimerService.Actions.TOGGLE.toString()
|
|
||||||
context.startService(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = modifier.padding(
|
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Settings> {
|
|
||||||
SettingsScreenRoot(
|
|
||||||
modifier = modifier.padding(
|
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Stats> {
|
|
||||||
StatsScreenRoot(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
modifier = modifier.padding(
|
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Surface(
|
) { contentPadding ->
|
||||||
color = Color.Black,
|
SharedTransitionLayout {
|
||||||
modifier = Modifier
|
NavDisplay(
|
||||||
.fillMaxSize()
|
backStack = backStack,
|
||||||
.clickable { showAOD = !showAOD }) {}
|
onBack = { backStack.removeLastOrNull() },
|
||||||
|
transitionSpec = {
|
||||||
|
ContentTransform(
|
||||||
|
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||||
|
fadeOut(motionScheme.defaultEffectsSpec())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popTransitionSpec = {
|
||||||
|
ContentTransform(
|
||||||
|
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||||
|
fadeOut(motionScheme.defaultEffectsSpec())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
predictivePopTransitionSpec = {
|
||||||
|
ContentTransform(
|
||||||
|
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||||
|
fadeOut(motionScheme.defaultEffectsSpec()) +
|
||||||
|
scaleOut(targetScale = 0.7f),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
entryProvider = entryProvider {
|
||||||
|
entry<Screen.Timer> {
|
||||||
|
TimerScreen(
|
||||||
|
timerState = uiState,
|
||||||
|
progress = { progress },
|
||||||
|
onAction = { action ->
|
||||||
|
when (action) {
|
||||||
|
TimerAction.ResetTimer ->
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.RESET.toString()
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TimerAction.SkipTimer ->
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.SKIP.toString()
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerAction.StopAlarm ->
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action =
|
||||||
|
TimerService.Actions.STOP_ALARM.toString()
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerAction.ToggleTimer ->
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.TOGGLE.toString()
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.AOD> {
|
||||||
|
AlwaysOnDisplay(
|
||||||
|
timerState = uiState,
|
||||||
|
progress = { progress },
|
||||||
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (isAODEnabled) Modifier.clickable {
|
||||||
|
if (backStack.size > 1) backStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Settings> {
|
||||||
|
SettingsScreenRoot(
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Stats> {
|
||||||
|
StatsScreenRoot(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@ sealed class Screen : NavKey {
|
|||||||
@Serializable
|
@Serializable
|
||||||
object Timer : Screen()
|
object Timer : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object AOD : Screen()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Settings : Screen()
|
object Settings : Screen()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -81,6 +83,7 @@ import androidx.compose.ui.tooling.preview.Devices
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
@@ -91,7 +94,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TimerScreen(
|
fun SharedTransitionScope.TimerScreen(
|
||||||
timerState: TimerState,
|
timerState: TimerState,
|
||||||
progress: () -> Float,
|
progress: () -> Float,
|
||||||
onAction: (TimerAction) -> Unit,
|
onAction: (TimerAction) -> Unit,
|
||||||
@@ -209,6 +212,12 @@ fun TimerScreen(
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"focus progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -221,6 +230,12 @@ fun TimerScreen(
|
|||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"break progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -261,7 +276,11 @@ fun TimerScreen(
|
|||||||
letterSpacing = (-2).sp
|
letterSpacing = (-2).sp
|
||||||
),
|
),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1
|
maxLines = 1,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
expanded,
|
expanded,
|
||||||
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
|
|||||||
)
|
)
|
||||||
TomatoTheme {
|
TomatoTheme {
|
||||||
Surface {
|
Surface {
|
||||||
TimerScreen(
|
SharedTransitionLayout {
|
||||||
timerState,
|
TimerScreen(
|
||||||
{ 0.3f },
|
timerState,
|
||||||
{}
|
{ 0.3f },
|
||||||
)
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user