DMM.comの、一番深くておもしろいトコロ。

DarkTheme対応のリソース設計

DarkTheme対応のリソース設計

  • このエントリーをはてなブックマークに追加

目次

はじめに

はじめまして、CTO室事業支援チームの松本(@keijumt)です。

DarkTheme対応にはリソース設計とリソース利用を適切に行うことが重要です。

リソースの設計や利用を疎かにすると、特定の色を変更したい場合などに工数が多く必要になったり修正漏れなどが発生したりする可能性があります。

今回は、そういったことを防ぐためにはどのようなリソース設計にするべきか、また、実際にリソースを利用する時に気をつけることなどをご紹介します。

DarkTheme対応について

方針

DarkTheme対応を行うにあたり、今回は Material Component を利用します。バージョンは現時点で1.3.0です。

大まかな対応としては values と values-night リソースディレクトリにthemes.xmlを作成し、それぞれにMDCから提供されているDayNightテーマを継承したテーマを作成します。

実装の流れとしては以下になります。

  1. valuesvalues-night リソースディレクトリを用意する
  2. それぞれにリソースディレクトリにthemes.xmlを作成する
  3. values/themes.xmlTheme.MaterialComponents.DayNight を継承したベーステーマを定義する
  4. values/themes.xml にベーステーマを継承したDayModeのテーマを作成し色の設定を行う
  5. values-night/themes.xml にベーステーマを継承したNightModeのテーマを作成し色の設定を行う
  6. 4,5で作成したDay,Nightのテーマを継承し、同じ命名でそれぞれの themes.xml に定義する
  7. AndroidManifestのtheme属性に6で定義したテーマを設定する
  8. Viewからのカラーリソースへのアクセスは基本的にtheme属性で行う

colors.xmlvaluesvalues-night に用意して対応を行う方法もありますが今回は利用しません。

理由としては以下があります。

  1. colors.xml はアプリ内で登場する全ての色を定義したい
  2. colors.xml でDarkTheme対応を行おうとすると、 themes で扱うような名前を定義する必要があり二重管理となる
  3. MaterialThemingを利用すると themes.xml を定義する手法になる

重要なこととして、基本的にViewからのカラーリソースへのアクセスは Theme 経由で行います。

今回の手法は MaterialTheming を利用しているため、Viewからのカラーリソース参照で colors.xml で定義したものにアクセスするとDarkTheme対応ができません。

また 「基本的に」 としているのは後述するTipsでCustomThemeAttributeを作らない場合のDarkTheme対応では Theme 経由でカラーリソースにアクセスしない場合があるためです。

リソース設計

次にリソース設計です。

colors.xml

プロジェクトで利用する色を values/colors.xml に定義します。

この時の命名としては colorPrimarybackgroundColor といった抽象的な名前ではなく 純粋な色の名前 を定義します。

blue_500orange_200 といった名前などが適切です。

理由としては、DayMode, NightModeを考慮したThemeから参照されるので抽象的な名前ではなく、色本来の具体的な名前のほうが良いためです。

AndroidStudio 4.1.0 でプロジェクトテンプレートがDarkTheme対応されましたが、 colors.xml には色本来の名前が定義されています。

colors.xml の段階ではDarkTheme対応は行わないので、Day,Nightで色が切り替わってほしいViewからは直接参照しないようにします。

themes.xml

themes.xmlvalues/themes.xmlvalues-night/themes.xml の2つを作成しDayModeとNightModeを考慮した色を定義します。

Themeは以下のものを作成します。

  • 具体的な色定義を含まないベースとなるTheme
  • DayModeを考慮したTheme
  • NightModeを考慮したTheme
  • Day, Nightを考慮したThemeで共通の名前のTheme

Themeは継承関係を持つことが可能で、親Themeで定義されているAttributeを受け継ぐことや子Themeで特定のAttributeを上書きすることも可能です。

この関係を利用することでそれぞれのThemeで必要最低限のリソース定義にすることを意識します。

Themeの設計は以下になります。

f:id:dmminside:20210224161524p:plain

Base.Theme.AppNameTheme.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経由で行います。

と記載しましたが大規模なアプリになると colorPrimarycolorSecondary などデフォルトで用意されているThemeAttributeだけでは対応できないケースがほとんどです。

colorBackgroundSecondary のようなアプリ全体で汎用的に使われるものなどの場合はCustomThemeAttributeとして定義し、特定の画面のみにある特定の色 などAttributeに定義するメリットが少ないものは定義を避けたいです。

こういったケースには ColorStateList を利用するとスコープを最小限にとどめて必要最低限の対応ができます。

res/color/color_favorite.xmlres/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対応を見据えたリソース設計をご紹介しました。

リソース設計次第で機能開発や機能修正などのやりやすさが変わってくるため、プロジェクト初期にしっかり行うことがおすすめです。