Android Styling をイチからやり直す


この記事なに?

Android Styling とは Android アプリの見た目を決める一連の仕組みです。Android アプリを作ったことがある人なら必ず触れている基本ですが、ちゃんと理解できていますか? デフォルトで生成された themes.xml を雰囲気で書いているだけではないですか?
これは僕が Android Styling を使いこなすために今一度イチから勉強し直したまとめです。
間違っていたら教えてください😋

ある特定の View の見た目を変える

Style や Theme の話に入る前に、View の見た目を変える方法を振り返ります。
Android の View の見た目を操作する最も基本の方法は View attribute です。View attribute は View に外部から設定する値です。xml で設定することが多いです。
例えば TextView の文字色は以下のように変えられます。

<TextView
    android:text="サンプル"
    android:textColor="#EC407A"
    ... />

android:textColor が1つの View attribute です。TextView は他にもたくさんの View attribute を定義しており、開発者は必要に応じてこれらの View attribute に値を設定します。xml で指定した View attribute は TextView のコンストラクタ引数に AttributeSet として渡され、textColor として設定されます。
もっと深く View attribute について知りたい場合は以下のドキュメントが参考になります。

Creating a View Class  |  Android Developers

Style と Theme

1つ1つの View に attribute を設定していてはリソースの適用が大変です。View attribute の設定値をUIコンポーネントとして意味のある単位でグループ化し、同じような View に再利用するほうが効率的です。
ここで登場するのが StyleTheme です。
Style と Theme の違いは Nick のこの記事がとてもわかり易いです。

Android styling: themes vs styles | by Nick Butcher | Android Developers | Medium

Style は特定の View の View attribute を一括設定するもの

Nick は StyleMap<View attribute, Resource> と表現しています。
つまり、設定したい View attribute と設定すべきリソースの値の組が集合したものです。
例えば以下の例は Material Button の Style を拡張して、カスタマイズしたい View attribute を設定しています。

<style name="Widget.MyApp.Button" parent="Widget.MaterialComponents.Button">
    <item name="android:textColor">#EF5350</item>
    <item name="android:padding">8dp</item>
</style>

Style は特定の View に適用する

Style は複数の View に対して同じ View attribute を設定する用途で利用します。
Style を View に適用するときは style attribute を使います。

<Button
    style="@style/Widget.MyApp.Button"
    android:text="ボタン"
    ... />

親 View や 子 View には Style は伝搬しません。
影響範囲は sytle attribute を設定した View のみです。

Theme はリソースの参照を提供するもの

他方、Nick は ThemeMap<Theme attribute, Resource> と表現しています。 Theme attribute とは、アプリ内で定義したリソース (Style, Color, TextAppearance, etc...) を別のリソースから参照できるように定義する値です。
Theme attribute はリソースの使い道を名前にしたセマンティック名で定義します。

Theme は以下のように定義します。

<style name="MyAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- アプリのメインカラー -->
    <item name="colorPrimary">@color/red</item>

    <!-- アプリで利用する Material Button の Style -->
    <item name="materialButtonStyle">@style/Widget.MyApp.Button</item> 
</style>

Theme attribute で設定したリソースを参照する場合は ?attr/[themeAttributeName] で参照できます。

<Button
    android:textAppearance="?attr/textAppearanceButton"
    ... />

利用する Theme attribute は <style> タグの中で好き勝手定義して良いものではなく、attrs.xml で定義しないといけません。
例えば、colorPrimary は AppCompat の attrs.xml で定義されています。自分のプロジェクトで attrs.xml を作ってオリジナルの Theme attribute を定義することもできます。

<attr name="colorPrimary" format="color"/>

Theme は Context に適用する

Theme は Context (Application, Activity, View, etc...) に適用します。
影響範囲は android:theme を設定したノードから下のノード全てです。
例えば、以下のような View のツリー構造があった場合、MyTheme の適用範囲は child1 の View と child1 を親に持つ全ての子 View です。

<LinearLayout
    android:id="+id/root" 
    ... >
    <LinearLayout
        android:id="+id/child1"
        android:theme="@style/MyTheme">
        <!-- child1 以下は MyTheme の attribute が使える -->
        <Button
            .../>
    </LinearLayout>

    <!-- child2 は MyTheme の attribute は使えない -->
    <Button
        android:id="+id/child2"
        ... />
</LinearLayout>

ここでいう Theme を適用する とは、Theme に設定された Theme attribute を利用できる状態にするという意味になります。

View attribute の設定はできるだけ Theme attribute を経由する

layout.xml に書いた View や自分で定義した Style で設定する View attribute の値は、できるだけ Theme attribute を経由したほうが良いです。なぜなら、ブランディング変更やダークテーマ対応など、アプリ全体の見た目を変更したい場合、Theme attribute を使ってリソースを間接参照していれば Theme を編集するだけで済みます。

View が直接リソースを参照していると、個々の layout.xml で設定している View attribute の設定値をすべて変更していかないといけなくなります。これは View の増加に伴って作業量が爆発的に増えていき、メンテが大変な layout が出来上がります。

Style や Theme は既存のものをカスタマイズしていく

Android には様々な View attribute, Theme attribute があるため、自分で Style や Theme をイチから作り上げるのは骨が折れます。そのため Android Styling システムは、ビルトインで提供されている Style や Theme を継承し、開発者が必要な attribute だけ変更できるように作られています。

以下の例だと、AppCompat で定義された Theme である Theme.AppCompat.Light.DarkActionBar を継承し、カスタマイズしたい Theme attribute だけ個別設定しています。

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary"> ... </item>
    <item name="colorAccent"> ... </item>
</style>

colorPrimary, colorAccent 以外の Theme attribute は Theme.AppCompat.Light.DarkActionBar から引き継いでいます。
他にどんな Theme attribute があるのか気になる人はここで確認できます。めちゃくちゃあります。

View attribute に設定されている値を確認する方法

Style や Theme を継承して attribute を引き継いでいると、具体的にどの値が View attribute に設定されるかわからなくなります。
個人的には、特に xml で明示的に設定していないデフォルト値が一体どの Style/Theme 由来なのかが分かりにくいです。
そんなときは、Android Studio で View に設定されている全ての View attribute を確認できます。

気になる View を選んで、Attributes パネル を見ます。All Attributes にデフォルト値を含むすべての attribute の設定が表示されています。虫眼鏡アイコンを押せば attribute 名で検索もできます。

Attributes パネルでは設定されているリソース名までしか確認できません。また、Style などで間接的に値を設定されている場合も確認できない気がします。(この辺はよく分かっていないので知ってる人教えて! )
とにかく、表示されないときの条件は不明ですが、Attribute パネルだけではわからないときがあります。

Attribute パネルでわからないときは、実際にアプリを実行して調べる方法があります。
アプリを実行して Layout Inspector で気になる View を選択すると、実際に利用されているリソースのリテラル値が表示されます。

まとめ

Android Styling の基本をまとめました。
この記事では Styling のコアな部分しか触れていません。View attribute が適用される優先順位や、Theme Overlay の細かい挙動など、Styling で知っておくべきことはまだまだあります。Android Developers のドキュメントやブログ記事でたくさん解説されているので、興味のある方はチェックしてみてください!