drawable属性にcolorリソースが指定できるのがモヤッとしたので調べてみた
TL;DR
Context#getDrawable()
の引数にColorStateList以外のcolor resource id(e.g. R.color.hogehoge
)を渡すとColorDrawable
が返ってきます。
本文
なんかタイトルがモヤっとしててわかりにくいと思いますがきっかけは下のようなコードが動いたからです。
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/colorPrimaryDark" android:state_enabled="false"/> <item android:drawable="@color/colorAccent" android:state_pressed="true"/> <item android:drawable="@color/colorPrimary"/> </selector>
一見平凡なStateListDrawable
のdrawable resourceですがandroid:drawable
にcolorリソースを指定してもColorDrawable
として認識されます。
Drawable Resourceのリファレンスを確認してもReference to a drawable resource
としか書いてなくて一見するとdrawable resourceしか受け付けないように見えます。
ただ、Context.getDrawableのリファレンス)を見るとid
の定義がThe desired resource identifier, as generated by the aapt tool.
と書いていて若干曖昧な表現になってました。
というわけで以下のようなコードがどういう処理をしているかAOSPの実装を掘り下げていきました。
val drawable = getDrawable(R.drawable.state_list_demo)
読み進んで行くとResources#getDrawableForDensity(id:Int, density:Int, theme)
に行き着きます。
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { final TypedValue value = obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValueForDensity(id, density, value, true); return impl.loadDrawable(this, value, id, density, theme); } finally { releaseTempTypedValue(value); } }
ResourcesImpl#getValueForDensity
も気になりますが*1、Drawable
を返しているのはloadDrawable
なのでこっちを読み進んでいきます。
コードが長いので端折りますが途中でこんなコードが見つかります。
final boolean isColorDrawable; final DrawableCache caches; final long key; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; caches = mColorDrawableCache; key = value.data; } else { isColorDrawable = false; caches = mDrawableCache; key = (((long) value.assetCookie) << 32) | value.data; }
どうも引数で渡されたvalue
(TypedValue
)のtypeがTYPE_FIRST_COLOR_INT
からTYPE_LAST_COLOR_INT
の範囲ならisColorDrawable
がtrueになることがわかります。この辺を追いかければ答えが見つかりそうです。
少し読み進めていくとあっさり答えが見つかりました。
} else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else {
つまり引数で渡したリソースがTypedColor.type
がColor関連ならばColorDrawable
を返すようです。
おまけ1
一応StateListDrawable
がどのように生成されるのかも確認してみました。やはり<item>
タグのandroid:drawable
属性も最終的にはResourcesImpl#loadDrawable()
を通るようです。
Context#getDrawable(id:Int) -> Resources#getDrawable(id) -> Resources#getDrawable(id, theme = null) -> Resources#getDrawableForDensity() -> ResourcesImpl#loadDrawable() -> ResourcesImpl#loadDrawableForCookie() -> Drawable.createFromXmlForDensity() ← xmlファイルのパース処理 -> Drawable.createFromXmlInnerForDensity() -> DrawableInflater#inflateFromXmlForDensity() ← xmlファイルの最初のタグ名に応じてDrawableインスタンスを生成 -> StateListDrawable#inflate() ← 残りのxmlのパースは生成されたDrawableに委譲 -> StateListDrawable#inflateChildElements() ← itemタグのパース -> TypedArray#getDrawable(index:Int) -> TypedArray#getDrawableForDensity(index, density = 0) -> Resources#loadDrawable() -> ResourcesImpl#loadDrawable() -> 以下続く
おまけ2
ちなみにStateListDrawable
と同じタグ(<selector>
)を使っているStateColorList
のリソースもこんな使い方ができるようですw
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textColor="@drawable/state_color_list_sample" /> <!-- res/drawable/state_color_list_sample.xml --> <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/colorPrimaryDark" android:state_enabled="false"/> <item android:color="@color/colorAccent" android:state_pressed="true"/> <item android:color="@color/colorPrimary"/> </selector>
追記1
Context.getDrawable()
にColorStateList
のリソースIDを渡すと見事に落ちました。
StateListDrawable
と勘違いしてパースに失敗するようです。
Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable at android.graphics.drawable.StateListDrawable.inflateChildElements(StateListDrawable.java:190) at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:122) at android.graphics.drawable.DrawableInflater.inflateFromXmlForDensity(DrawableInflater.java:142) at android.graphics.drawable.Drawable.createFromXmlInnerForDensity(Drawable.java:1332) at android.graphics.drawable.Drawable.createFromXmlForDensity(Drawable.java:1291) at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:833) at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631) at android.content.res.Resources.getDrawableForDensity(Resources.java:888) at android.content.res.Resources.getDrawable(Resources.java:827) at android.content.Context.getDrawable(Context.java:626) at androidx.core.content.ContextCompat.getDrawable(ContextCompat.java:463)
*1:コードをざっくり見たところ、指定したリソースIDの情報を取ってきてTypedValueにセットするメソッドのようです。