@@ -13,9 +13,11 @@ import androidx.activity.compose.LocalActivity
1313import androidx.compose.animation.SharedTransitionLayout
1414import androidx.compose.animation.SharedTransitionScope
1515import androidx.compose.animation.animateColorAsState
16+ import androidx.compose.animation.core.animateIntAsState
1617import androidx.compose.foundation.background
1718import androidx.compose.foundation.layout.Box
1819import androidx.compose.foundation.layout.fillMaxSize
20+ import androidx.compose.foundation.layout.offset
1921import androidx.compose.foundation.layout.size
2022import androidx.compose.material3.CircularProgressIndicator
2123import androidx.compose.material3.CircularWavyProgressIndicator
@@ -27,6 +29,7 @@ import androidx.compose.runtime.Composable
2729import androidx.compose.runtime.DisposableEffect
2830import androidx.compose.runtime.LaunchedEffect
2931import androidx.compose.runtime.getValue
32+ import androidx.compose.runtime.mutableIntStateOf
3033import androidx.compose.runtime.mutableStateOf
3134import androidx.compose.runtime.remember
3235import androidx.compose.runtime.setValue
@@ -37,10 +40,14 @@ import androidx.compose.ui.graphics.StrokeCap
3740import androidx.compose.ui.graphics.drawscope.Stroke
3841import androidx.compose.ui.platform.LocalDensity
3942import androidx.compose.ui.platform.LocalView
43+ import androidx.compose.ui.platform.LocalWindowInfo
4044import androidx.compose.ui.text.TextStyle
4145import androidx.compose.ui.text.font.FontWeight
4246import androidx.compose.ui.text.style.TextAlign
4347import androidx.compose.ui.tooling.preview.Preview
48+ import androidx.compose.ui.unit.Density
49+ import androidx.compose.ui.unit.Dp
50+ import androidx.compose.ui.unit.IntOffset
4451import androidx.compose.ui.unit.dp
4552import androidx.compose.ui.unit.sp
4653import androidx.core.view.WindowCompat
@@ -50,9 +57,20 @@ import androidx.navigation3.ui.LocalNavAnimatedContentScope
5057import kotlinx.coroutines.delay
5158import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
5259import org.nsh07.pomodoro.ui.theme.TomatoTheme
60+ import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
5361import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
5462import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
63+ import kotlin.random.Random
5564
65+ /* *
66+ * Always On Display composable. Must be called within a [SharedTransitionScope] which allows
67+ * animating the clock and progress indicator
68+ *
69+ * @param timerState [TimerState] instance. This must be the same instance as the one used on the
70+ * root [TimerScreen] composable
71+ * @param progress lambda that returns the current progress of the clock
72+ * randomized offset for the clock to allow smooth motion with sharedBounds
73+ */
5674@OptIn(ExperimentalMaterial3ExpressiveApi ::class )
5775@Composable
5876fun SharedTransitionScope.AlwaysOnDisplay (
@@ -62,8 +80,11 @@ fun SharedTransitionScope.AlwaysOnDisplay(
6280) {
6381 var sharedElementTransitionComplete by remember { mutableStateOf(false ) }
6482
65- val view = LocalView .current
6683 val activity = LocalActivity .current
84+ val density = LocalDensity .current
85+ val windowInfo = LocalWindowInfo .current
86+ val view = LocalView .current
87+
6788 val window = remember { (view.context as Activity ).window }
6889 val insetsController = remember { WindowCompat .getInsetsController(window, view) }
6990
@@ -126,70 +147,109 @@ fun SharedTransitionScope.AlwaysOnDisplay(
126147 animationSpec = motionScheme.slowEffectsSpec()
127148 )
128149
150+ var randomX by remember {
151+ mutableIntStateOf(
152+ Random .nextInt(
153+ 16 .dp.toIntPx(density),
154+ windowInfo.containerSize.width - 266 .dp.toIntPx(density)
155+ )
156+ )
157+ }
158+ var randomY by remember {
159+ mutableIntStateOf(
160+ Random .nextInt(
161+ 16 .dp.toIntPx(density),
162+ windowInfo.containerSize.height - 266 .dp.toIntPx(density)
163+ )
164+ )
165+ }
166+
167+ LaunchedEffect (timerState.timeStr[1 ]) { // Randomize position every minute
168+ if (sharedElementTransitionComplete) {
169+ randomX = Random .nextInt(
170+ 16 .dp.toIntPx(density),
171+ windowInfo.containerSize.width - 266 .dp.toIntPx(density)
172+ )
173+ randomY = Random .nextInt(
174+ 16 .dp.toIntPx(density),
175+ windowInfo.containerSize.height - 266 .dp.toIntPx(density)
176+ )
177+ }
178+ }
179+
180+ val x by animateIntAsState(randomX)
181+ val y by animateIntAsState(randomY)
182+
129183 Box (
130- contentAlignment = Alignment .Center ,
131184 modifier = modifier
132185 .fillMaxSize()
133186 .background(surface)
134187 ) {
135- if (timerState.timerMode == TimerMode .FOCUS ) {
136- CircularProgressIndicator (
137- progress = progress,
138- modifier = Modifier
139- .sharedBounds(
140- sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" focus progress" ),
141- animatedVisibilityScope = LocalNavAnimatedContentScope .current
142- )
143- .size(250 .dp),
144- color = primary,
145- trackColor = secondaryContainer,
146- strokeWidth = 12 .dp,
147- gapSize = 8 .dp,
148- )
149- } else {
150- CircularWavyProgressIndicator (
151- progress = progress,
152- modifier = Modifier
153- .sharedBounds(
154- sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" break progress" ),
155- animatedVisibilityScope = LocalNavAnimatedContentScope .current
156- )
157- .size(250 .dp),
158- color = primary,
159- trackColor = secondaryContainer,
160- stroke = Stroke (
161- width = with (LocalDensity .current) {
162- 12 .dp.toPx()
163- },
164- cap = StrokeCap .Round ,
165- ),
166- trackStroke = Stroke (
167- width = with (LocalDensity .current) {
168- 12 .dp.toPx()
169- },
170- cap = StrokeCap .Round ,
188+ Box (
189+ contentAlignment = Alignment .Center ,
190+ modifier = Modifier .offset {
191+ IntOffset (x, y)
192+ }
193+ ) {
194+ if (timerState.timerMode == TimerMode .FOCUS ) {
195+ CircularProgressIndicator (
196+ progress = progress,
197+ modifier = Modifier
198+ .sharedBounds(
199+ sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" focus progress" ),
200+ animatedVisibilityScope = LocalNavAnimatedContentScope .current
201+ )
202+ .size(250 .dp),
203+ color = primary,
204+ trackColor = secondaryContainer,
205+ strokeWidth = 12 .dp,
206+ gapSize = 8 .dp,
207+ )
208+ } else {
209+ CircularWavyProgressIndicator (
210+ progress = progress,
211+ modifier = Modifier
212+ .sharedBounds(
213+ sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" break progress" ),
214+ animatedVisibilityScope = LocalNavAnimatedContentScope .current
215+ )
216+ .size(250 .dp),
217+ color = primary,
218+ trackColor = secondaryContainer,
219+ stroke = Stroke (
220+ width = with (LocalDensity .current) {
221+ 12 .dp.toPx()
222+ },
223+ cap = StrokeCap .Round ,
224+ ),
225+ trackStroke = Stroke (
226+ width = with (LocalDensity .current) {
227+ 12 .dp.toPx()
228+ },
229+ cap = StrokeCap .Round ,
230+ ),
231+ wavelength = 42 .dp,
232+ gapSize = 8 .dp
233+ )
234+ }
235+
236+ Text (
237+ text = timerState.timeStr,
238+ style = TextStyle (
239+ fontFamily = openRundeClock,
240+ fontWeight = FontWeight .Bold ,
241+ fontSize = 56 .sp,
242+ letterSpacing = (- 2 ).sp
171243 ),
172- wavelength = 42 .dp,
173- gapSize = 8 .dp
244+ textAlign = TextAlign .Center ,
245+ color = onSurface,
246+ maxLines = 1 ,
247+ modifier = Modifier .sharedBounds(
248+ sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" clock" ),
249+ animatedVisibilityScope = LocalNavAnimatedContentScope .current
250+ )
174251 )
175252 }
176-
177- Text (
178- text = timerState.timeStr,
179- style = TextStyle (
180- fontFamily = openRundeClock,
181- fontWeight = FontWeight .Bold ,
182- fontSize = 56 .sp,
183- letterSpacing = (- 2 ).sp
184- ),
185- textAlign = TextAlign .Center ,
186- color = onSurface,
187- maxLines = 1 ,
188- modifier = Modifier .sharedBounds(
189- sharedContentState = this @AlwaysOnDisplay.rememberSharedContentState(" clock" ),
190- animatedVisibilityScope = LocalNavAnimatedContentScope .current
191- )
192- )
193253 }
194254}
195255
@@ -208,3 +268,5 @@ private fun AlwaysOnDisplayPreview() {
208268 }
209269 }
210270}
271+
272+ fun Dp.toIntPx (density : Density ) = with (density) { toPx().toInt() }
0 commit comments