【Android】ちょっと便利なDrawableを書く

この記事は[and factory Advent Calendar 2020](https://qiita.com/advent-calendar/2020/andfactory)の11日目の記事です。
昨日は@tokiyanさんの [[BigSur] アプリケーションを開くためのアクセス権限がありません。の対処法](https://qiita.com/tokiyan/items/6170afce9e02827a740f) でした。

# はじめに
Viewを角丸にする・色を変えるなど、Drawableを書く機会は多いと思います。
この記事では、私が個人的によく使っているDrawableの書き方のサンプルを紹介したいと思います。

# 色と同時にEffectを指定する
backgroundにcolorを指定し、foregroundにRippleEffectを指定する、なんて書き方をしている人は多いのでは無いでしょうか。
もちろんそれでも可能ですが、Effectを付け忘れること心配や、foreground指定がAPIレベルが23以上でないと使えないといった問題もあります。
そんな時に、Backgroundの色とEffectを同時に指定したDrawableを用意してしまうと便利です。

```xml:bg_color_effect.xml

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:attr/colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/blue" />
        </shape>
    </item>
</ripple>
```

![view1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/e706de06-999b-62fe-05de-f15f2af1fe63.gif)

shapeのみで使うパターンにrippleを被せただけです。
これをbackgroundに指定すれば、背景色 `@color/blue` と、RiipleEffectが同時に適応できます。
なお、 `?android:attr/colorControlHighlight` はデフォルトのRiipleEffectのカラーです。

# 角丸などの変形View＋Effect
角丸のViewなどをDrawableで作っても、Effectがそれをはみ出してしまう例をよく見ると思います。
あれはRipleEffectがView全体に広がってしまうのが原因です。
これも先程と同じ様にrippleを被せると、Effectがその範囲だけに広がってくれます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:attr/colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="@dimen/button_radius" /> <!-- 角の丸み -->
            <solid android:color="@color/blue" />
        </shape>
    </item>
</ripple>
```
![view2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/1e1d7fce-8bcb-8ec3-7f06-a1a940ea7294.gif)
Effectが角をはみ出ません

# Effect Only
ベタ塗りや枠の描画は行わず、Effectだけを適応するDrawableを作ることもできます。
複雑なViewの上部にFrameLayoutをかぶせて、そこにEffectを適用したい場合に使えます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:attr/colorControlHighlight">
    <item android:id="@android:id/mask"> <!-- maskを指定する -->
        <shape android:shape="rectangle">
            <solid android:color="@color/white" />
        </shape>
    </item>
</ripple>
```

`id/mask`以下の要素でEffectがMaskされます。
これは塗りつぶしているので、EffectだけのDrawableになります。

![view3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/2b231f76-23f5-9746-c395-8d060da02b6f.gif)

## 円形Effect Only
先程のを応用すると、円形のEffectだけのDrawableなんかもできます。
円形のViewをはみ出すようなEffectを付けたい場合などに使えると思います。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:attr/colorControlHighlight">
    <item android:id="@android:id/mask">
        <shape android:shape="oval"> <!-- ovalを指定 -->
            <solid android:color="@color/white" />
        </shape>
    </item>
</ripple>
```

![view4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/fd332baf-bb43-4f94-b4b9-e34b91d89c3c.gif)
Viewに沿ったovalになるので、長方形、正方形のViewでそれぞれ形が変わります。

# Effectを動的に作る
動的にViewを作りつつ、背景色を変え、Effectも付ける、なんていう時にはDrawableを動的に書く必要がでてきます。
`#ffffff` などを引数にして、以下の様に作る事ができます。

```kotlin
fun TextView.setCustomDrawable(textColor: String, backgroundColor: String, borderColor: String) {
    if (textColor.isNotBlank() && backgroundColor.isNotBlank() && borderColor.isNotBlank()) {
        setTextColor(Color.parseColor(textColor))
        // 角丸と色の設定
        val pressedDrawable = GradientDrawable().apply {
            setColor(Color.parseColor(backgroundColor))
            cornerRadius = context.resources.getDimensionPixelSize(R.dimen.radius).toFloat() // radiusは定義しているとします
            setStroke(4, Color.parseColor(borderColor)) // `1`はStorkeのWidth
        }
        // `?android:attr/colorControlHighlight` の取得
        val typedValue = TypedValue()
        context.theme.resolveAttribute(android.R.attr.colorControlHighlight, typedValue, true)
        val color = ContextCompat.getColor(context, typedValue.resourceId)
        // RippleEffectの設定
        val rippleDrawable = RippleDrawable(ColorStateList.valueOf(color), pressedDrawable, pressedDrawable)
        this.background = rippleDrawable
    }
}
```

![view5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/e62d74dc-e552-8294-b125-f00aab642769.gif)
少し複雑ですが、これで目的は果たせると思います。

# placeholderを自作する
少し毛色を変え、placeholderに使えるDrawableについてです。
対象として、アプリのロゴを入れたplaceholderを使う場合を想定しています。
placeholderを設定する箇所毎に、サイズの違う画像をデザイナーが用意してそれを使用する、なんていう事があると思います。
そんな時は、placeholder用のアプリロゴだけ用意してもらい、Drawableで必要サイズの画像を用意してしまえば、
画像分の容量が削減でき、デザイナーの負担も減ると思います。

```xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@android:color/darker_gray" /> <!-- 背景色を指定 -->
            <size
                android:width="100dp"
                android:height="100dp" />
        </shape>
    </item>
    <item
        android:width="24dp"
        android:height="24dp"
        android:drawable="@drawable/icon_logo"
        android:gravity="center" />
</layer-list>
```
また、これで作成した画像は `scaleType="fitXY"` で配置すると、伸縮するのが背景色部分になり、ロゴのサイズ自体は変わりません。
これで更に細かく画像を用意する必要が減ると思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/842f5406-3b40-8956-73e8-0b4f9852478f.png" width="300">

# おわりに
Drawableをうまく活用すると、結構いろいろなViewが作れます。
是非Drawableを活用し、UIのブラッシュアップや、容量・手間の削減に役立ててください。