Jetpack ComposeでのMaterial Youダイナミックカラー:壁紙が勝ったときにブランドカラーを守る方法
dynamicColorScheme()はユーザーの壁紙から生成されたパレットであなたのパレットを置き換える。ブランドカラーを失うのではなく調和させるための実践ガイド。
Composeアプリでダイナミックカラーを有効にすると、最初に気づくのはブランドが消えることだ。Granynの緑、Hydrameの青 — 消えて、dynamicColorScheme()がユーザーの壁紙から導き出した何かに置き換わる。標準的な青い壁紙なら問題ない。オレンジやマゼンタの壁紙だと、視覚的アイデンティティ全体が「緑のあれ」や「青のあれ」であるアプリが、アプリドロワーの他のすべてのアプリと同じに見えてしまう。これはMaterial Youが設計通りに動作しているだけだが、色がユーザーが混雑したアプリドロワーの中であなたのアプリを認識する手段である場合、本当の問題でもある。
解決策はダイナミックカラーをオフにすることではない — システム全体のテーマを選んだユーザーは、あるアプリだけが合わせようとしないことに気づく。解決策は調和させることだ。壁紙から導かれたニュートラルとインタラクションカラーは維持しつつ、ブランドの色相が静かに上書きされるのを許すのではなく、スキームに押し戻す。
dynamicColorSchemeが実際に与えるもの
API 31以上では、dynamicLightColorScheme(context)とdynamicDarkColorScheme(context)がandroid.R.color.system_accent1からsystem_accent3まで — システムが壁紙から抽出したトーナルパレット — を読み取り、完全なMaterial 3のColorSchemeを返す。OSの残りの部分と一貫して見えることが保証されたスキームが得られるので便利だ。同時に、あなたのアプリがどの色であるべきかについてはまったく知識がないスキームでもある。
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
else -> if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme
}
MaterialTheme(colorScheme = colorScheme, content = content)
}
ほとんどのチュートリアルはこのバージョンで止まるが、これはまさにHydrameの水滴アイコンとアプリ内のアクセントカラーが、テストした端末の半分で一致しなくなった原因となったコードだ。
置き換えるのではなく調和させる
androidx.core.graphics.ColorUtils — 追加の依存関係は不要で、core-ktxに同梱されている — にはblendHSLがあり、固定のブランドカラーをスキームのダイナミックプライマリに向かって少しずつ動かし、アイデンティティを失わずにネイティブに感じられるくらい近づけるために使える。Material自身のデザインガイダンスはこれを「ハーモナイゼーション(調和)」と呼んでいる。システムが所有していない色(エラーレッド、ブランドの色相、データビジュアライゼーションのシリーズ)を取り、衝突しないように最も近いダイナミックロールに部分的にブレンドすることだ。
fun harmonize(designColor: Color, dynamicColor: Color, fraction: Float = 0.2f): Color {
val blended = ColorUtils.blendHSL(
designColor.toArgb(),
dynamicColor.toArgb(),
fraction,
)
return Color(blended)
}
ブランドプライマリカラーが#3B82F6であるHydrameに適用すると:
val scheme = dynamicColorScheme(context)
val harmonizedAccent = harmonize(
designColor = Color(0xFF3B82F6),
dynamicColor = scheme.primary,
fraction = 0.15f,
)
fraction = 0.15fでは、アクセントはまだ紛れもなくHydrameの青として読めるが、壁紙が生成したどんなトーナルニュートラルとも争うのではなく、その隣に心地よく収まる。ハーモナイズした色は、ブランドアイデンティティを担う部分 — アプリアイコンのアプリ内でのエコー、水滴の進捗リング、オンボーディングのイラスト — にのみ使い、それ以外のすべて(サーフェス、テキスト、区切り線、ボタン)はシステム自身のダイナミックロールのままにしている。スキームのすべての色を調和させてしまうと、そもそもダイナミックカラーの目的が失われる。目標は、それ以外はシステムネイティブなパレットの中で生き残る、識別可能な単一のアクセントであって、ブランドによる完全な乗っ取りではない。
API 31未満でのフォールバック
上記のフォールバックロジックの3分の2は単一のwhen分岐だが、pre-Android 12のスキームがどう見えるかについては意図的であるべきだ。なぜならそれは劣化したダイナミックスキームではなく、それらの端末における唯一のスキームだからだ、それだけ。私はアプリごとに、ダイナミックスキームが何を生成したかを近似しようとするのではなく、各アプリのデザイントークンに既に存在する正確なブランド値(Granynの#22C55E、Mintlyの#F59E0B、Sublyの#6366F1)から直接構成した、lightColorScheme(primary = ..., secondary = ..., ...)による手作りのColorSchemeを保持している。これらの端末では、この記事のテーマであるブランドカラーの問題はそもそも存在しない — パレット全体をあなたが所有しているのだから — 得られもしないダイナミックな挙動をシミュレートすることに労力を割かないことだ。
ライト・ダークだけでなく、壁紙を横断してテストする
ダイナミックカラーは見落としやすいテストの次元を追加する。ライトテーマ、ダークテーマ、そして今や壁紙の色相の幅だ。デフォルトのPixel壁紙では正しく見える画面が、彩度の高い壁紙では読めないコントラストになることがある。system_accent1が色相とクロマを一緒に動かすためだ。テーマ化された画面を公開する前に、少なくとも寒色系の壁紙、暖色系の壁紙、そして低彩度のグレースケール壁紙に対してチェックしている — これは設定 → 壁紙とスタイル → 色を選択から設定でき、一致する画像を探すことなく特定の色相を強制できる。ハーモナイズしたアクセントのscheme.surfaceに対するコントラスト比がこれらのいずれかで4.5:1を下回る場合、上記の固定fractionはその色相範囲には強すぎるということであり、固定の定数ではなくコントラストチェックが必要だ。
fun harmonizeWithContrast(designColor: Color, scheme: ColorScheme, fraction: Float = 0.15f): Color {
val candidate = harmonize(designColor, scheme.primary, fraction)
return if (ColorUtils.calculateContrast(candidate.toArgb(), scheme.surface.toArgb()) >= 4.5) {
candidate
} else {
designColor // 低コントラストを出荷するくらいなら、手を加えていないブランドカラーに戻す
}
}
公開前に確認すべきこと
- ブランドカラーを担う各画面(アイコンのエコー、イラスト、グラフ)は、生のダイナミックプライマリでも手を加えていないブランドの16進値でもなく、ハーモナイズした値を使っている。
- pre-API-31のフォールバックスキームは、ダイナミックなものをハードコードした値でコピーしたものではなく、本物の手作業で調整された
ColorSchemeである。 - コントラストは、デフォルトだけでなく、少なくとも1つの彩度の高い壁紙と1つの低クロマの壁紙に対してチェックされている。
- ブランド以外のサーフェス(背景、区切り線、デフォルトボタン)はシステムのダイナミックロールのまま手を加えていない — すべてを調和させたい衝動には抵抗すること。
ダイナミックカラーは、アプリを汎用的な端末ではなく特定の端末に属しているように感じさせる、数少ないAndroidプラットフォームの機能の一つだ。維持する価値はある。ただし、それを維持するためにブランドカラーを犠牲にする価値はない。
// 関連記事
ジャーナルの他の記事
2026年のAndroid SplashScreen API: 白いフラッシュのないコールドスタート
Android SplashScreen APIの実践ガイド — テーマの設定、誰も読まないアニメーションアイコンのサイズ制限、まだ準備できていないデータのためのkeepOnScreenCondition。
Android の In-App Review API:うっとうしくならずに評価を頼む方法
Google の In-App Review API の実践ガイド。実際の仕組み、表示すべきタイミング、そしてありがちな『評価してください』ポップアップが Play ストアの評価を静かに損なっている理由。
2026年のAndroidアクセシビリティ:実際に使えるTalkBackとCompose semanticsのチェックリスト
2026年版、実践的なAndroidアクセシビリティチェックリスト——TalkBack、Compose semantics、タップ領域、そしてリリース前に毎回行うテストの手順。