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?
You can create shape that changes right side with a progress and clip a Box above another.
Result
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.