Goodbye `shape` - AndroidのMaterialButtonがすごい良かった話

# 概要

Material Design Theming対応を各アプリで進めるのは、AndroidX対応と依存して難しいところはありますが、そもそも対応するとどういいのかみたいな情報があまりなかったので、同じことをするにしても新しいのだとどうよくなるのか？みたいな話を書きたいと思ってます。

Material Deisgnのライブラリで、MaterialButtonとか他にもStyleが入ってきて、今まで作ってきていたオレオレのshape drawable（XMLで作っていたDrawable系のファイルを纏めて指している）を作らなくてもだいたいの事ができるようになったよねという話です。

## Material Themingとは？

MaterialDesignを、実際のアプリに合わせてカスタマイズできるように少しやわらかくしたもの。

MaterialThemingを実装した各プラットフォームごとの実装などをも提供されている

公式サイト：[https://material.io/design/material-theming/](https://material.io/design/material-theming/)

## MaterialButton

MaterialDesign Guidline[^1]のボタンを実装したビュークラス。

material-designのライブラリの一部として提供されている。

実装は、AppCompatButtonをベースにしているが、ガイドラインの基本的な実装が入っておりかなり高機能。

Document: https://material.io/develop/android/components/material-button/

### MaterialButton デフォルトで実装できるもの

![material-buttons.png](https://qiita-image-store.s3.amazonaws.com/0/42614/2b7e27d5-e927-72e4-0c24-ec392b402b17.png)


### よく使うスタイル

| ボタン | スタイル |
|:-------|:---------|
| 角丸ボタン | `@style/Widget.MaterialComponents.Button` |
| アイコンボタン | `@style/Widget.MaterialComponents.Button.Icon` |
| テキストボタン | `@style/Widget.MaterialComponents.Button.TextButton` |
| 外枠ボタン | `@style/Widget.MaterialComponents.Button.OutlinedButton` |

### MaterialButtonのよく使うプロパティ

| カテゴリ | property | 役割 |
|:--------|:-------|:----|
| 大きさ   | android:inset | タッチ領域内での大きさを変えられる。デフォルトで、topとbottomに4dpついている。 |
| アイコン | app:icon | アイコンを表示させられる。drawableLeftの上位互換。 |
|         | app:iconTint | アイコンの色を変えられる。modeも別で指定できる。 |
|         | app:iconGravity | アイコンの位置をテキストに合わせるか、全体に合わせるか決められる。 |
|         | app:iconPadding | アイコンとの距離を変えられる。 |
| 背景    | app:backgroundTint | 背景の色を変えられる。modeも別で指定できる。 |
| 角      | app:cornerRadius | ボタンの角のRadiusを変えられる。 |
| 枠線    | app:strokeColor | 線の色を変えられる。 |
|        | app:strokeWidth | 太さを変えられる。 |
| アニメーション | app:rippleColor | Rippleの色を変えられる。 |
| | android:stateListAnimator | 押した時など、ステートごとのアニメーションを変えられる。 |

### ビューのスペースの話

- margin : Viewの領域外とのスペース
- padding : View内のコンテンとのスペース(Buttonだと、TextなどコンテンツはViewによる)
- inset : Viewの領域とDrawableとのスペース※影は含まない

insetプロパティも、全部にきくわけじゃない？自作のdrawableなどでもInsetでないと効かないかもしれない :thinking: 

![padding-inset-margin.png](https://qiita-image-store.s3.amazonaws.com/0/42614/0f89594d-3f76-0e4d-7015-fd9de0f678cf.png)

### まとめ

- MaterialButtonすごい
- だいたいはプロパティとスタイルで実装できる
- アニメーションとかもいい感じに付くので、できるだけスタイルとプロパティを使う

## ところで、これどうやるの。。。？

![mio-design%2Fassets%2F0B6xUSjjSulxcN21PWXZ6VHZtMFk%2Fshapingmaterial-hero-1.png](https://qiita-image-store.s3.amazonaws.com/0/42614/4a91e8b2-dfaf-155f-3143-a17b181aff0c.png)


実例ShrineShape : https://material.io/design/material-studies/shrine.html#shape

# Welcome new `shape` :handshake: 

## MaterialComponentのShapeの状況

<img width="728" alt="スクリーンショット 2018-09-28 11.22.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/42614/91a00ae5-8dec-073c-0870-64e7c276706c.png">


RoadMap : https://github.com/material-components/material-components/blob/develop/ROADMAP.md

## Experimentalでも使えるShape

BottomAppBarではすでに使われてる。使い方も、BottomAppBarを見た。

- MaterialShapeDrawable : 画像情報。色とかMaterialDesignっぽいものを指定できる。
- ShapePathModel : パス情報だけ管理してるモデル
- (Treatment)
    - CornerTreatment : 角のパス整形ができる。
    - EdgeTreatment : 辺のパス整形ができる。

Document : https://github.com/material-components/material-components-android/blob/8f7dc21a27880eed391f45548a40de8189f31172/docs/theming/Shape.md

## 使ってみた

![diamond_cut.png](https://qiita-image-store.s3.amazonaws.com/0/42614/08e5cac5-17d2-ceb1-e9f3-a8c12aaf2843.png)

## 矢印ボタンの実際のコード

```kotlin
val shapePathModel = ShapePathModel().apply {
    // CutCornerTreatimentと、RoundedCornerTreatmentが用意されている
    val cornerTreatment = CutCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
    topRightCorner = cornerTreatment
    bottomRightCorner = cornerTreatment
    // TriangleEdgeTreatmentが用意されていて、三角に切り抜ける。
    leftEdge = TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size), true)
}
val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
    // MaterialButtonと同じ様な設定ができる
    setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
}
// toRippleはExtension。インタラクションつけるのはかなり大変。
sharp_cut_button.background = shapeDrawable.toRipple()
```

## 問題は色々ある

- BackgroundのDrawableをコードで指定する必要がある
- コードで指定すると、InsetとかTintとか、steteList~~とか色々無視される
- 自作のCornerTreatmentとEdgeTreatmentを作るのは難しい

## Comming Soonなもの

Styleで全体で、Cutにするかどうかなどを指定できるようになる？

MaterialDesignの話で、こういうShapeの計上などはアプリのブランドで一貫性をもたせないとダメだという話もあったので、Styleで設定できるのが自然そうです。

```
<style name="Theme.MyApp" parent="Theme.MaterialComponents.Light">
  ...
  <item name="cornerRadiusPrimary">8dp</item>
  <item name="cornerStylePrimary">cut</item>
  <item name="cornerRadiusSecondary">4dp</item>
  <item name="cornerStyleSecondary">cut</item>
  ...
</style>
```

Document : https://github.com/material-components/material-components-android/blob/8f7dc21a27880eed391f45548a40de8189f31172/docs/theming/Shape.md

## まとめ

- ShapeのStyleでのサポートが待ち遠しい
- どうしてもカットしたかったら、MaterialShapeDrawableを使う
- アニメーションなど色々と難しいのでボタンで使用するなら、 自分でパス書き出してVectorDrawableの方が楽

---

# おまけ

## サンプルコードとか

GitHub：https://github.com/Reyurnible/android-shape-sample

## MaterialShapeDrawableの例

```kotlin
private fun setSharpCutButton() {
    val shapePathModel = ShapePathModel().apply {
        topLeftCorner = CutCornerTreatment(resources.getDimension(R.dimen.btn_cut_corner_size))
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    sharp_cut_button.background = shapeDrawable.toRipple()
}

private fun setEdgeButton() {
    val shapePathModel = ShapePathModel().apply {
        setAllEdges(TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_cut_edge_size), true))
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    edge_button.background = shapeDrawable.toRipple()
}

private fun setAllowCutButton() {
    val shapePathModel = ShapePathModel().apply {
        val cornerTreatment = CutCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
        topRightCorner = cornerTreatment
        bottomRightCorner = cornerTreatment
        leftEdge = TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size), true)
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    allow_cut_button.background =shapeDrawable.toRipple()
}

private fun setRoundedCornerButton() {
    val shapePathModel = ShapePathModel().apply {
        val cornerTreatment = RoundedCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
        topLeftCorner = cornerTreatment
        topRightCorner = cornerTreatment
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    rounded_corner_button.background = shapeDrawable.toRipple()
}

private fun Drawable.toRipple(): RippleDrawable =
        RippleDrawable(
                ContextCompat.getColorStateList(this@MainActivity, R.color.mtrl_btn_ripple_color),
                this,
                null
        )
```

## Buttonのよく使う自作スタイル：

```xml
<!-- Buttons -->
<style name="Widget.AppTheme.Button" parent="Widget.MaterialComponents.Button" />

<style name="Widget.AppTheme.Button.Rectangle">
    <item name="android:insetLeft">0dp</item>
    <item name="android:insetRight">0dp</item>
    <item name="android:insetTop">0dp</item>
    <item name="android:insetBottom">0dp</item>
    <item name="cornerRadius">0dp</item>
</style>

<style name="Widget.AppTheme.Button.Rectangle.Rounded">
    <item name="cornerRadius">4dp</item>
</style>

<style name="Widget.AppTheme.Button.Rounded">
    <item name="android:insetLeft">@dimen/button_horizontal_inset</item>
    <item name="android:insetRight">@dimen/button_horizontal_inset</item>
    <item name="android:insetTop">@dimen/button_vertical_inset</item>
    <item name="android:insetBottom">@dimen/button_vertical_inset</item>
    <item name="android:minHeight">@dimen/view_size_normal</item>
    <item name="android:textStyle">bold</item>
    <item name="android:paddingStart">@dimen/button_rounded_horizontal_padding</item>
    <item name="android:paddingEnd">@dimen/button_rounded_horizontal_padding</item>
    <item name="cornerRadius">@dimen/button_rounded_radius</item>
</style>
```

## stateListAnimatorの例：

```xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Pressed viewAction -->
    <item
        android:state_enabled="true"
        android:state_pressed="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Hover viewAction. This is triggered via mouse. -->
    <item
        android:state_enabled="true"
        android:state_hovered="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Focused viewAction. This is triggered via keyboard. -->
    <item
        android:state_enabled="true"
        android:state_focused="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Base viewAction (enabled, not pressed) -->
    <item android:state_enabled="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:startDelay="100"
                android:valueTo="0dp"
                android:valueType="floatType"
                tools:ignore="UnusedAttribute" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Disabled viewAction -->
    <item>
        <set>
            <objectAnimator
                android:duration="0"
                android:propertyName="translationZ"
                android:valueTo="0dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="0dp"
                android:valueType="floatType" />
        </set>
    </item>

</selector>
```