How can I achieve before-after animation in Jetpack Compose?

351 Views Asked by At

I have used weight or changed width with infinite animation, but it is not the effect showed in the below gif. How can I achieve it in Jetpack Compose?

1

There are 1 best solutions below

3
On BEST ANSWER

You can create shape that changes right side with a progress and clip a Box above another.

Result

enter image description here

Implementation

@Composable
private fun BeforeAfterLayout(
    modifier: Modifier = Modifier,
    progress: Float,
    beforeLayout: @Composable BoxScope.() -> Unit,
    afterLayout: @Composable BoxScope.() -> Unit
) {

    val shape = remember(progress) {
        GenericShape { size: Size, layoutDirection: LayoutDirection ->
            addRect(
                rect = Rect(
                    topLeft = Offset.Zero,
                    bottomRight = Offset(size.width * progress, size.height)
                )
            )
        }
    }

    Box(modifier) {
        beforeLayout()

        Box(
            modifier = Modifier.clip(shape)
        ) {
            afterLayout()
        }
    }
}

Demo

@Preview
@Composable
private fun BeforeAfterSample() {

    val infiniteTransition = rememberInfiniteTransition("before-after")
    val progress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(4000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "before-after"
    )

    Column(
        modifier = Modifier.padding(20.dp).fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        BeforeAfterLayoutWithBlendMode(
            modifier = Modifier.clip(RoundedCornerShape(16.dp)).size(240.dp),
            progress = { progress },
            beforeLayout = {
                Image(
                    modifier = Modifier.fillMaxSize(),
                    painter = painterResource(R.drawable.avatar_1_raster),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds
                )
            },
            afterLayout = {
                Image(
                    modifier = Modifier.fillMaxSize(),
                    painter = painterResource(R.drawable.avatar_5_raster),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds
                )
            }
        )

        Spacer(modifier = Modifier.height(20.dp))

        BeforeAfterLayoutWithBlendMode(
            modifier = Modifier.clip(RoundedCornerShape(16.dp)).fillMaxWidth(.8f)
                .aspectRatio(16 / 9f),
            progress = { progress },
            beforeLayout = {
                Image(
                    modifier = Modifier.fillMaxSize(),
                    painter = painterResource(R.drawable.image_before_after_shoes_a),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds
                )
            },
            afterLayout = {
                Image(
                    modifier = Modifier.fillMaxSize(),
                    painter = painterResource(R.drawable.image_before_after_shoes_b),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds
                )
            }
        )

        Spacer(modifier = Modifier.height(20.dp))

        BeforeAfterLayout(
            progress = progress,
            beforeLayout = {
                Box(
                    modifier = Modifier
                        .border(4.dp, Purple400, RoundedCornerShape(16.dp))
                        .background(Color.White, RoundedCornerShape(16.dp))
                        .fillMaxWidth(.9f)
                        .height(80.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "Progress: ${(progress * 100).roundToInt()}",
                        color = Red400,
                        fontSize = 24.sp
                    )
                }
            },
            afterLayout = {
                Box(
                    modifier = Modifier
                        .border(4.dp, Purple400, RoundedCornerShape(16.dp))
                        .background(Red400, RoundedCornerShape(16.dp))
                        .fillMaxWidth(.9f)
                        .height(80.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "Progress: ${(progress * 100).roundToInt()}",
                        color = Color.White,
                        fontSize = 24.sp
                    )
                }
            }
        )

        Spacer(modifier = Modifier.height(20.dp))

        val context = LocalContext.current

        BeforeAfterLayout(
            progress = progress,
            beforeLayout = {
                Button(
                    modifier = Modifier
                        .padding(horizontal = 32.dp)
                        .fillMaxWidth(),
                    onClick = {
                        Toast.makeText(context, "M2 Button", Toast.LENGTH_SHORT).show()
                    }
                ) {
                    Text("M2 Button")
                }
            },
            afterLayout = {
                androidx.compose.material3.Button(
                    modifier = Modifier
                        .padding(horizontal = 32.dp)
                        .fillMaxWidth(),
                    onClick = {
                        Toast.makeText(context, "M3 Button", Toast.LENGTH_SHORT).show()
                    }
                ) {
                    androidx.compose.material3.Text("M3 Button")
                }
            }
        )
    }
}

If you need clip both shapes, like in Buttons, if you clip one other will also show on screen since shapes don't cover do it like this

@Composable
private fun BeforeAfterLayout(
    modifier: Modifier = Modifier,
    progress: Float,
    beforeLayout: @Composable BoxScope.() -> Unit,
    afterLayout: @Composable BoxScope.() -> Unit
) {

    val shapeBefore = remember(progress) {
        GenericShape { size: Size, layoutDirection: LayoutDirection ->
            addRect(
                rect = Rect(
                    topLeft = Offset(size.width * progress, 0f),
                    bottomRight = Offset(size.width, size.height)
                )
            )
        }
    }

    val shapeAfter = remember(progress) {
        GenericShape { size: Size, layoutDirection: LayoutDirection ->
            addRect(
                rect = Rect(
                    topLeft = Offset.Zero,
                    bottomRight = Offset(size.width * progress, size.height)
                )
            )
        }
    }

    Box(modifier) {
        Box(
            modifier = Modifier.clip(shapeBefore)
        ) {
            beforeLayout()
        }

        Box(
            modifier = Modifier.clip(shapeAfter)
        ) {
            afterLayout()
        }
    }
}

Second way is using BlendMode.Clear and passing progress in a lambda to animating with single recomposition as

@Composable
private fun BeforeAfterLayoutWithBlendMode(
    modifier: Modifier = Modifier,
    progress: () -> Float,
    beforeLayout: @Composable BoxScope.() -> Unit,
    afterLayout: @Composable BoxScope.() -> Unit
) {
    Box(modifier) {
        afterLayout()
        Box(
            modifier = Modifier
                .graphicsLayer {
                    compositingStrategy = CompositingStrategy.Offscreen
                }
                .drawWithContent {
                    drawContent()
                    drawRect(
                        color = Color.Transparent,
                        size = Size(size.width * progress(), size.height),
                        blendMode = BlendMode.Clear
                    )

                }
        ) {
            beforeLayout()
        }
    }
}

Another way to do it is, if it's only image you wish to draw, to draw 2 images on Canvas and change dstOffset of second drawImage function.

Also available as library with many customization options.

https://github.com/SmartToolFactory/Compose-BeforeAfter