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の定義は全てなくしています。）
<img width="256" alt="default_theme.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/a5aa9cf2-542e-9802-bfa4-97cd8b307d86.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](https://material.io/develop/android/theming/color/)にも記載されています。
ここを見るとわかるのですが、Material ComponentではAppCompatとは違ったテーマ属性が使われます。

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

## Material Componentsのカラー属性（テーマ属性）

マテリアルデザインではベースラインとして使える色の属性が定義されていて
基本的にはこれを用いて自分のアプリに合うようカスタマイズしていくことになります。
便宜上、テーマ属性における色に関する属性をカラー属性と呼びます。
<img width="512" alt="edgetoedge.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/b57baf1e-ad4e-8cbb-fd01-b1b1b6df6abc.png">

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

各色については[Using The Color Theming System](https://material.io/develop/android/theming/color/#using-the-color-theming-system)に記載されているので割愛します。

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

### カラー属性を変えてみる

わかりやすいように各カラー属性を雑に変えてみました。
![change_colors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/4bd7409e-ea1f-a6cb-3b16-b050dd36a3da.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`にアルファがかかっていたからなのです。
（黒みがかっているのは[内部的に](https://github.com/material-components/material-components-android/blob/5f4a1394e52bdb7b40518c18fc0f931a179d49bc/lib/java/com/google/android/material/textfield/TextInputLayout.java#L2264-L2277)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`にフォーカスがあたったときに色が変わる謎も解けます:+1:

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/8e9e01a6-a3cf-36f6-53d9-25641784b8af.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](https://github.com/material-components/material-components-android-examples/tree/develop/Owl)が上手に使い分けているので参考になります。

## ThemeとStyleの違い

Styleは1つのViewのみに適用され、Themeは適用したViewの子Viewにまで適用されるという違いがあります。
また、Themeはコンテキストに紐づきます。ActivityでThemeを設定していれば画面全体に適用されます。
設定していなければApplicationコンテキストで設定されたThemeが適用されます。
![theme_style.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/0a4471e7-0e3f-d71d-b18d-98ea5bdb1852.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128802/46bceb26-6a32-64ce-46ba-dad711af05d8.png)


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

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

### ThemeOverlayの使い所

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

公式サンプルの[Owl](https://github.com/material-components/material-components-android-examples/tree/develop/Owl)を見てみると非常にわかりやすいです。
`Base.Owl`では`textAppearanceHeadline1`や`bottomNavigationStyle`といったアプリ全体として共通のテーマ属性を定義しています。
そして`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](https://chris.banes.dev/2014/11/12/theme-vs-style/#themeoverlay--actionbar)によると、`colorControlNormal`を`android:textColorPrimary`にしているだけ、とあります。（実際にコードを追うと`colorControlNormal`が`?android:attr/textColorPrimary`と定義されています。）
これによって、`AppBarLayout`のタイトルテキストと、メニューなどのアイコンの色を揃えているというわけです。（メニューなどの色は`colorControlNormal`によって決まる）

# まとめ

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

それこそTheme vs Styleや、Dark Themeについても取り上げたかったんですが長くなるのでいつかの機会に。
カラー命名の良い方法などは[Android Dev Summit動画](https://www.youtube.com/watch?v=Owkf8DhAOSo)または [こちらのQiita記事](https://qiita.com/takahirom/items/f245b71c58245729d9d7)を見ると良いです！
（本記事の中では`<color name="colorPrimary">#6200EE</color>`のように定義してますが、あまりよくないです。）

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

# Links

- [Developing Themes with Style (Android Dev Summit '19)](https://www.youtube.com/watch?v=Owkf8DhAOSo)
- [Developing Themes with Style](https://speakerdeck.com/nickbutcher/developing-themes-with-style-7d0571e3-6008-4ec5-99c6-aa5669520124)
- [Androidのthemeにつけられた命名のメモ](https://qiita.com/konifar/items/524729300c437ea3719f)
- [Theme vs Style](https://chris.banes.dev/2014/11/12/theme-vs-style/#themeoverlay--actionbar)
- [Android Dev Summit '19で紹介されたサンプルから学ぶ、AndroidのTheme、Style、Colorの設定方法](https://qiita.com/takahirom/items/f245b71c58245729d9d7)
- [DroidKaigi2019で発表したAndroid Themeの話のスライド補足](http://konifar.hatenablog.com/entry/2019/02/11/122825)
- [Color theme creation ](https://material.io/design/color/#color-theme-creation)
- [Color Theming](https://material.io/develop/android/theming/color/)
- [Dark Theme](https://material.io/develop/android/theming/dark/)
- [Setting up a Material Components theme for Android](https://medium.com/over-engineering/setting-up-a-material-components-theme-for-android-fbf7774da739)
- [material-components-android | MaterialColors.java](https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/color/MaterialColors.java)