Androidアプリで複数のテーマを実装する方法

最近はナイトテーマという形で背景が黒系のテーマに切り替えられるアプリを見かけますが、3つ以上のテーマを切り替えるアプリの場合には使えないので、それを実装する方法です。

手順

  1. 使用する独自属性(アトリビュート)を定義する
  2. 独自テーマを定義
  3. テーマごとに、属性に対応する値を定義する
  4. レイアウトで使用する色、背景、画像定義を、独自属性に置き換える
  5. テーマの切り替え処理を実装する

独自属性(アトリビュート)を定義する

例えば文字色について考えてみましょう。文字色は白色であれば@color/whiteで定義しているでしょうが、このままではテーマに応じた色変更に対応できません。

対応するには独自属性を作りレイアウトではこれを参照させる必要があります。属性はリソースフォルダに以下のように定義します。

res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCustomTheme">
        <!-- 文字色の定義 -->
        <attr name="customTextColor" format="color" />
    </declare-styleable>
 </resources>

参考
属性には、色、文字列、数値や、任意のリソースIDなど設定できます。詳細はこちらの記事を参照ください。(drawableの場合はformat="refrences"を指定します)

独自テーマを定義

AndroidStudioでActivityを作成すると、AndroidManifest.xmlにはこのように書かれています。

AndroidManifest.xml
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <!-- 略 -->
</application>

リソースフォルダには、以下のように、AppThemeが定義されています。

res/values/style.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

これと同じようにして、独自のテーマを定義します。同時にAndroidManifest.xmlも変更します。デフォルトのテーマをMyCustomTheme.Default、もうひとつのテーマをMyCustomTheme.Darkとします。もし3つめを作りたければ追加してください。手順は変わりません。

(追記)

カスタムテーマのベースとなるテーマMyCustomThemeを定義して、それを、attrs.xmlのdeclare-styleableタグのnama属性に設定してください。

res/values/style.xml
<!-- カスタムベーステーマを追加 -->
<style name="MyCustomTheme" parent="Theme.AppCompat.Light.DarkActionBar">
</style>

<!-- カスタムベーステーマを継承するテーマを追加 -->
<style name="MyCustomTheme.Default" parent="MyCustomTheme">
</style>

<style name="MyCustomTheme.Dark" parent="MyCustomTheme">
</style>
AndroidManifest.xml
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/MyCustomTheme.Default"> // <-- 変更箇所
    <!--  -->
</application>

テーマごとに、属性に対応する値を定義する

テーマを定義したstyle.xmlに独自属性の具体的な値を定義します。

先ほど、?attr/customTextColorという属性を定義しましたが、このままではこの属性は値を持ちません。プログラムで例えると変数だけ定義したようなもので、具体的な値を入れないと参照しても意味がありません。?attr/customTextColorは色を定義する属性なので、@color/....のように、色を指定してあげます。デフォルトテーマでは文字色は黒、ダークテーマでは文字色は白、というようにする場合は以下のように記述します。

res/values/style.xml
<style name="MyCustomTheme.Default" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="customTextColor">@color/black</item> // <-- 追加箇所
</style>
<style name="MyCustomTheme.Dark" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="customTextColor">@color/white</item> // <-- 追加箇所
</style>

レイアウトで使用する色、背景、画像定義を、独自属性に置き換える

例えばテキストビューの色が下記のように直接指定されている場合、属性を参照するように変更します。独自属性は?attr:で参照します。(OSですでに定義されている属性は、?android:で指定します)

hoge_layout.xml
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="?attr/customTextColor" // <-- 属性を参照
/>

これで、AndroidManifest.xmlで、テーマをMyCustomTheme.Defaultとした場合と、MyCustomTheme.Darkにした場合で文字色が変わることを確認してください

テーマの切り替え処理を実装する

テーマの切り替え処理のサンプルを例示します。テーマを指定するにはActivity#onCreate()setTheme()を実行します。

MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setTheme(R.style.MyCustomTheme_Dark); // ここでテーマを設定
}

上記は直接テーマ指定していますが、実際は設定画面などで変更後のテーマをプレファレンスに保存し、onCreate()のタイミングでプレファレンスから適用テーマを読み出してsetTheme()する実装になるでしょう。

なお、テーマはonCreate()のタイミングで適用されるので、テーマの変更は一度アクティビティを再起動しないといけません。その場合のサンプルを示します。

MainActivity.java
// 選択したテーマの保存処理
changeTheme();
// アクティビティ再起動
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); // 起動しているActivityをすべて削除し、新しいタスクでMainActivityを起動する
startActivity(intent);

Tips

Javaプログラムからテーマに対応した属性の値を取得する

プログラムから現在のテーマに対応した、customTextColorの値を取得する方法は以下のようにします。(現在がデフォルトテーマなら@color/black、ダークテーマなら@color/whiteを取得したい)

MainActivity.java
TypedValue outValue = new TypedValue();
getTheme().resolveAttribute(R.attr.customTextColor, outValue, true);
int rId = outvalue.resourceId; // 現在のテーマに対応するcustomTextColorのcolorリソースID

// 例えば動的に色をセット
textView.setTextColor(rId);

まとめ

複数テーマの実装はこの手順で行えば難なく行えます。

色や画像などのリソースの定義は後から行なうのは大変なので、アプリを作りはじめから独自属性を作っておくと後が楽でしょう。