Help us understand the problem. What is going on with this article?

Material Components時代のAndroid Themeを改めて理解する

AndroidのMaterial Componentsを利用する場合、

theme.xml
<style name="Theme.App" parent="Theme.MaterialComponents.Light">
    <!-- ... -->
</style>

上記のように、Theme.MaterialComponents.Lightなどを継承させてthemeを定義しているかと思います。
Dark Themeに対応する場合はTheme.MaterialComponents.DayNightなどを継承させているでしょう。

世はまさにDark Theme時代と言っても過言ではなくなってきています。
今一度、Androidにおけるtheme/styleとは何かを簡単に整理していこうという記事になります。

Material ComponentsのTheming

デフォルトのThemeはどうなっているのか

Android Studioからプロジェクトを作ると、colorPrimaryなどのThemeが既に定義されています。
いきなりカスタムするのではなく、まずは素っ裸のTheme.MaterialComponents.DayNightを見てみましょう。

theme.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar"/>
    <style name="ThemeStudy" parent="Base.ThemeStudy"/>
</resources>

このように定義し、わかりやすいようにいくつかコンポーネントを置いてみるとこのようになります。
(レイアウトファイルの方でもtheme/styleやattributesの定義は全てなくしています。)
default_theme.png

なんとなくcolorPrimaryが紫でcolorSecondaryが青緑かな〜というのがわかりますね。
はてさてこの色はどこに定義されているのでしょう?

⌘を押しながらクリック(あるいはショートカット)で定義元を探ってみます :eyes:

values.xml
<!-- 継承しているTheme.MaterialComponents.DayNight.NoActionBarからスタート -->
<style name="Theme.MaterialComponents.DayNight.NoActionBar" parent="Theme.MaterialComponents.Light.NoActionBar"/>
 ↓
<style name="Theme.MaterialComponents.Light.NoActionBar">
 ↓
<style name="Theme.MaterialComponents.Light" parent="Base.Theme.MaterialComponents.Light"/>
 ↓
<style name="Base.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light"/>
 ↓
<style name="Base.V14.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light.Bridge">
    <!-- ... -->

    <!-- Colors -->
    <item name="colorPrimary">@color/design_default_color_primary</item> <!-- !!!! -->
    <item name="colorPrimaryDark">@color/design_default_color_primary_dark</item>
    <item name="colorAccent">?attr/colorSecondary</item>

    <!-- ... -->
</style>

なかなか深いところにいましたね。colorPrimaryはどうやら#6200EEのようです。

これらのデフォルトカラーはmaterial.ioのColor Themingにも記載されています。
ここを見るとわかるのですが、Material ComponentではAppCompatとは違ったテーマ属性が使われます。

というわけで、次は色について簡単に説明していきます。

Material Componentsのカラー属性(テーマ属性)

マテリアルデザインではベースラインとして使える色の属性が定義されていて
基本的にはこれを用いて自分のアプリに合うようカスタマイズしていくことになります。
便宜上、テーマ属性における色に関する属性をカラー属性と呼びます。
edgetoedge.gif

ref: https://material.io/design/color/the-color-system.html#color-theme-creation

各色についてはUsing The Color Theming Systemに記載されているので割愛します。

Material Components for Androidでは、Widget.MaterialComponents.~のスタイルをデフォルトで使うよう定義されています。
またこのスタイルではテーマ属性を参照して構成されています。
そのため、このベーステーマを継承させ、オーバーライドすることで簡単に各コンポーネントの色を変えることができるのです。

カラー属性を変えてみる

わかりやすいように各カラー属性を雑に変えてみました。
change_colors.png

なんとなくどのカラー属性が、どのコンポーネントのスタイルから参照されているかが掴めるかと思います。
しかし、TextInputLayoutの背景色のように、これはどのカラー属性だ?というものがいくつか出てきますね。

前述したように、各コンポーネントはテーマ属性を参照してスタイルが定義されています。次はその定義を見てみましょう。

Material ComponentsのWidget Style

Theme.MaterialComponents.DayNightなどのテーマでは、テーマ属性で各コンポーネントのスタイルを指定しています。

例として、TextInputLayoutのスタイルを見てみましょう。themeの継承をたどっていくと、textInputStyleというテーマ属性が定義されています。

values.xml
<style name="Base.V14.Theme.MaterialComponents.Light.Bridge" parent="Platform.MaterialComponents.Light">
    <!-- ... -->
    <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox</item>
    <!-- ... -->
</style>

その中身を見ると、どうやら背景色は@color/mtrl_filled_background_colorに定義されているということがわかります。

values.xml
<style name="Widget.MaterialComponents.TextInputLayout.FilledBox" parent="Base.Widget.MaterialComponents.TextInputLayout">
    <!-- ... -->
    <item name="boxBackgroundColor">@color/mtrl_filled_background_color</item>
    <!-- ... -->
    <item name="boxStrokeColor">@color/mtrl_filled_stroke_color</item>  
    <!-- ... -->
</style>

mtrl_filled_background_colorでは、Viewの状態に応じて透明度は変わるものの、colorOnSurfaceを使っていることがわかりました!
先程の図で、うすく赤い色になっていたのはcolorOnSurfaceにアルファがかかっていたからなのです。
(黒みがかっているのは内部的にsurfaceレイヤーがあり、colorSurfaceの色が見えているからです。)

mtrl_filled_background_color.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_hovered="true"/>
  <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_focused="true"/>
  <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
  <item android:alpha="0.12" android:color="?attr/colorOnSurface"/>
</selector>

同様に、boxStrokeColor@color/mtrl_filled_stroke_colorに定義がされており、TextInputLayoutにフォーカスがあたったときに色が変わる謎も解けます:thumbsup:

mtrl_filled_stroke_color.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:color="?attr/colorPrimary" android:state_focused="true"/>
  <!-- 4% overlay over 42% colorOnSurface -->
  <item android:alpha="0.46" android:color="?attr/colorOnSurface" android:state_hovered="true"/>
  <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
  <item android:alpha="0.42" android:color="?attr/colorOnSurface"/>
</selector>

このように、テーマ属性を適切に定義することで、マテリアルコンポーネントではアプリ全体に統一感のあるデザインを提供しています。

テーマ属性の参照

「テーマ属性を参照しているので」 と説明もなく言っていましたが、これはどうやるのでしょう?
私自身、今回ちゃんとThemeと向き合うまで、なにげなく使っていたけど知りませんでした。

すでに説明したコードにも出てきていますが、?attr/colorPrimaryと書くことで、現在のテーマ属性を参照できます。
この場合は、現在のテーマにおけるcolorPrimaryを参照しています。colorタグで定義したものではないことに注意です。
reference.png

現在のというのが肝です。

テーマ属性の参照を使ってコンポーネントの色を変える

例えば、ユーザのステータスに応じてcolorPrimaryを変えたいと言った場合を考えてみましょう。

単純に思いつくのは、プログラムで動的に色をコンポーネントに指定してあるげる方法です。
毎回Activity/Fragmentで処理を書くのはつらいのでBindingAdapterを作ってよしなに色を変えたりもできそう。

こういった場合、コンポーネントごとに色を変える処理を書くのは難儀なものです。

この場合、themeをまるまる変えてしまうとよいでしょう。
colorPrimary/colorSecondaryなど、必要な分だけ定義を上書きましょう。

theme.xml
<resources>

    <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorSecondary">@color/colorSecondary</item>
    </style>

    <style name="ThemeStudy" parent="Base.ThemeStudy" />

    <style name="ThemeStudy.Premium">
        <item name="colorPrimary">#f44336</item>
        <item name="colorSecondary">#ffc107</item>
    </style>

    <style name="ThemeStudy.Etc">
        <item name="colorPrimary">#36f443</item>
        <item name="colorSecondary">#07ffc1</item>
    </style>
</resources>

あとはthemeを条件に応じて指定してあげるだけです。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 条件に応じてテーマを設定
        setTheme(if (isPremium()) R.style.ThemeStudy_Premium else R.style.ThemeStudy)
        ...
    }
    ...
}

画面単位や、部分的な適用の場合はレイアウトで指定してあげると良いでしょう。
これに関しては公式のMaterial DesignサンプルのOwlが上手に使い分けているので参考になります。

ThemeとStyleの違い

Styleは1つのViewのみに適用され、Themeは適用したViewの子Viewにまで適用されるという違いがあります。
また、Themeはコンテキストに紐づきます。ActivityでThemeを設定していれば画面全体に適用されます。
設定していなければApplicationコンテキストで設定されたThemeが適用されます。
theme_style.png

ref: https://speakerdeck.com/nickbutcher/developing-themes-with-style-7d0571e3-6008-4ec5-99c6-aa5669520124?slide=29

ThemeOverlayとは

ThemeOverlay自体はただの命名規則です。Style/Themeの継承を利用し、この命名規則も納得の動きをします。

<style>タグではparentを指定しThemeを継承、そして<item>タグでテーマ属性などを定義し自分のThemeでオーバーライドすることが多いかと思います。
例えば、colorPrimaryを定義すると、継承元のcolorPrimaryを上書きしますね。

theme.xml
<style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorSecondary">@color/colorSecondary</item>
</style>

このとき、parentを指定せずにcolorPrimaryなどのテーマ属性を定義すると、その属性のみが上書きされ、定義されていないテーマ属性はそのViewにすでに適用されているThemeのものが使われるのです。(親のViewのThemeやApplication/Activity/Fragmentなどで適用されているTheme)
theme_overlay.png

この例では、便宜上Customというスタイルを定義していますが、命名規則を守り、ThemeOverlay.AppName.Redのように定義すると管理がしやすいでしょう。

TextInputLayoutはViewの属性としてboxBackgroundColorboxStrokeColorを持っているので、それを用いて色を変えられるには変えられます。
しかし、Material Componentsではテーマ属性を用いて丁寧にスタイルが定義されています。都度都度、ViewごとにViewの属性を設定していると管理が煩雑になっていまします。
ThemeOverlayの考え方を使って、ViewあるいはViewGroupに、colorPrimaryなどのテーマ属性を適切に設定し統一感を守っていきましょう。

ThemeOverlayの使い所

なぜThemeOverlayを使うかというと、必要なテーマ属性だけ上書きできるという利点が大きいのではないかと思います。

公式サンプルのOwlを見てみると非常にわかりやすいです。
Base.OwlではtextAppearanceHeadline1bottomNavigationStyleといったアプリ全体として共通のテーマ属性を定義しています。
そしてcolorPrimaryなどの色に関するテーマ属性は、Owl.Yellow/Owl.Blueそれぞれで定義されています。

theme.xml
<!-- ... -->
<style name="Owl" parent="@style/Base.Owl"/>

<style name="Owl.Yellow">
    <item name="colorPrimary">@color/owl_yellow_500</item>
    <item name="colorPrimaryVariant">@color/owl_yellow_400</item>
    <item name="colorSecondary">@color/owl_blue_700</item>
    <item name="colorSecondaryVariant">@color/owl_blue_800</item>
</style>

<style name="Owl.Blue">
    <item name="colorPrimary">@color/owl_blue_700</item>
    <item name="colorPrimaryVariant">@color/owl_blue_800</item>
    <item name="colorSecondary">@color/owl_yellow_500</item>
    <item name="colorSecondaryVariant">@color/owl_yellow_400</item>
</style>
<!-- ... -->

こうすることで変更にも強くなり見通しも良いですね。

(OwlではThemeOverlay.MaterialComponents.Lightを継承したThemeOverlay.Owl.Blueがありますが、Theme.MaterialComponents.Lightとの差分までは面倒なので追えてないです)

よく見るThemeOverlayでいうと、プロジェクトを作るとAppBarLayoutにデフォルトで定義されているThemeOverlay.Material.Dark.ActionBar(あるいはThemeOverlay.Material.Light.ActionBar)があります。
おなじみChris BanesさんのTheme vs Styleによると、colorControlNormalandroid:textColorPrimaryにしているだけ、とあります。(実際にコードを追うとcolorControlNormal?android:attr/textColorPrimaryと定義されています。)
これによって、AppBarLayoutのタイトルテキストと、メニューなどのアイコンの色を揃えているというわけです。(メニューなどの色はcolorControlNormalによって決まる)

まとめ

  • Material Componentsはテーマ属性によってThemingされている
  • ?atrr/を使うことでテーマ属性を参照できる
    • これをうまく使っていくの大事そう
  • Styleは1つのViewに適用され、Themeは子Viewにも適用される
  • テーマ属性を適切に設定することで統一感があり管理も煩雑にならない
  • ThemeとStyleを見ればView属性のデフォルトがどのテーマ属性を参照しているかわかる

それこそTheme vs Styleや、Dark Themeについても取り上げたかったんですが長くなるのでいつかの機会に。
カラー命名の良い方法などはAndroid Dev Summit動画または こちらのQiita記事を見ると良いです!
(本記事の中では<color name="colorPrimary">#6200EE</color>のように定義してますが、あまりよくないです。)

間違ったところや疑問点などがアレば歓迎しますぜひ教えて下さい!
参考にしたリンクなども書いておくのでThemeをみんなで攻略しましょう!

Links

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした