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 パッケージの任意のジオメトリと、マテリアル エクスプレッシブ シェイプを使用できます。

基本的なドロップ シャドウを実装するには、コンポーザブル チェーンに 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() を使用します。これにより、要素が下のサーフェスに凹んでいるか、押し込まれているかのような錯覚が生じます。

内側のシャドウを作成する場合、順序は重要です。内側のシャドウはコンテンツのに描画されるため、通常は次のことを行う必要があります。

  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 仕様を定義します。400 ミリ秒の期間と EaseInOut カーブの tween(中間)を指定します。これは、アニメーションがゆっくりと始まり、中盤で加速し、終盤で減速することを意味します。
  • 次のものを含む、すべてのアニメーション プロパティを適用してビジュアル要素を作成する修飾子関数のチェーンを含む 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. リアルな影の効果。