feat(stats): basic shared element transition for last week screen

This commit is contained in:
Nishant Mishra
2025-12-12 09:22:57 +05:30
parent 6d94b992b0
commit 4ec2ba1321
4 changed files with 227 additions and 69 deletions

View File

@@ -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<Screen.Stats.Main> {
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<Screen.Stats.Main> {
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<Screen.Stats.LastWeek> {
LastWeekScreen(
contentPadding = contentPadding,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
onBack = backStack::removeLastOrNull,
hoursMinutesFormat = hoursMinutesFormat,
lastWeekSummaryChartData = lastWeekSummaryChartData,
hoursFormat = hoursFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface
)
entry<Screen.Stats.LastWeek> {
LastWeekScreen(
contentPadding = contentPadding,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
onBack = backStack::removeLastOrNull,
hoursMinutesFormat = hoursMinutesFormat,
lastWeekSummaryChartData = lastWeekSummaryChartData,
hoursFormat = hoursFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface
)
}
}
}
)
)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}

View File

@@ -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<Int>,
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
)
)
}
}

View File

@@ -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<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
@@ -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
)
)
}
}