Reputation: 6957
I want two composables to begin and play their animation at the same time. For instance, this is my set up:
LazyColumn(
.semantics(mergeDescendants = true) { contentDescription = contentDes }
) {
item {
ShimmeringComposable() // takes up a lot of the page
}
item {
Spacer()
}
item {
ShimmeringComposable() // under the fold (off the screen)
}
}
I'm noticing that most of the time, the first composable's animation starts to play first, before the second composable's animation. This also sometimes happens if the first composable is visible in the layout and the second composable is off screen.
Is there a way to "sync" the two and make them start/play their animations at the same time?
This code is based on that in this YouTube video tutorial: https://www.youtube.com/watch?v=NyO99OJPPec
@Composable
fun ShimmeringComposable() {
Box(Modifier
.size(50.dp)
.shimmer()
)
}
fun Modifier.shimmer(): Modifier = composed {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val width = size.width.toFloat()
val height = size.height.toFloat()
val transition = rememberInfiniteTransition()
val startOffsetX by transition.animateFloat(
initialValue = -2 * width,
targetValue = 2 * width,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
)
)
)
background(
brush = Brush.linearGradient(
colors = listOf(Color.gray,Color.white,Color.gray),
start = Offset(startOffsetX, 0),
end = Offset(startOffsetX + width, height)
)
).onGloballyPositioned { size = it.size }
}
UPDATE: Added more code for clarity
EDIT: I think I understand what the issue is. For InfiniteTransition, the animation starts on composition, and a LazyColumn will compose items that are visible on the screen. However, for those that are off screen will not be composed until they come into view (user scrolled down). Wrapping all of the shimmering in a Column
could mitigate this since, as long as you don't use a lot of the shimmering composables as Column isn't as performant as LazyColumn.
Upvotes: 2
Views: 739
Reputation: 67179
You can provide progress as param to each Composable while controlling progress change centrally by one animation. This way even if a Composable enters composition after another it will be animated with same progress.
Instead of Modifier.globallyPositioned you can get size using DrawScope via draw Modifiers.
fun Modifier.newShimmer(progress: Float) = this.then(
Modifier
.drawWithContent {
val width = size.width
val height = size.height
val offset = progress * width
drawContent()
val brush = Brush.linearGradient(
colors = listOf(Color.Gray, Color.White, Color.Gray),
start = Offset(offset, 0f),
end = Offset(offset + width, height)
)
drawRect(brush)
}
)
And pass progress to composable
@Composable
fun ShimmerView(progress: Float) {
Box(
Modifier
.size(80.dp)
.newShimmer(progress = progress)
)
}
val transition = rememberInfiniteTransition(label = "shimmer")
val startOffsetX by transition.animateFloat(
initialValue = -2f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
)
), label = "shimmer"
)
Text(text = "NEW STYLE")
ShimmerView(progress = startOffsetX)
Spacer(Modifier.height(20.dp))
ShimmerView(progress = startOffsetX)
Full code
@Preview
@Composable
private fun ShimmerTest() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
var showItem by remember {
mutableStateOf(false)
}
Text(text = "OLD STYLE")
ShimmeringComposable() // first composable
Spacer(Modifier.height(20.dp))
ShimmeringComposable() // second composable
Spacer(Modifier.height(20.dp))
if (showItem) {
ShimmeringComposable()
}
Spacer(Modifier.height(20.dp))
val transition = rememberInfiniteTransition(label = "shimmer")
val startOffsetX by transition.animateFloat(
initialValue = -2f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
)
), label = "shimmer"
)
Text(text = "NEW STYLE")
ShimmerView(progress = startOffsetX)
Spacer(Modifier.height(20.dp))
ShimmerView(progress = startOffsetX)
Spacer(Modifier.height(20.dp))
if (showItem) {
ShimmerView(progress = startOffsetX)
}
Button(onClick = { showItem = showItem.not() }) {
Text(text = "Show item: $showItem")
}
}
}
@Composable
fun ShimmerView(progress: Float) {
Box(
Modifier
.size(80.dp)
.newShimmer(progress = progress)
)
}
@Composable
fun ShimmeringComposable() {
Box(
Modifier
.size(80.dp)
.shimmer()
)
}
fun Modifier.newShimmer(progress: Float) = this.then(
Modifier
.drawWithContent {
val width = size.width
val height = size.height
val offset = progress * width
drawContent()
val brush = Brush.linearGradient(
colors = listOf(Color.Gray, Color.White, Color.Gray),
start = Offset(offset, 0f),
end = Offset(offset + width, height)
)
drawRect(brush)
}
)
fun Modifier.shimmer(): Modifier = composed {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val width = size.width.toFloat()
val height = size.height.toFloat()
val transition = rememberInfiniteTransition(label = "shimmer")
val startOffsetX by transition.animateFloat(
initialValue = -2 * width,
targetValue = 2 * width,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
)
), label = "shimmer"
)
Modifier
.background(
brush = Brush.linearGradient(
colors = listOf(Color.Gray, Color.White, Color.Gray),
start = Offset(startOffsetX, 0f),
end = Offset(startOffsetX + width, height)
)
)
.onGloballyPositioned { size = it.size }
}
Upvotes: 2