Android 9(API 28)以下で同一VectorDrawableをCanvasに描画すると全部同じ色になる件

同一VectorDrawableをCanvasに描画すると全部同じ色になる場合があるとの噂を聞いたので調べてみました。
適当にカスタムViewを作って以下のように赤、緑、青で描画してみます。

```kotlin
private val drawable = AppCompatResources.getDrawable(context, R.drawable.ic_android)!!.mutate()
private val rect = Rect(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)

override fun onDraw(canvas: Canvas) {
    rect.offsetTo(50, 50)
    drawable.bounds = rect
    drawable.setTint(Color.RED)
    drawable.draw(canvas)

    rect.offsetTo(150, 50)
    drawable.bounds = rect
    drawable.setTint(Color.GREEN)
    drawable.draw(canvas)

    rect.offsetTo(250, 50)
    drawable.bounds = rect
    drawable.setTint(Color.BLUE)
    drawable.draw(canvas)
}
```

|API28(Android 9)|API29(Android 10)|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4fc359b0-60b8-ea8d-7917-8f210a318850.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d9543403-1123-770c-9fde-32723819a8d7.png)|

どうもAPI28以下とAPI29以上で違いがあるようですね。
※`setTint`を使っていますが、`setColorFilter`でも、また、`DrawableCompat.wrap()`したDrawableでも同じでした

Drawableの扱いでよくある問題としては`mutate()`をコールしていなくて全部同じ色になってしまうってのがありますが、それとも様子が違いますね。順序的には赤で描画、緑で描画、青で描画、という順序で処理を書いているのに、最後の青で全部が描画されてしまっています。
なんとなく描画処理の最適化が問題な感じがしますね。

解決方法がないか検証してみましょう。

## 検証1：LayerTypeを変更してみる

とりあえずハードウェアアクセラレーションが関係してるのでは？
カスタムViewのコンストラクタでsetLayerTypeをコールしてみます。

```kotlin
init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
}
```

|LAYER_TYPE_SOFTWARE|LAYER_TYPE_HARDWARE|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/60bb9d49-9d21-cf87-6005-f033e65afdd7.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4fc359b0-60b8-ea8d-7917-8f210a318850.png)|

LAYER_TYPE_SOFTWAREを指定するとちゃんと色が反映された。
反映されたのはいいけど、ハードウェアアクセラレーション切らないといけないってのは、おいそれととれる手段じゃない気がする

## 検証2：mutateのコールタイミングを変更してみる

初回に`mutate()`をコールするのではなく、

```kotlin
private val drawable = AppCompatResources.getDrawable(context, R.drawable.ic_android)!!
private val rect = Rect(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)

override fun onDraw(canvas: Canvas) {
    rect.offsetTo(50, 50)
    drawable.bounds = rect
    drawable.setTint(Color.RED)
    drawable.draw(canvas)

    drawable.mutate()

    rect.offsetTo(150, 50)
    drawable.bounds = rect
    drawable.setTint(Color.GREEN)
    drawable.draw(canvas)

    drawable.mutate()

    rect.offsetTo(250, 50)
    drawable.bounds = rect
    drawable.setTint(Color.BLUE)
    drawable.draw(canvas)
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/dc419cb7-38a1-5ba5-5915-0cf144beff55.png)

`mutate()`をコールすることで、DrawableStateがアプリ内で共有されたものから切り離されるため、その前に設定したものとその後の2パターンができて、2色で描画されました。`mutate()`をコールする度に別のインスタンスになる訳じゃないので複数回コールしても解消はしません。

## 検証3：色ごとにDrawableを用意する

色ごとに別のDrawableインスタンスを持たせるようにすればいいのでは？ということでやってみます。

```kotlin
private val redDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_android)!!.mutate().also {
    it.setTint(Color.RED)
}
private val greenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_android)!!.mutate().also {
    it.setTint(Color.GREEN)
}
private val blueDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_android)!!.mutate().also {
    it.setTint(Color.BLUE)
}
private val rect = Rect(0, 0, redDrawable.intrinsicWidth, redDrawable.intrinsicHeight)

override fun onDraw(canvas: Canvas) {
    rect.offsetTo(50, 50)
    redDrawable.bounds = rect
    redDrawable.draw(canvas)
    redDrawable.invalidateSelf()

    rect.offsetTo(150, 50)
    greenDrawable.bounds = rect
    greenDrawable.draw(canvas)
    greenDrawable.invalidateSelf()

    rect.offsetTo(250, 50)
    blueDrawable.bounds = rect
    blueDrawable.draw(canvas)
    blueDrawable.invalidateSelf()
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/bd314414-2bc4-65ab-28a8-a6b6f867e2f4.png)

うまくいきました、しかし、色を変えて描画するってことは、だいたいの場合それなりのバリエーションの色があるってことでしょうから、その数のdrawableインスタンスを用意しておかないといけないってのも微妙ですね。

## 検証4：VectorDrawable以外を使ってみる

これまではVectorDrawableを使っていましたが、別のDrawableだったらどうでしょう？

### BitmapDrawable

png画像のDrawableです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8ed04c9d-c509-96a0-0b09-f7ae574a4b47.png)

問題なし

### GradientDrawable

shapeタグで作るDrawableです。

```xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="30dp" />
    <solid android:color="@color/black" />
    <size android:width="24dp" android:height="24dp" />
</shape>
```

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/72a8acaf-dbc5-bbc6-62e9-550464b784d7.png)

問題なし

### AdaptiveIconDrawable

Android 8以上でランチャーアイコンに使われる奴ですね

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/1518baa5-ffe7-88db-24b1-a11d6be7525c.png)

全部同じ色になってしまいました。
中身はVectorDrawableなので想定の範囲内ではあります。

----

# まとめ

VectorDrawableに着色しCanvasに描画しようとすると、Android 9(API 28)以下ではすべての色が同じになってしまうようです。

解決策としては
- LAYER_TYPE_SOFTWAREに変更する
- 色ごとにDrawableのインスタンスを利用する
- BitmapDrawableやGradientDrawableを利用する

などがありそうです。
しかし、いずれも本質的には解決していない、もやもやした感じが残ります。