在 Compose 中新增陰影

陰影可從視覺上提升 UI,向使用者表明互動性,並針對使用者動作提供即時回饋。Compose 提供多種方式,可將陰影納入應用程式:

  • Modifier.shadow():在符合 Material Design 指南的可組合函式後方,建立以高度為準的陰影。
  • Modifier.dropShadow():建立可自訂的陰影,顯示在可組合函式後方,讓函式看起來有立體感。
  • Modifier.innerShadow():在可組合函式的邊界內建立陰影,使其看起來像是壓入後方表面。

Modifier.shadow() 適合建立基本陰影,而 dropShadowinnerShadow 修飾符則可更精細地控制陰影的算繪方式。

本頁說明如何實作這些修飾符,包括如何在使用者互動時製作陰影動畫,以及如何串連 innerShadow()dropShadow() 修飾符來建立漸層陰影擬物化陰影等。

建立基本陰影

Modifier.shadow() 會根據 Material Design 指南建立基本陰影,模擬來自上方的光源。陰影深度取決於 elevation 值,而投射陰影會裁剪為可組合函式的形狀。

@Composable
fun ElevationBasedShadow() {
    Box(
        modifier = Modifier.aspectRatio(1f).fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Box(
            Modifier
                .size(100.dp, 100.dp)
                .shadow(10.dp, RectangleShape)
                .background(Color.White)
        )
    }
}

白色矩形周圍投射灰色陰影。
圖 1. 使用 Modifier.shadow 建立的陰影,以高度為準。

實作投射陰影

使用 dropShadow() 修飾符在內容後方繪製精確的陰影,讓元素看起來有立體感。

您可以透過 Shadow 參數控制下列重要層面:

  • radius:定義模糊效果的柔和度和擴散程度。
  • color:定義色調的顏色。
  • offset:沿著 x 軸和 y 軸放置陰影的幾何圖形。
  • spread:控制陰影幾何圖形的擴展或收縮。

此外,shape 參數會定義陰影的整體形狀。它可以採用 androidx.compose.foundation.shape 套件中的任何幾何圖形,以及 Material Expressive 形狀

如要實作基本投射陰影,請在可組合函式鏈結上新增 dropShadow() 修飾符,並提供半徑、顏色和擴散。請注意,顯示在陰影頂端的 purpleColor 背景是在 dropShadow() 修飾符之後繪製:

@Composable
fun SimpleDropShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(300.dp)
                .dropShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 6.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 4.dp, 4.dp)
                    )
                )
                .align(Alignment.Center)
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
        ) {
            Text(
                "Drop Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

程式碼重點

  • dropShadow() 修飾元會套用至內部 Box。陰影具有下列特徵:
    • 圓角矩形 (RoundedCornerShape(20.dp))
    • 模糊半徑為 10.dp,使邊緣柔和且擴散
    • 擴散值 6.dp,可擴大陰影大小,使陰影大於投射陰影的方塊
    • Alpha 值為 0.5f,使陰影呈現半透明狀態
  • 定義陰影後,已套用 background() 修飾符。
    • Box 填滿白色。
    • 背景會裁剪成與陰影相同的圓角矩形。

結果

白色矩形周圍投射灰色陰影。
圖 2. 形狀周圍繪製的陰影。

導入內陰影

如要建立 dropShadow 的反向效果,請使用 Modifier.innerShadow(),這會產生元素凹陷或壓入底層表面的錯覺。

建立內陰影時,順序十分重要。內陰影會繪製在內容的頂端,因此您通常應執行下列操作:

  1. 繪製背景內容。
  2. 套用 innerShadow() 修飾符,即可建立凹面外觀。

如果 innerShadow() 放置在背景之前,系統會在陰影上繪製背景,完全遮蓋陰影。

以下範例顯示在 RoundedCornerShape 上套用 innerShadow() 的方式:

@Composable
fun SimpleInnerShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 2.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 6.dp, 7.dp)
                    )
                )

        ) {
            Text(
                "Inner Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

白色矩形內有灰色內陰影。
圖 3. 在圓角矩形上套用 Modifier.innerShadow()

在使用者互動時為陰影加上動畫效果

如要讓陰影回應使用者互動,可以將陰影屬性與 Compose 的動畫 API 整合。舉例來說,使用者按下按鈕時,陰影可以改變,提供即時的視覺回饋。

以下程式碼會建立帶有陰影的「按下」效果 (表面被推入螢幕的錯覺):

@Composable
fun AnimatedColoredShadows() {
    SnippetsTheme {
        Box(Modifier.fillMaxSize()) {
            val interactionSource = remember { MutableInteractionSource() }
            val isPressed by interactionSource.collectIsPressedAsState()

            // Create transition with pressed state
            val transition = updateTransition(
                targetState = isPressed,
                label = "button_press_transition"
            )

            fun <T> buttonPressAnimation() = tween<T>(
                durationMillis = 400,
                easing = EaseInOut
            )

            // Animate all properties using the transition
            val shadowAlpha by transition.animateFloat(
                label = "shadow_alpha",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) 0f else 1f
            }
            // ...

            val blueDropShadow by transition.animateColor(
                label = "shadow_color",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) Color.Transparent else blueDropShadowColor
            }

            // ...

            Box(
                Modifier
                    .clickable(
                        interactionSource, indication = null
                    ) {
                        // ** ...... **//
                    }
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = blueDropShadow,
                            offset = DpOffset(x = 0.dp, -(2).dp),
                            alpha = shadowAlpha
                        )
                    )
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = darkBlueDropShadow,
                            offset = DpOffset(x = 2.dp, 6.dp),
                            alpha = shadowAlpha
                        )
                    )
                    // note that the background needs to be defined before defining the inner shadow
                    .background(
                        color = Color(0xFFFFFFFF),
                        shape = RoundedCornerShape(70.dp)
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 8.dp,
                            spread = 4.dp,
                            color = innerShadowColor2,
                            offset = DpOffset(x = 4.dp, 0.dp)
                        )
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 20.dp,
                            spread = 4.dp,
                            color = innerShadowColor1,
                            offset = DpOffset(x = 4.dp, 0.dp),
                            alpha = innerShadowAlpha
                        )
                    )

            ) {
                Text(
                    "Animated Shadows",
                    // ...
                )
            }
        }
    }
}

程式碼重點

  • 宣告參數的開始和結束狀態,以便在按下 transition.animateColortransition.animateFloat 時產生動畫。
  • 使用 updateTransition 並提供所選 targetState (targetState = isPressed),確認所有動畫都已同步。每當 isPressed 變更時,轉場物件會自動管理所有子項屬性的動畫,從目前值變更為新的目標值。
  • 定義 buttonPressAnimation 規格,可控制轉場效果的時間和緩和程度。這會指定 tween (中間的簡寫),時間長度為 400 毫秒,並使用 EaseInOut 曲線,也就是說動畫開始時速度較慢,中間會加快,最後會減慢。
  • 定義 Box,並使用一連串的修飾符函式套用所有動畫屬性,以建立視覺元素,包括:
    • .clickable():可讓 Box 互動的修飾符。
    • .dropShadow():系統會先套用兩個外部投射陰影,顏色和 Alpha 屬性會連結至動畫值 (blueDropShadow 等),並建立初始的凸起外觀。
    • .innerShadow():在背景上繪製兩個內陰影。 這些屬性會連結至另一組動畫值 (innerShadowColor1 等),並建立縮排外觀。

結果

圖 4. 使用者按下時的陰影動畫。

建立漸層陰影

陰影不限於實心顏色,陰影 API 會接受 Brush,讓您建立漸層陰影。

Box(
    modifier = Modifier
        .width(240.dp)
        .height(200.dp)
        .dropShadow(
            shape = RoundedCornerShape(70.dp),
            shadow = Shadow(
                radius = 10.dp,
                spread = animatedSpread.dp,
                brush = Brush.sweepGradient(
                    colors
                ),
                offset = DpOffset(x = 0.dp, y = 0.dp),
                alpha = animatedAlpha
            )
        )
        .clip(RoundedCornerShape(70.dp))
        .background(Color(0xEDFFFFFF)),
    contentAlignment = Alignment.Center
) {
    Text(
        text = breathingText,
        color = Color.Black,
        style = MaterialTheme.typography.bodyLarge
    )
}

程式碼重點

  • dropShadow() 會在方塊後方加上陰影。
  • brush = Brush.sweepGradient(colors) 會使用漸層為陰影著色,並透過預先定義的 colors 清單輪流套用顏色,營造彩虹般的效果。

結果

你可以使用筆刷做為陰影,建立具有「呼吸」動畫的漸層 dropShadow()

圖 5. 動畫漸層投射陰影。

合併陰影

您可以組合及分層使用 dropShadow()innerShadow() 修飾符,建立各種效果。以下各節將說明如何運用這項技術,製作新擬物、新粗獷主義和真實陰影。

建立擬物陰影

新擬物陰影的特色是柔和的外觀,會從背景自然浮現。如要建立擬物化陰影,請按照下列步驟操作:

  1. 使用與背景顏色相同的元素。
  2. 套用兩個相對的淺色投射陰影:一個角落套用淺色陰影,另一個角落套用深色陰影。

下列程式碼片段會疊加兩個 dropShadow() 修飾符,建立擬物化效果:

@Composable
fun NeumorphicRaisedButton(
    shape: RoundedCornerShape = RoundedCornerShape(30.dp)
) {
    val bgColor = Color(0xFFe0e0e0)
    val lightShadow = Color(0xFFFFFFFF)
    val darkShadow = Color(0xFFb1b1b1)
    val upperOffset = -10.dp
    val lowerOffset = 10.dp
    val radius = 15.dp
    val spread = 0.dp
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(bgColor)
            .wrapContentSize(Alignment.Center)
            .size(240.dp)
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = lightShadow,
                    spread = spread,
                    offset = DpOffset(upperOffset, upperOffset)
                ),
            )
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = darkShadow,
                    spread = spread,
                    offset = DpOffset(lowerOffset, lowerOffset)
                ),

            )
            .background(bgColor, shape)
    )
}

白色背景中的白色矩形,具有擬物化效果。
圖 6. 新擬物陰影效果。

建立新粗獷主義陰影

新粗獷主義風格的特色是高對比、方塊狀的版面配置、鮮豔的色彩和粗邊框。如要建立這項效果,請使用模糊程度為零的 dropShadow(),並設定明顯的偏移,如以下程式碼片段所示:

@Composable
fun NeoBrutalShadows() {
    SnippetsTheme {
        val dropShadowColor = Color(0xFF007AFF)
        val borderColor = Color(0xFFFF2D55)
        Box(Modifier.fillMaxSize()) {
            Box(
                Modifier
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(0.dp),
                        shadow = Shadow(
                            radius = 0.dp,
                            spread = 0.dp,
                            color = dropShadowColor,
                            offset = DpOffset(x = 8.dp, 8.dp)
                        )
                    )
                    .border(
                        8.dp, borderColor
                    )
                    .background(
                        color = Color.White,
                        shape = RoundedCornerShape(0.dp)
                    )
            ) {
                Text(
                    "Neobrutal Shadows",
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

黃色背景上,白色矩形周圍有紅色邊框,並帶有藍色陰影。
圖 7. 新粗獷主義陰影效果。

建立逼真的陰影

真實陰影會模擬實體世界的陰影,看起來像是主要光源照亮,因此會產生直接陰影和較為擴散的陰影。您可以堆疊多個具有不同屬性的 dropShadow()innerShadow() 執行個體,重現逼真的陰影效果,如下列程式碼片段所示:

@Composable
fun RealisticShadows() {
    Box(Modifier.fillMaxSize()) {
        val dropShadowColor1 = Color(0xB3000000)
        val dropShadowColor2 = Color(0x66000000)

        val innerShadowColor1 = Color(0xCC000000)
        val innerShadowColor2 = Color(0xFF050505)
        val innerShadowColor3 = Color(0x40FFFFFF)
        val innerShadowColor4 = Color(0x1A050505)
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 40.dp,
                        spread = 0.dp,
                        color = dropShadowColor1,
                        offset = DpOffset(x = 2.dp, 8.dp)
                    )
                )
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 0.dp,
                        color = dropShadowColor2,
                        offset = DpOffset(x = 0.dp, 4.dp)
                    )
                )
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.Black,
                    shape = RoundedCornerShape(100.dp)
                )
// //
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 3.dp,
                        color = innerShadowColor1,
                        offset = DpOffset(x = 6.dp, 6.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 1.dp,
                        color = Color.White,
                        offset = DpOffset(x = 5.dp, 5.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 5.dp,
                        color = innerShadowColor2,
                        offset = DpOffset(x = (-3).dp, (-12).dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 10.dp,
                        color = innerShadowColor3,
                        offset = DpOffset(x = 0.dp, 0.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 9.dp,
                        color = innerShadowColor4,
                        offset = DpOffset(x = 1.dp, 1.dp)
                    )
                )

        ) {
            Text(
                "Realistic Shadows",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 24.sp,
                color = Color.White
            )
        }
    }
}

程式碼重點

  • 套用兩個具有不同屬性的鏈結 dropShadow() 修飾符, 然後套用 background 修飾符。
  • 鏈結的 innerShadow() 修飾符會套用至元件邊緣,形成金屬邊框效果。

結果

上述程式碼片段會產生下列內容:

黑色圓角形狀周圍有白色擬真陰影。
圖 8.逼真的陰影效果。