Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

ModifierのdropShadowは何が新しいのかを考える



Androidエンジニアの藤原(@fuji_tech7)です。

Jetpack Compose を使った UI 実装で、最近私が悩まされているのが Shadow(影)表現 です。

  • Material Design の仕様に忠実にしたい
  • デザイナー指定の微妙な影を再現したい
  • それでいてパフォーマンスは落としたくない

こうした要求に対して、従来の Compose の shadow() API では
「微調整が難しい」「表現に限界がある」と感じています。
ミラティブでもFigmaで表現している影の再現に苦労する、または妥協案で許容してもらうことがありました。

Compose 1.9 では、こうした課題に応える形で
新しい Shadow API(dropShadow / innerShadow が追加されました。

本記事では、この新しい Shadow API が何を解決し
従来の shadow 表現と何がどう変わったのか、これからの影表現をどう実装していくかを
実装例と設計観点の両面から整理していきます。

※本記事のサンプルコードはCompose Bom"2025.12.01"のバージョンで作成しています。実行環境によっては異なる結果となることが予想されます。ご留意ください。

目次



新しい Shadow API

なぜ新しい Shadow API が必要だったのか

Jetpack Compose ではこれまで、Modifier.shadow()Modifier.graphicsLayer によって影を表現するのが一般的でした。
しかし、実際のプロダクト開発では「影をつけたいだけなのに、制約が多い」と感じる場面がありました。

例えば、影の形状がコンポーネントの形に強く結びついていたり、 背景色やクリップとの組み合わせによって意図しない描画結果になることがあります。
これは、影が「装飾」ではなくelevationによって描画されているからです。

Compose 1.9 で導入された新しい Shadow API は、elevationを介さず色、方向、広がりを自由に設定できるため
独立した視覚表現としての影を描画させることができます。

Android Developers Blogでも紹介されており
shadow()は照明効果なのに対し、dropShadow / innerShadowbox-shadow効果 (CSSのbox-shadowプロパティを指すと思われます)を適用する、とされています。

android-developers.googleblog.com



新 Shadow API が解決する課題

新しい Shadow API(dropShadow / innerShadow)を用いることで
影を UI 表現の一要素として、より直感的に扱えるようなります。

従来の API では、影はレイアウトや shape に密結合していました。
そのため、デザイン上は単純な影であっても、実装上は複雑な graphicsLayer や Modifier の追加が必要になることがありました。

新 Shadow API では、「どんな形状に、どんな影を重ねるか」を自由にdropShadow / innerShadow内で完結させることができます。

従来のシャドウの作り方

Jetpack Compose では、長らく「影を表現するための決定版 API」が存在せず、用途に応じて複数の手段を使い分ける必要がありました。
代表的なのが Modifier.shadow(), Modifier.graphicsLayer, Modifier.drawBehind の3つです。

ここでは、それぞれの記述例と実際の見え方を紹介します。

Modifier.shadow()

最も手軽に影を付けられる API です。
Material コンポーネントの elevation 表現と親和性が高く、簡単な UI では十分に機能します。

Box(
    modifier = Modifier
        .size(120.dp)
        .shadow(
            elevation = 8.dp,
            shape = RoundedCornerShape(12.dp),
            spotColor = Color.Black,
        )
        .background(Color.White)
)

しかしこの API には制約があります。

  • blur 半径やオフセットを制御できない
  • elevation に依存した Material 的な影表現に限定される

結果として、「デザイン通りの影を再現したい」ケースでは不足することが多くありました。

Modifier.graphicsLayer によるシャドウ表現

より柔軟な表現が必要な場合、graphicsLayer を使って影を描画する方法が取られてきました。

Box(
    modifier = Modifier
        .size(120.dp)
        .graphicsLayer {
            shadowElevation = 12.dp.toPx()
            spotShadowColor = Color.Black
            shape = RoundedCornerShape(12.dp)
            clip = false
        }
        .background(Color.White)
)

graphicsLayer を使うことで shadowElevation を直接指定できますが、

  • pxでの指定が必要になり、密度変換する必要がある
  • 依然としてblur の調整はできない
  • レイヤー生成によるパフォーマンス影響を考慮する必要がある

といった設計上の負債を抱えやすい API でした。

Modifier.drawBehind によるカスタム描画

最終手段として使われていたのが drawBehind です。
Canvas を直接操作することで、完全に自由な影表現が可能になります。

Box(
    modifier = Modifier
        .size(120.dp)
        .drawBehind {
            drawRoundRect(
                color = Color.Black.copy(alpha = 0.2f),
                size = size,
                cornerRadius = CornerRadius(24f, 24f),
                style = Fill
            )
        }
        .background(Color.White, RoundedCornerShape(12.dp))
)

この方法は柔軟ですが、

  • 影としての再利用性が低い
  • blur やオフセットの実装が煩雑
  • リアルな影の描画をするには実装コストが高い

という問題があり、選択し難いものでした。

従来のシャドウの課題点

Compose 1.8 以前のシャドウ表現は、

  • 簡単だが表現力が足りない
  • 自由だが実装コストと設計負債が大きい

という両極端な選択肢しかありませんでした。

このギャップを埋めるために登場したのが、次章で紹介する 新しい Shadow API です。

新しい Shadow APIの使い方

基本的な dropShadow

最もシンプルな例です。
背景・レイアウト・影の責務が明確に分離 されている点が特徴です。

Box(
    modifier = Modifier
        .size(100.dp)
        .dropShadow(
            shape = RectangleShape,
            shadow = Shadow(
                radius = 12.dp,
                color = Color.Black.copy(alpha = 0.2f),
                offset = DpOffset(
                    x = 4.dp,
                    y = 4.dp,
                )
            )
        )
         .background(
            color = Color.White,
            shape = RoundedCornerShape(12.dp)
        )
)

簡単に影を描画しつつ、課題でもあったBlur、影の方向も指定可能となっています。

Spread(影の広がり具合)も設定可能なのでFigmaでのDropShadowの設定値がそのまま設定可能になりました。
FigmaでのShadowの設定は以下のようになっており

  • XY -> offset
  • Blur -> radius(影のぼかし具合なのでraduisに)
  • Spread -> spread

として設定可能です。



innerShadow による凹み表現

新 API の特徴のひとつが inner shadow(内側の影) です。

Box(
    modifier = Modifier
        .size(100.dp)
        .background(
            color = Color.White,
            shape = RoundedCornerShape(12.dp)
        )
        .innerShadow(
            shape = RoundedCornerShape(12.dp),
            shadow = Shadow(
                radius = 12.dp,
                color = Color.Black.copy(alpha = 0.2f),
            ),
        )
)

ミラティブでは内側に影をつけるケースはありませんが、押し込まれたボタンの表現に今後使うことがあるかもしれません。

Shadow API 導入時の注意点(パフォーマンス・再コンポジション)

新しい Shadow API(dropShadow / innerShadow)は表現力が高い一方で
無意識に使うとパフォーマンスや再コンポジションに影響を与える可能性があります。
ここでは、導入時に意識しておきたいポイントを整理します。

シャドウは「描画コスト」を持つ

Shadow API は内部的に 描画処理(描画レイヤ・ブラー処理) を伴います。
特に以下の条件が重なると、描画コストが増えやすくなります。

  • 大きな blur radius
  • 不透明度の高い影
  • 画面内に多数の Shadow 要素が存在する場合

上記のような影を描画する際には注意しましょう。

Lazy 系コンポーネントは更に注意

LazyColumn / LazyRow 内で Shadow API を使う場合、

  • スクロール中に描画が発生する
  • セル数が多いほど影響が顕在化する という特性があります。

その場合は既存のModifier.shadowを利用する、またはdropShadowのパラメータにshadowではなくblock(DropShadowScope)を指定しましょう。

Box(
    modifier = Modifier
        .size(100.dp)
        .dropShadow(
            shape = RectangleShape,
            block = {
                color = Color.Black.copy(alpha = 0.2f)
                radius = 12.dp.toPx()
                offset = Offset(
                    x = 4.dp.toPx(),
                    y = 4.dp.toPx()
                )
            }
        )
        .background(
            color = Color.White,
            shape = RoundedCornerShape(12.dp)
        )
)

DropShadowScopeを指定することでアニメーション時の再コンポジションを避けることが可能です。
これは公式ドキュメントにも記載されています。

developer.android.com

ただし、DropShadowScopeの内部はpx指定となってしまい、密度変換を要してしまうのが非常に惜しいです。

「常に使うAPI」ではなく「選択肢が増えた」と捉える

Shadow API は万能な置き換えではありません。

  • シンプルな elevation → Modifier.shadow()
  • 表現が重要な UI → dropShadow / innerShadow
  • パフォーマンス最重視 → 影自体を使わない

Compose における Shadow は、 UI の意味とコストを天秤にかけて選択する設計要素だと言えます。

Shadow API は、Compose における UI 表現の自由度を一段引き上げた API ですが
利用者の判断力がより問われる領域でもあります。

使い分け指針

影の表現方法が増えたので、新しいShadow APIと既存手法をどのように使い分けるべきかを整理します。

シンプルに影をつけたい場合、shadowが最もシンプルに書けるので、簡単に影を表現したい場合はshadowで構いません。

UIの「意味」を表したい、方向やぼかし具合といった複雑な影を表現したい場合は 新しいShadow API を使う。 浮いている、押されている、といったコンポーネントの意味を表したい場合に適しています。

スケール・回転・Z軸操作などと連動した動的に変形する影表現が必要な場合、graphicsLayer は今後も避けられない選択肢です。

Shadow API の登場でdrawBehind で影を表現するケースは減ると思われますが dropShadow, innerShadowで表現できない、人形の影のような独自表現が必要な場合はdrawBehind を使うことがあるかもしれません。

表にすると以下のようになります。

使いたいケース 影の作り方
シンプルに影を作りたい shadow
UIに意味をもたせたい dropShadow / innerShadow
動的に影を変化させたい graphicsLayer
独自表現を行いたい drawBehind

選択は「表現」ではなく「意図」で行う

シャドウ表現の選択は、 「どれが描けるか」ではなく 「この影は何を意味するのか」 から考えるべきです。

  • UIの意味 → Shadow API
  • 簡易的 → shadow
  • 動的表現 → graphicsLayer
  • 例外的表現 → drawBehind

この指針を持つことで、Compose におけるシャドウ設計は より読みやすく、変更に強いものになります。

全体のまとめ

新しい Shadow API は

  • シャドウ表現を 専用の API として分離
  • UI の意図(外側か、内側か、装飾か)を コード上で明示
  • 描画ロジックを Compose に委ねることで 実装と設計のノイズを削減

といった点で、UI 設計そのものをシンプルにします。

一方で、すべての影を新 API に置き換えればよいわけではなく
既存コンポーネントとの整合性や、描画コスト、再コンポジションの影響などを考慮し、適切な箇所で利用することが重要と考ええられます。

Shadow API は魔法の道具ではなく、 「影をどう扱うか」という設計判断を、より正しくコードに反映できる選択肢だと言えるでしょう。

Compose の UI 表現が複雑化していく中で、 こうした 専用 API による責務の明確化 は、今後さらに重要になっていくはずです。



We are hiring!

ミラティブでは一緒に開発してくれるAndroidエンジニアを募集しています! 少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、お気軽にご連絡ください。

hrmos.co

mirrativ.notion.site