返回

Jetpack Compose 立体按钮终极指南:告别扁平设计!

Android

在 Jetpack Compose 中,想让按钮看起来有立体感,可不是简单加个阴影就完事了。很多人觉得加个黑白阴影就行了,结果做出来却扁平扁平的,完全没有预期的效果。 这篇文章就来聊聊怎么在 Compose 里做出真正有“体积感”的按钮。

我们先来看看一般人容易犯的错误。 他们通常会用 Spacer 来加阴影,一个黑色一个白色,试图模拟光从上面照下来的效果。想法是没错,但 Spacer 的用法和阴影参数的设置才是关键。如果 Spacer 几乎占满了整个按钮,还叠在一起,阴影就会互相干扰,看起来模糊不清,反而更扁平了。 再加上如果用了高斯模糊(blur),那效果就更糟了。

那要怎么做才能做出立体的按钮呢?核心在于理解光影的变化,以及怎么用代码来模拟这种变化。 现实世界里的物体之所以看起来有体积,是因为光照在上面形成了明暗过渡,而不是简单的黑白两色。 所以,只用两个简单的阴影肯定是不够的,我们需要更精确地控制阴影的颜色、大小和位置。

下面提供一个更有效的做法,用 Modifier.drawBehind 来画更精细的阴影,再用渐变色来模拟光照效果:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.NumberFormat


@Composable
fun VolumableButton(
    modifier: Modifier = Modifier,
    coins: Int,
    onClick: () -> Unit = {},
) {
    val shape = RoundedCornerShape(12.dp)
    val buttonColor = Color.White.copy(alpha = 0.08f)
    val shadowColorTop = Color.Black.copy(alpha = 0.28f)
    val shadowColorBottom = Color.White.copy(alpha = 0.08f)

    Box(
        modifier = modifier
            .clip(shape)
            .drawBehind {
                drawButtonShadow(shape, shadowColorTop, shadowColorBottom, 2.dp)
            }
            .background(buttonColor)
            .padding(vertical = 8.dp, horizontal = 12.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = NumberFormat.getNumberInstance().format(coins),
            fontSize = 18.sp,
            lineHeight = 23.4.sp,
            fontWeight = FontWeight.W600,
        )
    }
}


private fun DrawScope.drawButtonShadow(
    shape: Shape,
    topShadowColor: Color,
    bottomShadowColor: Color,
    elevation: Dp
) {
    val offsetPx = elevation.toPx()
    val topShadow = Path().apply {
        addOuterShadow(shape.toPath(size), offsetPx, 0f, topShadowColor)
        close()
    }

    val bottomShadow = Path().apply {
        addOuterShadow(shape.toPath(size), -offsetPx / 2, offsetPx, bottomShadowColor)
        close()

    }
    drawPath(topShadow, topShadowColor)
    drawPath(bottomShadow, bottomShadowColor)
}


fun Path.addOuterShadow(
    target: Path,
    offsetX: Float,
    offsetY: Float,
    shadowColor: Color
) {

    val shadowPath = Path()
    target.transform(
        Matrix().apply {
            translate(offsetX, offsetY)
        }
    ) { outline ->
        shadowPath.addPath(outline)
    }
    op(this, shadowPath, PathOperation.Union)


}

这段代码的关键是 drawButtonShadow 函数。 它用 drawBehind Modifier 来画阴影,并用 PathMatrix 精确控制阴影的形状和位置。 它分别画了顶部和底部的阴影,用了不同的颜色和偏移量,这样就能模拟出更真实的立体感。 而且,它没有用高斯模糊,让阴影更清晰,避免了模糊带来的扁平感。 通过调整 shadowColorTopshadowColorBottom 的 alpha 值,可以更细致地控制阴影的强度,达到想要的视觉效果。

这个方案的核心就是用更精细的阴影绘制来模拟光影效果,而不是简单地叠加两个模糊的阴影。 这让按钮看起来更立体,也更符合设计稿的预期。 在 UI 设计中,细节很重要。只有把细节做好,才能做出真正好的用户体验。

常见问题解答:

  1. 为什么我的按钮看起来还是扁平的? 可能是阴影的颜色和偏移量设置不合适。 尝试调整 shadowColorTopshadowColorBottomelevation 的值,看看效果如何变化。

  2. drawBehind Modifier 是如何工作的? 它允许你在 Composable 的内容后面绘制自定义图形。 这对于创建自定义阴影和背景效果非常有用。

  3. 如何改变按钮的形状? 修改 RoundedCornerShape 的参数可以改变按钮的圆角大小。 你也可以使用其他形状,例如 CircleShapeCutCornerShape

  4. 如何改变按钮的颜色? 修改 buttonColor 的值可以改变按钮的颜色。

  5. 这个方案的性能如何? 由于使用了 drawBehind,可能会对性能有一些影响,尤其是在处理大量按钮时。 但在大多数情况下,这种影响是可以忽略不计的。 如果性能成为瓶颈,可以考虑使用更轻量级的方案。