diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt index f41f9bb..ea3f277 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt @@ -19,12 +19,16 @@ package org.nsh07.pomodoro.ui.statsScreen import android.graphics.Typeface import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.animation.unveilIn import androidx.compose.animation.veilOut import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -99,6 +103,8 @@ fun StatsScreen( generateSampleData: () -> Unit, modifier: Modifier = Modifier ) { + val colorScheme = colorScheme + val hoursFormat = stringResource(R.string.hours_format) val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format) val minutesFormat = stringResource(R.string.minutes_format) @@ -107,56 +113,58 @@ fun StatsScreen( val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface } val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface } - NavDisplay( - backStack = backStack, - onBack = backStack::removeLastOrNull, - transitionSpec = { - unveilIn().togetherWith(veilOut()) - }, - popTransitionSpec = { - unveilIn().togetherWith(veilOut()) - }, - predictivePopTransitionSpec = { - unveilIn().togetherWith(veilOut()) - }, - entryProvider = entryProvider { - entry { - StatsMainScreen( - contentPadding = contentPadding, - lastWeekSummaryChartData = lastWeekSummaryChartData, - lastMonthSummaryChartData = lastMonthSummaryChartData, - lastYearSummaryChartData = lastYearSummaryChartData, - todayStat = todayStat, - lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, - lastMonthAverageFocusTimes = lastMonthAverageFocusTimes, - lastYearAverageFocusTimes = lastYearAverageFocusTimes, - generateSampleData = generateSampleData, - hoursFormat = hoursFormat, - hoursMinutesFormat = hoursMinutesFormat, - minutesFormat = minutesFormat, - axisTypeface = axisTypeface, - markerTypeface = markerTypeface, - onNavigate = { - if (backStack.size < 2) backStack.add(it) - else backStack[backStack.lastIndex] = it - }, - modifier = modifier - ) - } + SharedTransitionLayout { + NavDisplay( + backStack = backStack, + onBack = backStack::removeLastOrNull, + transitionSpec = { + fadeIn().togetherWith(veilOut(targetColor = colorScheme.surfaceDim)) + }, + popTransitionSpec = { + unveilIn(initialColor = colorScheme.surfaceDim).togetherWith(fadeOut()) + }, + predictivePopTransitionSpec = { + unveilIn(initialColor = colorScheme.surfaceDim).togetherWith(fadeOut()) + }, + entryProvider = entryProvider { + entry { + StatsMainScreen( + contentPadding = contentPadding, + lastWeekSummaryChartData = lastWeekSummaryChartData, + lastMonthSummaryChartData = lastMonthSummaryChartData, + lastYearSummaryChartData = lastYearSummaryChartData, + todayStat = todayStat, + lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, + lastMonthAverageFocusTimes = lastMonthAverageFocusTimes, + lastYearAverageFocusTimes = lastYearAverageFocusTimes, + generateSampleData = generateSampleData, + hoursFormat = hoursFormat, + hoursMinutesFormat = hoursMinutesFormat, + minutesFormat = minutesFormat, + axisTypeface = axisTypeface, + markerTypeface = markerTypeface, + onNavigate = { + if (backStack.size < 2) backStack.add(it) + else backStack[backStack.lastIndex] = it + }, + modifier = modifier + ) + } - entry { - LastWeekScreen( - contentPadding = contentPadding, - lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, - onBack = backStack::removeLastOrNull, - hoursMinutesFormat = hoursMinutesFormat, - lastWeekSummaryChartData = lastWeekSummaryChartData, - hoursFormat = hoursFormat, - minutesFormat = minutesFormat, - axisTypeface = axisTypeface, - markerTypeface = markerTypeface - ) + entry { + LastWeekScreen( + contentPadding = contentPadding, + lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, + onBack = backStack::removeLastOrNull, + hoursMinutesFormat = hoursMinutesFormat, + lastWeekSummaryChartData = lastWeekSummaryChartData, + hoursFormat = hoursFormat, + minutesFormat = minutesFormat, + axisTypeface = axisTypeface, + markerTypeface = markerTypeface + ) + } } - } - ) + ) + } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/components/sharedBoundsReveal.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/components/sharedBoundsReveal.kt new file mode 100644 index 0000000..d89847f --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/components/sharedBoundsReveal.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.statsScreen.components + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.SharedTransitionDefaults +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.navigation3.ui.LocalNavAnimatedContentScope + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun Modifier.sharedBoundsReveal( + sharedContentState: SharedTransitionScope.SharedContentState, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope = LocalNavAnimatedContentScope.current, + boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform, + enter: EnterTransition = EnterTransition.None, + exit: ExitTransition = ExitTransition.None, + resizeMode: SharedTransitionScope.ResizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds, + clipShape: Shape = MaterialTheme.shapes.largeIncreased, + renderInOverlayDuringTransition: Boolean = true, +): Modifier { + with(sharedTransitionScope) { + return this@sharedBoundsReveal + .sharedBounds( + sharedContentState = sharedContentState, + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform, + enter = enter, + exit = exit, + resizeMode = resizeMode, + clipInOverlayDuringTransition = OverlayClip(clipShape), + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + ) + .skipToLookaheadSize() + .skipToLookaheadPosition() + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/LastWeekScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/LastWeekScreen.kt index b7ad754..8921cfe 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/LastWeekScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/LastWeekScreen.kt @@ -18,10 +18,13 @@ package org.nsh07.pomodoro.ui.statsScreen.screens import android.graphics.Typeface +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -30,10 +33,11 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -43,18 +47,21 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.common.data.ExtraStore import org.nsh07.pomodoro.R import org.nsh07.pomodoro.ui.mergePaddingValues import org.nsh07.pomodoro.ui.statsScreen.components.TimeColumnChart +import org.nsh07.pomodoro.ui.statsScreen.components.sharedBoundsReveal import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun LastWeekScreen( +fun SharedTransitionScope.LastWeekScreen( contentPadding: PaddingValues, lastWeekAverageFocusTimes: List, onBack: () -> Unit, @@ -66,7 +73,7 @@ fun LastWeekScreen( axisTypeface: Typeface, markerTypeface: Typeface ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val rankList = remember(lastWeekAverageFocusTimes) { val sortedIndices = @@ -82,9 +89,17 @@ fun LastWeekScreen( Scaffold( topBar = { - LargeFlexibleTopAppBar( + TopAppBar( title = { - Text(stringResource(R.string.last_week), fontFamily = robotoFlexTopBar) + Text( + text = stringResource(R.string.last_week), + fontFamily = robotoFlexTopBar, + modifier = Modifier.sharedBounds( + sharedContentState = this@LastWeekScreen + .rememberSharedContentState("last week heading"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) + ) }, subtitle = { Text(stringResource(R.string.stats)) @@ -100,18 +115,35 @@ fun LastWeekScreen( ) } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorScheme.surfaceBright, + scrolledContainerColor = colorScheme.surfaceBright + ) ) }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .sharedBoundsReveal( + sharedTransitionScope = this@LastWeekScreen, + sharedContentState = this@LastWeekScreen.rememberSharedContentState( + "last week card" + ), + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + clipShape = topListItemShape + ) ) { innerPadding -> val insets = mergePaddingValues(innerPadding, contentPadding) LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = insets, - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier + .fillMaxSize() + .background(colorScheme.surfaceBright) + .padding(horizontal = 16.dp) ) { item { + Spacer(Modifier.height(16.dp)) Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -123,12 +155,24 @@ fun LastWeekScreen( }, hoursMinutesFormat ), - style = typography.displaySmall + style = typography.displaySmall, + modifier = Modifier + .sharedElement( + sharedContentState = this@LastWeekScreen + .rememberSharedContentState("last week average focus timer"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) Text( stringResource(R.string.focus_per_day_avg), style = typography.titleSmall, - modifier = Modifier.padding(bottom = 5.2.dp) + modifier = Modifier + .padding(bottom = 5.2.dp) + .sharedElement( + sharedContentState = this@LastWeekScreen + .rememberSharedContentState("focus per day average (week)"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) } Spacer(Modifier.height(16.dp)) @@ -141,7 +185,13 @@ fun LastWeekScreen( markerTypeface = markerTypeface, xValueFormatter = CartesianValueFormatter { context, x, _ -> context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()] - } + }, + modifier = Modifier + .sharedElement( + sharedContentState = this@LastWeekScreen + .rememberSharedContentState("last week chart"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/StatsMainScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/StatsMainScreen.kt index ba88b53..c428a67 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/StatsMainScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/screens/StatsMainScreen.kt @@ -18,6 +18,7 @@ package org.nsh07.pomodoro.ui.statsScreen.screens import android.graphics.Typeface +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -26,6 +27,7 @@ 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.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -52,6 +54,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.common.data.ExtraStore @@ -62,6 +65,7 @@ import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.mergePaddingValues import org.nsh07.pomodoro.ui.statsScreen.components.TimeColumnChart import org.nsh07.pomodoro.ui.statsScreen.components.TimeLineChart +import org.nsh07.pomodoro.ui.statsScreen.components.sharedBoundsReveal import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors @@ -72,7 +76,7 @@ import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun StatsMainScreen( +fun SharedTransitionScope.StatsMainScreen( contentPadding: PaddingValues, lastWeekSummaryChartData: Pair>>, lastMonthSummaryChartData: Pair>>, @@ -90,7 +94,7 @@ fun StatsMainScreen( onNavigate: (Screen.Stats) -> Unit, modifier: Modifier = Modifier, ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { @@ -102,7 +106,10 @@ fun StatsMainScreen( fontFamily = robotoFlexTopBar, fontSize = 32.sp, lineHeight = 32.sp - ) + ), + modifier = Modifier + .padding(top = contentPadding.calculateTopPadding()) + .padding(vertical = 14.dp) ) }, actions = if (BuildConfig.DEBUG) { @@ -119,7 +126,8 @@ fun StatsMainScreen( subtitle = {}, titleHorizontalAlignment = Alignment.CenterHorizontally, scrollBehavior = scrollBehavior, - colors = topBarColors + colors = topBarColors, + windowInsets = WindowInsets() ) }, modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -212,6 +220,14 @@ fun StatsMainScreen( Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier + .sharedBoundsReveal( + sharedTransitionScope = this@StatsMainScreen, + sharedContentState = this@StatsMainScreen.rememberSharedContentState( + "last week card" + ), + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + clipShape = topListItemShape + ) .clip(topListItemShape) .background(listItemColors.containerColor) .clickable { onNavigate(Screen.Stats.LastWeek) } @@ -223,7 +239,12 @@ fun StatsMainScreen( ) { Text( stringResource(R.string.last_week), - style = typography.headlineSmall + style = typography.headlineSmall, + modifier = Modifier.sharedBounds( + sharedContentState = this@StatsMainScreen + .rememberSharedContentState("last week heading"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) Row( @@ -237,12 +258,24 @@ fun StatsMainScreen( }, hoursMinutesFormat ), - style = typography.displaySmall + style = typography.displaySmall, + modifier = Modifier + .sharedElement( + sharedContentState = this@StatsMainScreen + .rememberSharedContentState("last week average focus timer"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) Text( stringResource(R.string.focus_per_day_avg), style = typography.titleSmall, - modifier = Modifier.padding(bottom = 5.2.dp) + modifier = Modifier + .padding(bottom = 5.2.dp) + .sharedElement( + sharedContentState = this@StatsMainScreen + .rememberSharedContentState("focus per day average (week)"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) } @@ -255,7 +288,13 @@ fun StatsMainScreen( markerTypeface = markerTypeface, xValueFormatter = CartesianValueFormatter { context, x, _ -> context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()] - } + }, + modifier = Modifier + .sharedElement( + sharedContentState = this@StatsMainScreen + .rememberSharedContentState("last week chart"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) } }