同一VectorDrawableをCanvasに描画すると全部同じ色になる場合があるとの噂を聞いたので調べてみました。
適当にカスタムViewを作って以下のように赤、緑、青で描画してみます。
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) |
---|---|
どうもAPI28以下とAPI29以上で違いがあるようですね。
※setTint
を使っていますが、setColorFilter
でも、また、DrawableCompat.wrap()
したDrawableでも同じでした
Drawableの扱いでよくある問題としてはmutate()
をコールしていなくて全部同じ色になってしまうってのがありますが、それとも様子が違いますね。順序的には赤で描画、緑で描画、青で描画、という順序で処理を書いているのに、最後の青で全部が描画されてしまっています。
なんとなく描画処理の最適化が問題な感じがしますね。
解決方法がないか検証してみましょう。
検証1:LayerTypeを変更してみる
とりあえずハードウェアアクセラレーションが関係してるのでは?
カスタムViewのコンストラクタでsetLayerTypeをコールしてみます。
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
LAYER_TYPE_SOFTWARE | LAYER_TYPE_HARDWARE |
---|---|
LAYER_TYPE_SOFTWAREを指定するとちゃんと色が反映された。
反映されたのはいいけど、ハードウェアアクセラレーション切らないといけないってのは、おいそれととれる手段じゃない気がする
検証2:mutateのコールタイミングを変更してみる
初回にmutate()
をコールするのではなく、
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)
}
mutate()
をコールすることで、DrawableStateがアプリ内で共有されたものから切り離されるため、その前に設定したものとその後の2パターンができて、2色で描画されました。mutate()
をコールする度に別のインスタンスになる訳じゃないので複数回コールしても解消はしません。
検証3:色ごとにDrawableを用意する
色ごとに別のDrawableインスタンスを持たせるようにすればいいのでは?ということでやってみます。
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()
}
うまくいきました、しかし、色を変えて描画するってことは、だいたいの場合それなりのバリエーションの色があるってことでしょうから、その数のdrawableインスタンスを用意しておかないといけないってのも微妙ですね。
検証4:VectorDrawable以外を使ってみる
これまではVectorDrawableを使っていましたが、別のDrawableだったらどうでしょう?
BitmapDrawable
png画像のDrawableです。
問題なし
GradientDrawable
shapeタグで作るDrawableです。
<?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>
問題なし
AdaptiveIconDrawable
Android 8以上でランチャーアイコンに使われる奴ですね
全部同じ色になってしまいました。
中身はVectorDrawableなので想定の範囲内ではあります。
まとめ
VectorDrawableに着色しCanvasに描画しようとすると、Android 9(API 28)以下ではすべての色が同じになってしまうようです。
解決策としては
- LAYER_TYPE_SOFTWAREに変更する
- 色ごとにDrawableのインスタンスを利用する
- BitmapDrawableやGradientDrawableを利用する
などがありそうです。
しかし、いずれも本質的には解決していない、もやもやした感じが残ります。