如何剪辑或剪切可组合项?

Thr*_*ian 7 android android-jetpack-compose

在此输入图像描述

如何剪辑或剪切可组合内容以使图像、按钮或可组合项具有自定义形状?这个问题不是关于使用Modifier.clip(),更像是使用替代方法来完成任务,这些方法允许产生不可能的结果,或者当很难创建像云或方圆这样的形状时。

这是分享您的知识、问答式问题,其灵感来自 M3 BottomAppBar 或 BottomNavigation 没有切口形状,无法找到问题,并且像这个问题一样绘制Squircle形状很困难。

我们非常欢迎更多更好的剪切或自定义形状和可组合项的方法。

Thr*_*ian 9

无需创建自定义可组合项即可实现剪切或裁剪可组合项的方法之一是使用

Modifier.drawWithContent{}具有层和 aBlendModePorterDuff 模式

使用 Jetpack Compose 要使这些模式正常工作,您需要将 alpha 设置为小于 1f 或使用 Layer(如此处的答案所示

我选择图层解决方案,因为我不想更改内容 Alpha

fun ContentDrawScope.drawWithLayer(block: ContentDrawScope.() -> Unit) {
    with(drawContext.canvas.nativeCanvas) {
        val checkPoint = saveLayer(null, null)
        block()
        restoreToCount(checkPoint)
    }
}
Run Code Online (Sandbox Code Playgroud)

Modifier.drawWithContent{}block lambda 是进行裁剪的绘制范围

和另一个进一步简化的扩展

fun Modifier.drawWithLayer(block: ContentDrawScope.() -> Unit) = this.then(
    Modifier.drawWithContent {
        drawWithLayer {
            block()
        }
    }
)
Run Code Online (Sandbox Code Playgroud)

左侧的剪辑按钮

首先我们画出左侧空出一圈的按钮
@Composable
private fun WhoAteMyButton() {
    val circleSize = LocalDensity.current.run { 100.dp.toPx() }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .drawWithLayer {
                // Destination
                drawContent()
               
                // Source
                drawCircle(
                    center = Offset(0f, 10f),
                    radius = circleSize,
                    blendMode = BlendMode.SrcOut,
                    color = Color.Transparent
                )
            }
    ) {
        Button(
            modifier = Modifier
                .padding(horizontal = 10.dp)
                .fillMaxWidth(),
            onClick = { /*TODO*/ }) {
            Text("Hello World")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们只是画了一个圆,但由于BlendMode.SrcOut目的地的交集被删除了。

剪辑按钮和带有自定义图像的图像

对于松鼠按钮,我从网上找到了一张图片

并使用此图像剪切按钮和图像

@Composable
private fun ClipComposables() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        val imageBitmap = ImageBitmap.imageResource(id = R.drawable.squircle)

        Box(modifier = Modifier
            .size(150.dp)
            .drawWithLayer {

                // Destination
                drawContent()

                // Source
                drawImage(
                    image = imageBitmap,
                    dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
                    blendMode = BlendMode.DstIn
                )

            }
        ) {

            Box(
                modifier = Modifier
                    .size(150.dp)
                    .clickable { }
                    .background(MaterialTheme.colorScheme.inversePrimary),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Squircle", fontSize = 20.sp)
            }
        }

        Box(modifier = Modifier
            .size(150.dp)
            .drawWithLayer {
                // Destination
                drawContent()

                // Source
                drawImage(
                    image = imageBitmap,
                    dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
                    blendMode = BlendMode.DstIn
                )

            }
        ) {

            Image(
                painterResource(id = R.drawable.squirtle),
                modifier = Modifier
                    .size(150.dp),
                contentScale = ContentScale.Crop,
                contentDescription = ""
            )
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

这里有两点需要注意

1- 混合模式是BlendMode.DstIn因为我们想要形状为Destination2-Source 在 ContentDrawScope 内绘制图像的纹理,并使用 dstSize 来匹配可组合大小。默认情况下,它是使用上面发布的 png 大小绘制的。

创建带有切口形状的 BottomNavigation

@Composable
private fun BottomBarWithCutOutShape() {
    val density = LocalDensity.current
    val shapeSize = density.run { 70.dp.toPx() }

    val cutCornerShape = CutCornerShape(50)
    val outline = cutCornerShape.createOutline(
        Size(shapeSize, shapeSize),
        LocalLayoutDirection.current,
        density
    )

    val icons =
        listOf(Icons.Filled.Home, Icons.Filled.Map, Icons.Filled.Settings, Icons.Filled.LocationOn)

    Box(
        modifier = Modifier.fillMaxWidth()
    ) {
        BottomNavigation(
            modifier = Modifier
                .drawWithLayer {
                    with(drawContext.canvas.nativeCanvas) {

                        val checkPoint = saveLayer(null, null)
                        val width = size.width

                        val outlineWidth = outline.bounds.width
                        val outlineHeight = outline.bounds.height

                        // Destination
                        drawContent()

                        // Source
                        withTransform(
                            {
                                translate(
                                    left = (width - outlineWidth) / 2,
                                    top = -outlineHeight / 2
                                )
                            }
                        ) {
                            drawOutline(
                                outline = outline,
                                color = Color.Transparent,
                                blendMode = BlendMode.Clear
                            )
                        }

                        restoreToCount(checkPoint)
                    }
                },
            backgroundColor = Color.White
        ) {

            var selectedIndex by remember { mutableStateOf(0) }

            icons.forEachIndexed { index, imageVector: ImageVector ->
                if (index == 2) {
                    Spacer(modifier = Modifier.weight(1f))
                    BottomNavigationItem(
                        icon = { Icon(imageVector, contentDescription = null) },
                        label = null,
                        selected = selectedIndex == index,
                        onClick = {
                            selectedIndex = index
                        }
                    )
                } else {
                    BottomNavigationItem(
                        icon = { Icon(imageVector, contentDescription = null) },
                        label = null,
                        selected = selectedIndex == index,
                        onClick = {
                            selectedIndex = index
                        }
                    )
                }
            }
        }

        // This is size fo BottomNavigationItem
        val bottomNavigationHeight = LocalDensity.current.run { 56.dp.roundToPx() }

        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.TopCenter)
                .offset {
                    IntOffset(0, -bottomNavigationHeight / 2)
                },
            shape = cutCornerShape,
            onClick = {}
        ) {
            Icon(imageVector = Icons.Default.Add, contentDescription = null)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码有点长,但我们基本上像往常一样创建一个形状并创建一个要剪辑的轮廓

    val cutCornerShape = CutCornerShape(50)
    val outline = cutCornerShape.createOutline(
        Size(shapeSize, shapeSize),
        LocalLayoutDirection.current,
        density
    )
Run Code Online (Sandbox Code Playgroud)

And before clipping we move this shape section up as half of the height to cut only with half of the outline

withTransform(
{
    translate(
        left = (width - outlineWidth) / 2,
        top = -outlineHeight / 2
    )
}
) {
    drawOutline(
        outline = outline,
        color = Color.Transparent,
        blendMode = BlendMode.Clear
    )
}
Run Code Online (Sandbox Code Playgroud)

Also to have a BottomNavigation such as BottomAppBar that places children on both side i used a Spacer

icons.forEachIndexed { index, imageVector: ImageVector ->
    if (index == 2) {
        Spacer(modifier = Modifier.weight(1f))
        BottomNavigationItem(
            icon = { Icon(imageVector, contentDescription = null) },
            label = null,
            selected = selectedIndex == index,
            onClick = {
                selectedIndex = index
            }
        )
    } else {
        BottomNavigationItem(
            icon = { Icon(imageVector, contentDescription = null) },
            label = null,
            selected = selectedIndex == index,
            onClick = {
                selectedIndex = index
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

Then we simply add a FloatingActionButton, i used offset but you can create a bigger parent and put our custom BottomNavigation and button inside it.