AndroidのMaterial Componentsを利用する場合、
<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
を見てみましょう。
<?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の定義は全てなくしています。)
なんとなくcolorPrimary
が紫でcolorSecondary
が青緑かな〜というのがわかりますね。
はてさてこの色はどこに定義されているのでしょう?
⌘を押しながらクリック(あるいはショートカット)で定義元を探ってみます
<!-- 継承している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のカラー属性(テーマ属性)
マテリアルデザインではベースラインとして使える色の属性が定義されていて
基本的にはこれを用いて自分のアプリに合うようカスタマイズしていくことになります。
便宜上、テーマ属性における色に関する属性をカラー属性と呼びます。
ref: https://material.io/design/color/the-color-system.html#color-theme-creation
各色についてはUsing The Color Theming Systemに記載されているので割愛します。
Material Components for Androidでは、Widget.MaterialComponents.~
のスタイルをデフォルトで使うよう定義されています。
またこのスタイルではテーマ属性を参照して構成されています。
そのため、このベーステーマを継承させ、オーバーライドすることで簡単に各コンポーネントの色を変えることができるのです。
カラー属性を変えてみる
なんとなくどのカラー属性が、どのコンポーネントのスタイルから参照されているかが掴めるかと思います。
しかし、TextInputLayout
の背景色のように、これはどのカラー属性だ?というものがいくつか出てきますね。
前述したように、各コンポーネントはテーマ属性を参照してスタイルが定義されています。次はその定義を見てみましょう。
Material ComponentsのWidget Style
Theme.MaterialComponents.DayNight
などのテーマでは、テーマ属性で各コンポーネントのスタイルを指定しています。
例として、TextInputLayout
のスタイルを見てみましょう。themeの継承をたどっていくと、textInputStyle
というテーマ属性が定義されています。
<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
に定義されているということがわかります。
<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
の色が見えているからです。)
<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
にフォーカスがあたったときに色が変わる謎も解けます
<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
タグで定義したものではないことに注意です。
現在のというのが肝です。
テーマ属性の参照を使ってコンポーネントの色を変える
例えば、ユーザのステータスに応じてcolorPrimary
を変えたいと言った場合を考えてみましょう。
単純に思いつくのは、プログラムで動的に色をコンポーネントに指定してあるげる方法です。
毎回Activity/Fragmentで処理を書くのはつらいのでBindingAdapter
を作ってよしなに色を変えたりもできそう。
こういった場合、コンポーネントごとに色を変える処理を書くのは難儀なものです。
この場合、themeをまるまる変えてしまうとよいでしょう。
colorPrimary
/colorSecondary
など、必要な分だけ定義を上書きましょう。
<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を条件に応じて指定してあげるだけです。
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が適用されます。
ThemeOverlayとは
ThemeOverlay自体はただの命名規則です。Style/Themeの継承を利用し、この命名規則も納得の動きをします。
<style>
タグではparent
を指定しThemeを継承、そして<item>
タグでテーマ属性などを定義し自分のThemeでオーバーライドすることが多いかと思います。
例えば、colorPrimary
を定義すると、継承元のcolorPrimary
を上書きしますね。
<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)
この例では、便宜上Custom
というスタイルを定義していますが、命名規則を守り、ThemeOverlay.AppName.Red
のように定義すると管理がしやすいでしょう。
TextInputLayout
はViewの属性としてboxBackgroundColor
やboxStrokeColor
を持っているので、それを用いて色を変えられるには変えられます。
しかし、Material Componentsではテーマ属性を用いて丁寧にスタイルが定義されています。都度都度、ViewごとにViewの属性を設定していると管理が煩雑になっていまします。
ThemeOverlayの考え方を使って、ViewあるいはViewGroupに、colorPrimary
などのテーマ属性を適切に設定し統一感を守っていきましょう。
ThemeOverlayの使い所
なぜThemeOverlayを使うかというと、必要なテーマ属性だけ上書きできるという利点が大きいのではないかと思います。
公式サンプルのOwlを見てみると非常にわかりやすいです。
Base.Owl
ではtextAppearanceHeadline1
やbottomNavigationStyle
といったアプリ全体として共通のテーマ属性を定義しています。
そしてcolorPrimary
などの色に関するテーマ属性は、Owl.Yellow
/Owl.Blue
それぞれで定義されています。
<!-- ... -->
<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によると、colorControlNormal
をandroid: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
- Developing Themes with Style (Android Dev Summit '19)
- Developing Themes with Style
- Androidのthemeにつけられた命名のメモ
- Theme vs Style
- Android Dev Summit '19で紹介されたサンプルから学ぶ、AndroidのTheme、Style、Colorの設定方法
- DroidKaigi2019で発表したAndroid Themeの話のスライド補足
- Color theme creation
- Color Theming
- Dark Theme
- Setting up a Material Components theme for Android
- material-components-android | MaterialColors.java