VIN
VIN

Reputation: 6957

Animating a group of shimmering composables, controlling sync/timing

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

Answers (1)

Thracian
Thracian

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.

enter image description here

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

Related Questions