I'm displaying a flash of some text over a box as an indication that something has happened, so let me know if there is already an intuitive way to do that already.
My self-contained demo program:
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.TimeSource
/**
* Holds state for the flash box.
*/
interface FlashBoxState {
/**
* State of the visibility of the flash.
*/
val isVisible: MutableTransitionState<Boolean>
/**
* Triggers the flash.
*/
fun flash()
}
private class DefaultFlashBoxState(
private val coroutineScope: CoroutineScope,
) : FlashBoxState {
override val isVisible = MutableTransitionState(false)
override fun flash() {
isVisible.targetState = true
coroutineScope.launch {
delay(timeMillis = 500)
isVisible.targetState = false
}
}
}
/**
* Creates a state object for use with [FlashBox].
*/
@Composable
fun rememberFlashBoxState(): FlashBoxState {
val coroutineScope = rememberCoroutineScope()
val lastFlash = remember { mutableStateOf(TimeSource.Monotonic.markNow()) }
return remember(lastFlash.value) { DefaultFlashBoxState(coroutineScope) }
}
/**
* A box which supports flashing a message over the top.
*
* @param state the state of the flash box. Can get from [rememberFlashBoxState].
* @param content the content of the box.
*/
@Composable
fun FlashBox(
state: FlashBoxState,
content: @Composable () -> Unit
) {
Box {
content()
Column(modifier = Modifier.align(Alignment.BottomCenter)) {
AnimatedVisibility(
visibleState = state.isVisible,
enter = fadeIn(animationSpec = tween(durationMillis = 200)) +
slideIn(
animationSpec = tween(durationMillis = 200),
initialOffset = { size -> IntOffset(0, y = size.height) }
),
exit = fadeOut(animationSpec = tween(durationMillis = 1000)) +
slideOut(
animationSpec = tween(durationMillis = 1000),
targetOffset = { size -> IntOffset(x = 0, y = size.height * (-6)) },
),
) {
Text(text = "Copied!", textAlign = TextAlign.Center, modifier = Modifier.width(150.dp))
}
}
}
}
@Composable
fun CodePointCell(size: Dp, onClick: () -> Unit = {}, modifier: Modifier = Modifier) {
val fontHeight = size * (2.0f / 3.0f)
val fontSize = with(LocalDensity.current) { fontHeight.toSp() }
Box(modifier = modifier) {
OutlinedButton(
shape = RectangleShape,
onClick = onClick,
contentPadding = PaddingValues(0.dp),
modifier = Modifier.wrapContentSize()
) {
Text(
text = "\uD83D\uDD74",
fontSize = fontSize,
textAlign = TextAlign.Center,
modifier = Modifier.requiredSize(size),
)
}
}
}
fun main() = application {
MaterialTheme(colorScheme = darkColorScheme()) {
Window(state = rememberWindowState(size = DpSize.Unspecified), onCloseRequest = { exitApplication() }) {
Surface {
Box(modifier = Modifier.padding(16.dp)) {
val flashBoxState = rememberFlashBoxState()
FlashBox(state = flashBoxState) {
CodePointCell(
size = 150.dp,
onClick = flashBoxState::flash,
)
}
}
}
}
}
}
Demo video:
(SO's embedding doesn't like this site?)
As you see, it's well-behaved if the clicks are a second apart. If you click faster though, the thing animates as if I had told it to do the slow animation back down.
How do I force it to restart the animation at every click?