Compose でシャドウを追加する

シャドウは、UI を視覚的に引き立て、ユーザーの操作にすばやく反応し、インタラクティブ性を示します。Compose には、アプリにシャドウを組み込むための方法がいくつか用意されています。

  • Modifier.shadow(): マテリアル デザイン ガイドラインに準拠したコンポーザブルの背後に、高度に基づいた影を作成します。
  • Modifier.dropShadow(): コンポーザブルの背後に表示されるカスタマイズ可能な影を作成し、コンポーザブルが浮き上がって見えるようにします。
  • Modifier.innerShadow(): コンポーザブルの境界線の内側に影を作成し、背後のサーフェスに押し込まれているように見せます。

Modifier.shadow() は基本的なシャドウの作成に適しています。一方、dropShadow() 修飾子と innerShadow() 修飾子を使用すると、シャドウのレンダリングをより細かく制御して精度を高めることができます。

このページでは、ユーザー操作時にシャドウをアニメーション表示する方法や、innerShadow() 修飾子と dropShadow() 修飾子を連結してグラデーション シャドウやニューモーフィック シャドウなどを作成する方法など、これらの修飾子を実装する方法について説明します。

基本的なシャドウを作成する

Modifier.shadow() は、上からの光源をシミュレートする マテリアル デザイン ガイドラインに沿った基本的なシャドウを作成します。シャドウの深さは 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 の広がり。影のサイズを拡大し、影を投影するボックスよりも大きくする
    • アルファ値は 0.5f で、影は半透明になります。
  • シャドウを定義したら、background() 修飾子が適用されます。
    • Box は白色で塗りつぶされています。
    • 背景は、シャドウと同じ角の丸い長方形の形にクリップされます。

結果

白い長方形の周りに灰色のドロップ シャドウが投影されている。
図 2. シェイプの周囲に描画されるドロップ シャドウ。

インナー シャドウを実装する

dropShadow() と逆のエフェクトを作成するには、Modifier.innerShadow() を使用します。これにより、要素が凹んでいるか、下のサーフェスに押し込まれているような視覚効果が生まれます。

インナー シャドウを作成する場合、順序が重要となります。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.animateColor と transition.animateFloat を使用して、押したときにアニメーション化するパラメータの開始状態と終了状態を宣言します。
  • updateTransition を使用し、選択した targetState (targetState = isPressed) を指定して、すべてのアニメーションが同期されていることを確認します。isPressed が変更されるたびに、遷移オブジェクトは、すべての子プロパティのアニメーションを現在の値から新しいターゲット値に自動的に管理します。
  • buttonPressAnimation 仕様を定義します。これにより、遷移のタイミングとイージングを制御します。これは、400 ミリ秒の長さと EaseInOut 曲線を持つ tween(in-between の略)を指定します。つまり、アニメーションはゆっくりと始まり、途中で加速し、最後に減速します。
  • Box を定義します。これには、アニメーション化されたすべてのプロパティを適用してビジュアル要素を作成する修飾子関数のチェーンが含まれます。これには、次のものが含まれます。
    • .clickable(): Box をインタラクティブにする修飾子。
    • .dropShadow(): 最初に 2 つの外側のドロップ シャドウが適用されます。色とアルファ プロパティはアニメーション値(blueDropShadow など)にリンクされ、最初の浮き上がった外観を作成します。
    • .innerShadow(): 背景の上に 2 つのインナー シャドウが描画されます。それらのプロパティは、別のアニメーション値のセット(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. 2 つの薄いドロップ シャドウを反対方向に適用します。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
            )
        }
    }
}

コードに関する主なポイント

  • 異なるプロパティを持つ 2 つの dropShadow() 修飾子がチェーンされ、その後に background() 修飾子が適用されます。
  • チェーン化された innerShadow() 修飾子が適用され、コンポーネントの端に金属製のリム効果が形成されます。

結果

上のコード スニペットは、次の内容を生成します。

黒い丸みを帯びた形状の周りに白いリアルな影。
図 8. リアルなシャドウ効果。