はじめに
はじめまして、CTO室事業支援チームの松本(@keijumt)です。
DarkTheme対応にはリソース設計とリソース利用を適切に行うことが重要です。
リソースの設計や利用を疎かにすると、特定の色を変更したい場合などに工数が多く必要になったり修正漏れなどが発生したりする可能性があります。
今回は、そういったことを防ぐためにはどのようなリソース設計にするべきか、また、実際にリソースを利用する時に気をつけることなどをご紹介します。
DarkTheme対応について
方針
DarkTheme対応を行うにあたり、今回は Material Component を利用します。バージョンは現時点で1.3.0です。
大まかな対応としては values
と values-night
リソースディレクトリにthemes.xmlを作成し、それぞれにMDCから提供されているDayNightテーマを継承したテーマを作成します。
実装の流れとしては以下になります。
values
とvalues-night
リソースディレクトリを用意する- それぞれにリソースディレクトリにthemes.xmlを作成する
values/themes.xml
にTheme.MaterialComponents.DayNight
を継承したベーステーマを定義するvalues/themes.xml
にベーステーマを継承したDayModeのテーマを作成し色の設定を行うvalues-night/themes.xml
にベーステーマを継承したNightModeのテーマを作成し色の設定を行う- 4,5で作成したDay,Nightのテーマを継承し、同じ命名でそれぞれの
themes.xml
に定義する - AndroidManifestのtheme属性に6で定義したテーマを設定する
- Viewからのカラーリソースへのアクセスは基本的にtheme属性で行う
colors.xml
を values
と values-night
に用意して対応を行う方法もありますが今回は利用しません。
理由としては以下があります。
colors.xml
はアプリ内で登場する全ての色を定義したいcolors.xml
でDarkTheme対応を行おうとすると、themes
で扱うような名前を定義する必要があり二重管理となる- MaterialThemingを利用すると
themes.xml
を定義する手法になる
重要なこととして、基本的にViewからのカラーリソースへのアクセスは Theme
経由で行います。
今回の手法は MaterialTheming
を利用しているため、Viewからのカラーリソース参照で colors.xml
で定義したものにアクセスするとDarkTheme対応ができません。
また 「基本的に」 としているのは後述するTipsでCustomThemeAttributeを作らない場合のDarkTheme対応では Theme
経由でカラーリソースにアクセスしない場合があるためです。
リソース設計
次にリソース設計です。
colors.xml
プロジェクトで利用する色を values/colors.xml
に定義します。
この時の命名としては colorPrimary
や backgroundColor
といった抽象的な名前ではなく 純粋な色の名前
を定義します。
blue_500
や orange_200
といった名前などが適切です。
理由としては、DayMode, NightModeを考慮したThemeから参照されるので抽象的な名前ではなく、色本来の具体的な名前のほうが良いためです。
AndroidStudio 4.1.0 でプロジェクトテンプレートがDarkTheme対応されましたが、 colors.xml
には色本来の名前が定義されています。
colors.xml
の段階ではDarkTheme対応は行わないので、Day,Nightで色が切り替わってほしいViewからは直接参照しないようにします。
themes.xml
themes.xml
は values/themes.xml
と values-night/themes.xml
の2つを作成しDayModeとNightModeを考慮した色を定義します。
Themeは以下のものを作成します。
- 具体的な色定義を含まないベースとなるTheme
- DayModeを考慮したTheme
- NightModeを考慮したTheme
- Day, Nightを考慮したThemeで共通の名前のTheme
Themeは継承関係を持つことが可能で、親Themeで定義されているAttributeを受け継ぐことや子Themeで特定のAttributeを上書きすることも可能です。
この関係を利用することでそれぞれのThemeで必要最低限のリソース定義にすることを意識します。
Themeの設計は以下になります。
Base.Theme.AppName
は Theme.MaterialComponents.DayNight.*
を継承したThemeです。
これには具体的な色以外の定義を行います。
TextAppearanceやShapeAppearanceなどを定義します。
Theme.AppName
はベースThemeを継承し、DayModeを考慮したThemeです。colorPrimaryやcolorBackgroundなど具体的な色定義を行います。
Theme.AppName.Night
: ベースThemeを継承し、NightModeを考慮したThemeです。 DayModeを考慮したTheme同様にNightModeを考慮し、具体的な色定義を行います。
Theme.AppName.DayNight
: DayMode, NightModeを考慮したThemeを共通の名前にしたThemeです。AndroidManifestのthemeに設定します。DayMode, NightModeが切り替わった時にそれぞれのThemeが参照されるようにするために共通の名前にします。
他の設計として Theme.AppName.Night
定義で Base.Theme.AppName
を継承せずに Theme.AppName
を継承し、具体的な色定義をDayMode時との差分のみ定義するという方法もあります。
しかしこの設計だと Theme.AppName
を更新した際にNightMode時にも反映されるため、意図しない挙動になる可能性があることから採用していません。
res/values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 具体的な色定義を含まないベースTheme -->
<style name="Base.Theme.AppName" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="textAppearanceHeadline1">...</item>
<item name="textAppearanceHeadline2">...</item>
<item name="shapeAppearanceSmallComponent">...</item>
<item name="shapeAppearanceMediumComponent">...</item>
</style>
<!-- DayModeのTheme -->
<style name="Theme.AppName" parent="Base.Theme.AppName">
<item name="colorPrimary">@color/blue_500</item>
<item name="colorSecondary">@color/orange_400</item>
</style>
<!-- DayModeのThemeと同じ名前のTheme -->
<style name="Theme.AppName.DayNight" parent="Theme.AppName" />
</resources>
res/values-night/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- NightModeのTheme -->
<style name="Theme.AppName.Night" parent="Base.Theme.AppName">
<item name="colorPrimary">@color/blue_400</item>
<item name="colorSecondary">@color/orange_300</item>
</style>
<!-- NightModeのThemeと同じ名前のTheme -->
<style name="Theme.AppName.DayNight" parent="Theme.AppName.Night" />
</resources>
AndroidManifest.xml
<application android:theme="@style/Theme.AppName.DayNight" />
Tips
ColorStateList
ColorStateListは便利な使い方が可能です。
色定義でalpha値を扱う際に以下のようにcolors.xmlに定義するとalpha値が16進数で分かりづらいことや、ベースとなる色が変わった際に全てのalpha値が反映されたcolorを変更する必要があります。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#212121</color>
<color name="black_alpha_80">#36212121</color>
<color name="black_alpha_50">#80212121</color>
<color name="black_alpha_30">#4D212121</color>
</resources>
ColorStateListからは定義した色を参照可能なので上記で定義したblackを参照し、alpha値を設定するとベースとなるblackの色が変更された際に修正部分を必要最低限にできます。
res/color/black_alpha_80
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/black" android:alpha="0.8" />
</selector>
また、ColorStateListのcolorの参照先としてはThemeAttributeを参照可能です。
TextLegibilityの考慮時などに使うと便利です。
res/color/color_on_primary_emphasis_high_type.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:alpha="0.87" />
</selector>
CustomThemeAttributeを作らない時の対応
基本的にViewからのカラーリソースへのアクセスはTheme経由で行います。
と記載しましたが大規模なアプリになると colorPrimary
や colorSecondary
などデフォルトで用意されているThemeAttributeだけでは対応できないケースがほとんどです。
colorBackgroundSecondary
のようなアプリ全体で汎用的に使われるものなどの場合はCustomThemeAttributeとして定義し、特定の画面のみにある特定の色
などAttributeに定義するメリットが少ないものは定義を避けたいです。
こういったケースには ColorStateList
を利用するとスコープを最小限にとどめて必要最低限の対応ができます。
res/color/color_favorite.xml
と res/color/color_favorite.xml
を作成し、Day, Nightを考慮した色を定義します。
<!-- res/color/favorite.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/pink_500" />
</selector>
<!-- res/color-night/favorite.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/pink_300" />
</selector>
Viewからは @color/favorite
を参照することで、CustomThemeAttributeを作成せずにDarkTheme対応を行ったうえで実装が可能です。
注意点としては ColorStateList
はAPI29未満でbackgroundとして設定するとクラッシュします。
この問題の回避方法としては、 backgroundTint
を利用することにより全てのAPIバージョンで正常に動作するようになります。
res/drawable/rectangle.xml
<shape>
<solid android:color="#FF00FF" />
</shape>
<View
...
android:background="@color/rectangle"
android:backgroundTint="@color/favorite" />
rectangle.xml
には万が一設定が間違えていた時にすぐに気付けるようにマゼンタピンクなど派手な色を設定することをおすすめします。
リソースファイルの肥大化
themes.xml
に全てのthemeAttributeを定義するとかなり肥大化します。リソースは適宜分割して定義することがおすすめです。
ファイル名 | 概要 |
---|---|
themes.xml | 色のThemeAttributeを定義+その他xmlファイルを参照しAttributeを設定 |
styles.xml | Viewの属性を定義 |
types.xml | TextAppearanceを定義 |
shapes.xml | ShapeAppearanceを定義 |
motions.xml | アニメーションの時間などを定義 |
attrs.xml | カスタムAttributeを定義 |
おわりに
今回はDarkTheme対応を見据えたリソース設計をご紹介しました。
リソース設計次第で機能開発や機能修正などのやりやすさが変わってくるため、プロジェクト初期にしっかり行うことがおすすめです。