如何在 Jetpack Compose 中实现前后动画?

Abd*_*lah 2 android android-jetpack android-jetpack-compose android-jetpack-compose-animation

我已经使用了重量或改变了无限动画的宽度,但这不是下面gif中显示的效果。如何在 Jetpack Compose 中实现它?

Thr*_*ian 6

您可以创建随着进度而改变右侧的形状,并将一个盒子夹在另一个盒子之上。

结果

在此输入图像描述

执行

@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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

演示

@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")
                }
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要剪辑两个形状,就像在按钮中一样,如果您剪辑另一个形状也会显示在屏幕上,因为形状没有覆盖,请像这样

@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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

第二种方法是使用 BlendMode.Clear 并将 lambda 中的进度传递给单个重组的动画,如下所示

@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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

另一种方法是,如果您只想绘制图像,则在 Canvas 上绘制 2 个图像并更改dstOffset第二个drawImage功能。

还可以作为具有许多自定义选项的库提供。

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