Thr*_*ian 7 android android-jetpack-compose
如何剪辑或剪切可组合内容以使图像、按钮或可组合项具有自定义形状?这个问题不是关于使用Modifier.clip(),更像是使用替代方法来完成任务,这些方法允许产生不可能的结果,或者当很难创建像云或方圆这样的形状时。
这是分享您的知识、问答式问题,其灵感来自 M3 BottomAppBar 或 BottomNavigation 没有切口形状,无法找到问题,并且像这个问题一样绘制Squircle形状很困难。
我们非常欢迎更多更好的剪切或自定义形状和可组合项的方法。
无需创建自定义可组合项即可实现剪切或裁剪可组合项的方法之一是使用
Modifier.drawWithContent{}具有层和 aBlendMode或PorterDuff 模式。
使用 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 大小绘制的。
@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.
| 归档时间: |
|
| 查看次数: |
4002 次 |
| 最近记录: |