Androidのテーマに関するTips

4 min read読了の目安(約4100字

これはAndroid Advent Calendar 2020 12日目の記事です。

Android開発でちょっと使えるテーマ周りのTipsを4つ紹介します!

テーマ属性として定義した色を取得する

colorPrimarycolorSecondaryといったテーマ属性を取得、利用する方法を紹介します。

XMLから取得

XMLから取得する方法は簡単で、?attr/attributeNameというように書くことことで、現在のテーマで定義している値を取得することができます。

<View
    android:id="@+id/view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary" />

この使い方は、既存コンポーネントのスタイル定義などでも使われているため、利用するコンポーネントのcolorやdimenの定義を追うときに覚えておくと便利です。

コードから取得

コードから取得する場合はobtainStyledAttributesを利用します。

@ColorInt
fun Context.getThemeColor(
    @AttrRes themeAttrId: Int
): Int {
    return obtainStyledAttributes(
        intArrayOf(themeAttrId)
    ).use {
        it.getColor(0, Color.TRANSPARENT)
    }
}

これは下記のように使います。

val colorPrimary = context.getThemeColor(R.attr.colorPrimary)

この方法を使うと、呼び出しに使ったContextに紐付いたテーマ属性を利用することができるので、ダークテーマを適用している場合は必要に応じてnightのテーマで定義した色を使うことができるので便利です。

テーマ属性を独自に定義する

カスタム属性を定義することでテーマ属性としてかんたん理利用できます。

<!-- attrs.xml -->
<resources>
    <attr name="customColor" format="color" />
    <attr name="dividerColor" format="color" />
</resources>

<!-- themes.xml -->
...
    <style name="Base.AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        ...
        <item name="customColor">?attr/colorPrimary</item>
        <item name="dividerColor">?attr/colorOnBackground</item>
    </style>
...

こうすることで、Viewの属性に対して定義した値を利用することができます。

<View
    android:id="@+id/divider"
    android:layout_width="wrap_content"
    android:layout_height="1dp"
    android:background="?attr/dividerColor" />

Google I/O Android Appでは、セッションリストのキーラインとしてsessionListKeylineというカスタム属性が定義されています。

テーマの一部を置き換える

なにげなく利用している手法ですが、テーマやスタイル属性は置き換えが可能です。

<!-- themes.xml -->
...
    <style name="Base.AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        ...
        <item name="colorPrimary">@color/app_yellow_500</item>
        ...
    </style>
...

上記のように書くことで、parentに指定したテーマのcolorPrimaryを置き換えています。

Google I/O Android Appでは、カスタム属性のsessionListKeylineをセッションの詳細画面で利用するテーマで置き換えることで、同じレイアウトを使いつつキーラインのみを変えていました。

    ...
    <!-- Non-top-level screens should inflate with this theme to replace the keyline. -->
    <style name="AppTheme.Detail">
        <item name="sessionListKeyline">@dimen/margin_normal</item>
    </style>
    ...

コンテキストに紐付いたテーマを変更する

テーマはApplicationやActivityといったコンテキストに紐付いています。
ContextThemeWrapperを利用することで、コンテキストに紐付いたテーマを変更することが可能です。

たとえば、Fragmentに対してテーマを設定することはできませんが、ContextThemeWrapperを利用してlayoutInflaterを生成することで、指定のテーマでFragmentのViewを生成することができます。

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val themedInflater: LayoutInflater = if (status.isProMode) {
        inflater.cloneInContext(
            ContextThemeWrapper(requireContext(), R.style.Theme_Pro)
        )
    } else inflater
    return super.onCreateView(themedInflater, container, savedInstanceState)
}

その他にも、コンテキストを使って生成するダイアログなどでもContextThemeWrapperを利用することでテーマを置き換えることができます。

何度も紹介しているGoogle I/O Android Appでも、同様にしてアプリの詳細画面用のテーマを適用しています。

まとめ

Androidでは、テーマとスタイルを活用することでUIの構造や動作からアプリのデザインの詳細を分離できます。
注目されているJetpack Composeでも同じ概念が適用されるようなので、テーマとスタイル周りについては徐々に整理していきたいですね!

参考リンク