feat(ui): implement randomizing position in AOD

to prevent burn-in
This commit is contained in:
Nishant Mishra
2025-10-21 10:35:21 +05:30
parent b16aeb499d
commit 518f172054

View File

@@ -13,9 +13,11 @@ import androidx.activity.compose.LocalActivity
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator
@@ -27,6 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -37,10 +40,14 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
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.core.view.WindowCompat import androidx.core.view.WindowCompat
@@ -50,9 +57,20 @@ import androidx.navigation3.ui.LocalNavAnimatedContentScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.TomatoTheme import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import kotlin.random.Random
/**
* Always On Display composable. Must be called within a [SharedTransitionScope] which allows
* animating the clock and progress indicator
*
* @param timerState [TimerState] instance. This must be the same instance as the one used on the
* root [TimerScreen] composable
* @param progress lambda that returns the current progress of the clock
* randomized offset for the clock to allow smooth motion with sharedBounds
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun SharedTransitionScope.AlwaysOnDisplay( fun SharedTransitionScope.AlwaysOnDisplay(
@@ -62,8 +80,11 @@ fun SharedTransitionScope.AlwaysOnDisplay(
) { ) {
var sharedElementTransitionComplete by remember { mutableStateOf(false) } var sharedElementTransitionComplete by remember { mutableStateOf(false) }
val view = LocalView.current
val activity = LocalActivity.current val activity = LocalActivity.current
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val view = LocalView.current
val window = remember { (view.context as Activity).window } val window = remember { (view.context as Activity).window }
val insetsController = remember { WindowCompat.getInsetsController(window, view) } val insetsController = remember { WindowCompat.getInsetsController(window, view) }
@@ -126,70 +147,109 @@ fun SharedTransitionScope.AlwaysOnDisplay(
animationSpec = motionScheme.slowEffectsSpec() animationSpec = motionScheme.slowEffectsSpec()
) )
var randomX by remember {
mutableIntStateOf(
Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.width - 266.dp.toIntPx(density)
)
)
}
var randomY by remember {
mutableIntStateOf(
Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.height - 266.dp.toIntPx(density)
)
)
}
LaunchedEffect(timerState.timeStr[1]) { // Randomize position every minute
if (sharedElementTransitionComplete) {
randomX = Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.width - 266.dp.toIntPx(density)
)
randomY = Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.height - 266.dp.toIntPx(density)
)
}
}
val x by animateIntAsState(randomX)
val y by animateIntAsState(randomY)
Box( Box(
contentAlignment = Alignment.Center,
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(surface) .background(surface)
) { ) {
if (timerState.timerMode == TimerMode.FOCUS) { Box(
CircularProgressIndicator( contentAlignment = Alignment.Center,
progress = progress, modifier = Modifier.offset {
modifier = Modifier IntOffset(x, y)
.sharedBounds( }
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"), ) {
animatedVisibilityScope = LocalNavAnimatedContentScope.current if (timerState.timerMode == TimerMode.FOCUS) {
) CircularProgressIndicator(
.size(250.dp), progress = progress,
color = primary, modifier = Modifier
trackColor = secondaryContainer, .sharedBounds(
strokeWidth = 12.dp, sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"),
gapSize = 8.dp, animatedVisibilityScope = LocalNavAnimatedContentScope.current
) )
} else { .size(250.dp),
CircularWavyProgressIndicator( color = primary,
progress = progress, trackColor = secondaryContainer,
modifier = Modifier strokeWidth = 12.dp,
.sharedBounds( gapSize = 8.dp,
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"), )
animatedVisibilityScope = LocalNavAnimatedContentScope.current } else {
) CircularWavyProgressIndicator(
.size(250.dp), progress = progress,
color = primary, modifier = Modifier
trackColor = secondaryContainer, .sharedBounds(
stroke = Stroke( sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"),
width = with(LocalDensity.current) { animatedVisibilityScope = LocalNavAnimatedContentScope.current
12.dp.toPx() )
}, .size(250.dp),
cap = StrokeCap.Round, 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 = 42.dp,
gapSize = 8.dp
)
}
Text(
text = timerState.timeStr,
style = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 56.sp,
letterSpacing = (-2).sp
), ),
trackStroke = Stroke( textAlign = TextAlign.Center,
width = with(LocalDensity.current) { color = onSurface,
12.dp.toPx() maxLines = 1,
}, modifier = Modifier.sharedBounds(
cap = StrokeCap.Round, sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"),
), animatedVisibilityScope = LocalNavAnimatedContentScope.current
wavelength = 42.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
)
)
} }
} }
@@ -208,3 +268,5 @@ private fun AlwaysOnDisplayPreview() {
} }
} }
} }
fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() }