Skip to content

Commit 518f172

Browse files
committed
feat(ui): implement randomizing position in AOD
to prevent burn-in
1 parent b16aeb4 commit 518f172

File tree

1 file changed

+119
-57
lines changed

1 file changed

+119
-57
lines changed

app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt

Lines changed: 119 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import androidx.activity.compose.LocalActivity
1313
import androidx.compose.animation.SharedTransitionLayout
1414
import androidx.compose.animation.SharedTransitionScope
1515
import androidx.compose.animation.animateColorAsState
16+
import androidx.compose.animation.core.animateIntAsState
1617
import androidx.compose.foundation.background
1718
import androidx.compose.foundation.layout.Box
1819
import androidx.compose.foundation.layout.fillMaxSize
20+
import androidx.compose.foundation.layout.offset
1921
import androidx.compose.foundation.layout.size
2022
import androidx.compose.material3.CircularProgressIndicator
2123
import androidx.compose.material3.CircularWavyProgressIndicator
@@ -27,6 +29,7 @@ import androidx.compose.runtime.Composable
2729
import androidx.compose.runtime.DisposableEffect
2830
import androidx.compose.runtime.LaunchedEffect
2931
import androidx.compose.runtime.getValue
32+
import androidx.compose.runtime.mutableIntStateOf
3033
import androidx.compose.runtime.mutableStateOf
3134
import androidx.compose.runtime.remember
3235
import androidx.compose.runtime.setValue
@@ -37,10 +40,14 @@ import androidx.compose.ui.graphics.StrokeCap
3740
import androidx.compose.ui.graphics.drawscope.Stroke
3841
import androidx.compose.ui.platform.LocalDensity
3942
import androidx.compose.ui.platform.LocalView
43+
import androidx.compose.ui.platform.LocalWindowInfo
4044
import androidx.compose.ui.text.TextStyle
4145
import androidx.compose.ui.text.font.FontWeight
4246
import androidx.compose.ui.text.style.TextAlign
4347
import 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
4451
import androidx.compose.ui.unit.dp
4552
import androidx.compose.ui.unit.sp
4653
import androidx.core.view.WindowCompat
@@ -50,9 +57,20 @@ import androidx.navigation3.ui.LocalNavAnimatedContentScope
5057
import kotlinx.coroutines.delay
5158
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
5259
import org.nsh07.pomodoro.ui.theme.TomatoTheme
60+
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
5361
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
5462
import 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
5876
fun 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

Comments
 (0)