Androidのボタンスタイルを理解する

（今から新規でアプリ作るなら、Material Components使うほうがオススメだよ！）

# AppCompat.Buttonスタイル

とにもかくにも、スタイル定義を理解するためにも、`AppCompat.Button`を理解しましょう。

`AppCompat.Button`には主に以下の4種類のスタイルがあります。

* Widget.AppCompat.Button
* Widget.AppCompat.Button.Colored
* Widget.AppCompat.Button.Borderless
* Widget.AppCompat.Button.Borderless.Colored


| Default | backgroundTint="#4DB6AC" |
|:--:|:--:|
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/40926/e40dc029-3baa-46dd-20cc-e43a44c95456.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/40926/dd435333-89f5-0d23-76d8-c5a2eb6134f9.png)
 |

当然、Borderlessスタイルは背景がないのでtintされません。

## AppCompat.Button

Androidの標準のボタンです。
旧マテリアルデザインではRaised Button、現在のマテリアルデザインではContained Buttonなどと呼ばれています

**TextAppearance**
`?android:textAppearanceButton = TextAppearance.AppCompat.Widget.Button`

**AppCompat.Buttonの色**

|  | enabled | disabled[^button_disabled] |
|:--:|:--:|:--:|
| **background** | `?colorButtonNormal` | `?colorButtonNormal` |
| **textColor[^button_textColor]** | `?android:textColorPrimary` | `?android:textColorPrimary` |
| **Ripple** | `?colorControlHighlight` | - |



## サイズ

`AppCompat.Button`の`minHeight`は**48dp**です。
しかし、実際表示してみると明らかにそれより小さいことがわかると思います。

これは、`AppCompat.Button`に指定されている`background`リソースにインセットが設けられているためです。
それぞれ、上下に**6dp**、左右に**4dp**のインセットが設定されています。

結果的に、

```
48dp (minHeight) - 6dp (insetTop) - 6dp (insetBottom) = 36dp
```

が見た目上の高さとなっています。

なお、この設定は旧マテリアルデザインのスペックに沿っています。
現在のマテリアルデザインガイドラインでは[コンポーネントのサイズと見た目のサイズを違えるという記述は消えている](https://material.io/design/components/buttons.html#specs)ため、そのうちなくなるかもしれません。
（そもそもAppCompatよりも[Material Components](https://github.com/material-components/material-components-android)が推奨されていく可能性のほうが高いと思いますが）


## elevation

`AppCompat.Button`の特徴として`elevation`が設定されています。
ただし、この`elevation`は`android:elevation="0dp"`としても無効にすることはできません。

`AppCompat.Button`の`elevation`は、実際は`android:stateListAnimator`によって制御されているため、無効にしたい場合は`android:stateListAnimator="@null"`とする必要があります。


## AppCompat.Button.Colored

`AppCompat.Button`のサブスタイルで、ボタンに適用されるテーマの`colorAccent`の色になるボタンです。

**TextAppearance**
`TextAppearance.AppCompat.Widget.Button.Colored`

**AppCompat.Button.Coloredの色**

|  | enabled | disabled |
|:--:|:--:|:--:|
| **background** | `?colorAccent` | `?colorButtonNormal` |
| **textColor** | `?android:textColorPrimaryInverse` | `?android:textColorPrimary`[^button_disabled] |
| **Ripple** | `?colorControlHighlight` | - |


ただ単にtintする場合、`AppCompat.Button`と`AppCompat.Button.Colored`どちらのスタイルを使うかについては、**ボタンの明度がLightかDarkか**で決めていいと思います。（ざっくりテキストがInverseかどうかの差です）


# AppCompat.Button.Borderless

旧マテリアルデザインではFlat Button、現在はText Buttonと呼ばれているボタンです。

`AppCompat.Button`のbackgroundからshapeを除いているだけです。

**TextAppearance**
`?android:textAppearanceButton = TextAppearance.AppCompat.Widget.Button`

**AppCompat.Button.Borderlessの色**

|  | enabled | disabled[^button_disabled] |
|:--:|:--:|:--:|
| **background** | - | - |
| **textColor** | `?android:textColorPrimary` | `?android:textColorPrimary` |
| **Ripple** | `?colorControlHighlight` | - |


## AppCompat.Button.Borderless.Colored

`AppCompat.Button.Borderless`のテキストに色付けをおこなったスタイルです。

**AppCompat.Button.Borderlessの色**

|  | enabled | disabled |
|:--:|:--:|:--:|
| **background** | - | - |
| **textColor** | `?colorAccent` | `?android:textColorSecondary`[^button_disabled] |
| **Ripple** | `?colorControlHighlight` | - |


# AppCompat.Button系スタイルの色はどう指定するのがよいか？

`AppCompat.Button`系スタイルの色の変え方には2種類あります。

1つは、最初に扱った`backgroundTint`を利用する方法です。
2つめは、`ThemeOverlay`を利用する方法です。

それぞれにメリット・デメリットが存します。


## backgoundTint

`backgroundTint`とは、`background`にあてた画像リソースにカラーフィルタをかけて色を変化させる属性です。

**対応するminSdkVersion**によって利用できるネームスペースが違うので気をつけてください。

| Androidバージョン | app:backgroundTint | android:backgroundTint |
|:--:|:--:|:--:|
| 21未満 | o | x |
| 21+ | o | o (推奨) |


`backgroundTint`に指定できるのは、ColorリソースまたはColorStateListリソースになります。

ColorStateListとは以下のような定義をしたリソースを指します。[^colorStateList]

```xml:example_color_state_list
<selector>
    <item android:state_enabled="false" android:color="@color/disabled_color"/>
    <item android:color="@color/enabled_color"/>
</selector>
```

### メリット

レイアウト時に即席でどんな色でも当てられるので、デメリットを気にしない場合はデザインの要求に対してコストが圧倒的に低いです。

```xml
<Button
    style="@style/Widget.AppCompat.Button.Colored"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:backgroundTint="@color/red"
    android:text="Red Button"/>
```


### デメリット

ステート対応がめんどくさい。

特殊なステートの場合はどっちにしろめんどくさいのですが、enabled - disabledの状態にすらColorStateListを作る必要があります。

将来的に、ColorStateListリソースがres/drawableに大量に作られて管理しきれなくなります。


## ThemeOverlay

ThemeOverlayとは、View及びViewGroup階層で**テーマの任意の変数をオーバーライドする**手法です。

`theme`属性で指定しますが、これについても**対応するminSdkVersion**によって利用できるネームスペースが違います。

| Androidバージョン | app:theme[^theme_preview] | android:theme |
|:--:|:--:|:--:|
| 21未満 | o | x |
| 21+ | o | o (推奨) |

ThemeOverlayの適用範囲は、**そのView及びそれ以下のView階層**です。

ThemeOverlayを使ったボタンの色の変更は、以下のようにします。

```xml:example_button
<Button
    style="@style/Widget.AppCompat.Button.Colored"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Red Button"
    android:theme="@style/ThemeOverlay.Button.Red"/>
```

```xml:example_button_theme
<style name="ThemeOverlay.Button.Red" parent="ThemeOverlay">
    <item name="colorAccent">@color/red</item>
</style>
```

`AppCompat.Button.Colored`の色は、先述したように`?colorAccent`が利用されるため、ThemeOverlayでその値だけ変更することでボタンの色が変更できます。


### メリット

既存のボタンのステート定義を尊重しつつ、部分的に色を変更できる。

ThemeOverlayで当てる場合、`?colorAccent`のみを変更しているためdisabled状態は変わらず標準の表現が使われます。
逆にdisabledの色も任意にやりたい場合は、`?colorButtonNormal`を変更すればいいだけです。


### デメリット

即席で色を当てる用途で使うには手間がかかる。

テーマを定義しないといけないため、1箇所でしか使わないボタンスタイルのためにやる作業としては、コストが重いように感じます。


# ボタンのスタイル管理

ここまで、`AppCompat.Button`系スタイルの話しかしませんでしたが、独自スタイルにおいても同じことが言えます。
そのため`AppCompat.Button`を理解することで、独自定義の方法や何を懸念するべきかということも見えてくるはずです。

例えば今僕の携わっているプロジェクトでは、アウトラインボタンという枠線のみのボタンスタイルが定義されていますが、`AppCompat.Button`を参考に`drawable`、`textAppearance`、`style`の定義を行っています。

## 結局tintとthemeどちらを利用すればいいか

ハッキリ言って一貫した答えはありません。
画面仕様、デザインの指定から読み取ったり、デザイナーさんに訊いてみるのが手っ取り早いです。

目安としては

* disabledステートが必要なら`ThemeOverlay`
* とりあえず色付きのボタンが置ければいいなら`backgroundTint`

くらいの認識でいいと思います。


# 注釈

[^button_disabled]: disabled状態の場合、テーマの`?android:disabledAlpha`の値がアルファ値として適用されます。

[^button_textColor]: `android:textAppearance`のデフォルト値が`?android:textAppearanceButton`になっており、そのテーマデフォルトとなるTextAppearanceの結果としてこの変数が利用されています。

[^colorStateList]: `selector`リソースに`android:color`を指定したものです。Android Studio 3.1.4の時点では、`item`タグの`android:color`は補完されませんが実際は利用可能です、（他にもいくつか補完されない属性が存在しています）

[^theme_preview]: `app:theme`の場合、結果がレイアウトプレビューには反映されません。その場合は、`tools:theme`にも同じ値を設定することで解決できます。