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

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

# 手順

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

## 独自属性（アトリビュート）を定義する

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

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

```xml: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など設定できます。詳細は[こちらの記事](http://qiita.com/Hoshi_7/items/57c3a79c43efe05b5368#attrs%E5%B1%9E%E6%80%A7%E4%B8%80%E8%A6%A7)を参照ください。（drawableの場合は`format="refrences"`を指定します）


## 独自テーマを定義

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

```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`が定義されています。

```xml: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属性に設定してください。

```xml: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>
```

```xml: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/....`のように、色を指定してあげます。デフォルトテーマでは文字色は黒、ダークテーマでは文字色は白、というようにする場合は以下のように記述します。

```xml: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:`で指定します）

```xml: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()`を実行します。

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

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

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

```java: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`を取得したい）

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

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

# まとめ

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

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