今更な話ですが、Android5(APIレベル21)以降ではベクタードローアブルがサポートされるようになりました。ですのでAPI21以上のみをターゲットとする場合、アプリをビルドして実行する上ではテンプレ通りに設定すれば問題はありません、たぶん。(ベクタードローアブル自体は色々微妙な挙動する場合があって困ったものですが)
Android4.xの場合でもサポートライブラリやandroixを使うことで一応はベクタードローアブルに対応することができます。例えばコードを書いてベクタードローアブルを任意Viewの背景に割り当てるのこんな感じ(Android4.xに対応)。
1 2 3 4 5 6 7 |
// Vector画像を利用できるように設定(これはアプリ内で最初に1回だけ呼べばいい) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); ... // ContextCompatを使って読み込み final Drawable bk = ContextCompat.getDrawable(getContext(), ${ベクタードローアブルのリソースID}); // 背景に割り当て ${背景を割り当てたいView}.setBackground(bk); |
でもいちいちコード書かなきゃいけないなんてなんかスマートじゃないよね? レイアウトxmlで色々設定したいですよね?
サポートライブラリまたはandroidxを使っていれば、ImageView系(実際にはAppCompatImageViewとその子クラス)では、今までは android:src で指定していたところを app:srcCompat にすることでAndroid4.xでも勝手にベクタードローアブルを読み込めるようになります。またTextView系のCompoundDrawbalesもStateListDrawable等でベクタードローアブルをラップしてあげることで正常に動作します。
しかしですよ、AppCompat系も含めたViewにレイアウトxmlで背景にVectorDrawableを割り当てて、Android4.xで実行しようとするといきなり頓挫します。
さきほどはコードで設定しましたが、代わりにViewの背景をレイアウトxmlファイルで設定する場合、通常であれば御存知の通り、
android:background 属性にドローアブルリソースを割り当てます。
Android5(API21)以上であれば割り当てたドローアブルがベクタードローアブルであっても問題なく実行できます。でもAPI21未満の場合Viewのinflate中にそんなリソースはないんじゃぁおらぁ〜と言われてクラッシュします、100%ですチクセゥ〜
TextView系のCompoundDrawbalesと同じ様にStateListDrawable等でベクタードローアブルをラップしてみても
AppCompatDelegate#setCompatVectorFromResourcesEnabled(true) にしてもだめです。
コードを書けばAndroid4.xでもベクタードローアブルを読み込めるんだから、例えばAppCompatActivityの Context#getResources を上書きして Context#getResources#getDrawable でドローアブルを読み込むときにvectorタグならVectorDrawableCompatに差し替えてくれりゃいいのにと思ってみたり。
あるいは、ImageView系の app:srcCompat と同じように app:backgroundCompat なんてものがあって勝手にベクタードローアブルを読み込んでくれればいいのにと思うのですが、そんな都合のいいものはありません。
本当は各View側を修正するのではなく、ContextとかResourcesをらっぷしてゴニョゴニョするだけですめばいいのですが、ちと面倒そうなのでとりあえず自前のカスタムViewだけでも割り当てることができるようにします。まぁぶっちゃけ、先に書いた app:backgroundCompat を実現しようってことです。
ここで大事なことを1つ。androidネームスペースの属性( android:background とか)に対してVectorDrawableを割り当てるとAndroid5(API21)未満では(大本のViewクラスのコンストラクタ内で)必ずクラッシュします。なのでレイアウトxml内でベクタードローアブルを割り当ててAndroid4.xで実行する場合は必ずカスタム属性の定義が必要です。
ということでカスタム属性を定義します。まぁこんな感じですかね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?xml version="1.0" encoding="utf-8"?> <resources> ... <attr name="drawableCompat" format="reference" /> <attr name="backgroundCompat" format="reference" /> <attr name="backgroundTintCompat" format="color" /> ... <declare-styleable name="ProgressView"> <!-- 背景用のDrawable --> <attr name="backgroundCompat" /> <!-- 背景用Drawableのtint指定 --> <attr name="backgroundTintCompat" /> <!-- プログレス表示用のDrawable --> <attr name="drawableCompat" /> ... </declare-styleable> </resources> |
ここではとりあえず、
drawableCompat ,
backgroundCompat ,
backgroundTintCompat の3つのカスタム属性を定義しています(意味や目的意図は読んで字のごとくなので察してくだされ)。
レイアウトxml内で設定する際にはそれぞれ
app:drawableCompat ,
app:backgroundCompat ,
app:backgroundTintCompat としてアクセスします。
カスタム属性は該当Viewのコンストラクタで TypedArray としてアクセスします。
1 2 3 4 5 6 7 8 9 |
... public ProgressView(final Context context, final AttributeSet attrs, final int defStyleAttr) { super(context, attrs, defStyleAttr); final TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.ProgressView, defStyleAttr, 0); ... a.recycle(); } ... |
ここで、Android5(API21)以上またはベクタードローアブルを使うことを考えなければ、カスタム属性からのドローアブルの取得は例えば次のようになります。
1 2 3 |
... mDrawable = a.getDrawable(R.styleable.ProgressView_drawableCompat); ... |
しかぁ〜し、Android4.xで app:drawableCompat 属性にVectorDrawableを割り当てていると先程のコードは android.content.res.Resources$NotFoundException 例外でクラッシュします。ですので少し面倒ですが、例えば次のようにしないといけません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... static { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // API21未満の場合でもVectorDrawableを読み込めるように設定します AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } } ... public ProgressView(final Context context, final AttributeSet attrs, final int defStyleAttr) { ... final int drawableId = a.getResourceId(R.styleable.ProgressView_drawableCompat, 0); if (drawableId != 0) { mDrawable = ContextCompat.getDrawable(context, drawableId); } ... |
ちなみに、 ContextCompat#getDrawable(Context, int) はAPI16以上では Context#getResources()#getDrawable(int) と等価です。
ということでレイアウトxmlで設定したVectorDrawableをAndroid4.xでも読み込めるようになりました(VectorDrawableCompatとして読み込まれます)\(^o^)/
ひゃっほぉ〜と言いたいところなのですが、実はまだちょっと足りません。何が足りないかというとtintです。VectorDrawableの一般的な使い方ではfillColorを#FF000000(黒)に設定しておいて、使うときにtintで色を指定するようにします。また従来のpngからのBitmapDrawableやShapeDrawableでもtintで色を変更できると便利です。
Android5未満では Drawable#setColorFilter , Android5以上では Drawable#setTint を使えばいいのですが、わざわざ自分でオレオレ実装しなくともサポートライブラリ/androidxには DrawableCompat#setTint という便利クラス/メソッドがあるのでそれを使いたいと思います。
DrawableCompatでのtint処理は簡単で、次のようにすると好きな色を付けることができます(PorterDuff.Modeは用途に応じて適時変更してくだされ)。
1 2 |
DrawableCompat.setTint(mDrawable, ${好きな色}); DrawableCompat.setTintMode(mDrawable, PorterDuff.Mode.SRC_IN); |
しかし、実はここにも落とし穴があるのです? DrawableComat#setTint の実装を見てみると一目瞭然です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Specifies a tint for {@code drawable}. * * @param drawable The Drawable against which to invoke the method. * @param tint Color to use for tinting this drawable */ public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) { if (Build.VERSION.SDK_INT >= 21) { drawable.setTint(tint); } else if (drawable instanceof TintAwareDrawable) { ((TintAwareDrawable) drawable).setTint(tint); } } |
API21以上だと Drawable#setTint を呼び出すだけですので問題ないですよね、しかしAPI21未満の場合はDrawableがTintAwareDrawableインターフェースを実装していない場合には何も処理が行われないのです。VectorDrawableCompatであればTintAwareDrawableインターフェースを実装しているので問題ないですが、他のドローアブルではTintAwareDrawableインターフェースを実装していないものもたくさんありますので、先程のドロー炙る読み込み処理だけでは思ったとおりに色がつかにゃぁーということになってしまいます?
そんなあなたに強い味方、DrawableCompatです。 DrawableCompat#wrap を使うとTintAwareDrawableインターフェースを実装していないドローアブルでもラップして良きにはからってくれるのです✨
ということで、先程のドローアブルの読み込み処理は次のようにしましょう。
1 2 3 4 |
final int drawableId = a.getResourceId(R.styleable.ProgressView_drawableCompat, 0); if (drawableId != 0) { mDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, drawableId)); } |
これでどんなドローアブルを設定してもtintを適用できるようになります、やったね?
同じ様にAndroid4.xで背景にもtint付きでベクタードローアブル(他のドローアブルも可)を適用できるように読み込みます。
1 2 3 4 5 6 7 8 9 10 |
final int bkDrawableId = a.getResourceId(R.styleable.ProgressView_backgroundCompat, 0); if (bkDrawableId != 0) { final Drawable bk = DrawableCompat.wrap(ContextCompat.getDrawable(context, bkDrawableId)); final int bkTintColor = a.getColor(R.styleable.ProgressView_backgroundTintCompat, 0); if (bkTintColor != 0) { DrawableCompat.setTint(bk, bkTintColor); DrawableCompat.setTintMode(bk, PorterDuff.Mode.SRC_IN); } setBackground(bk); } |
Android5(API21)以上では android:background 属性と android:backgroundTint 使えば済むことですが、Android4.x対応 & VectorDrawable対応をするにはこんなことをすることになります。
これで本来はおしまいなのですが、今のVectorDrawable/VectorDrawableCompatはですねぇ、
Drawable#setTint でも
DrawableComat#setTint でも正常にtintが当たらないのんですよ(機種依存する)
原因は昔から頻発するあれです、アレ。
なのでtint適用時にVectorDrawable/VectorDrawableCompatのときの対策を追加しておきます。
1 2 3 4 5 6 7 8 9 10 |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (mDrawable instanceof VectorDrawable) { setLayerType(View.LAYER_TYPE_SOFTWARE, null/*Paint*/); } } else if (mDrawable instanceof VectorDrawableCompat) { setLayerType(View.LAYER_TYPE_SOFTWARE, null/*Paint*/); } DrawableCompat.setTint(mDrawable, ${好きな色}); DrawableCompat.setTintMode(mDrawable, PorterDuff.Mode.SRC_IN); |
ということで、Android4.xでレイアウトxml内でカスタムViewにベクタードローアブルを割り当てるには、
- カスタム属性を使ってDrawableの割り当て設定をする
- TypedArray#getDrawableは使っちゃだめ
- Android5(API21)未満ではAppCompatDelegate.setCompatVectorFromResourcesEnabled(true)を呼んでおく
- TypedArray#getResourceIdを使って割り当てたDrawableのリソースIDを取得してResources#getDrawable(id)でDrawableを読み込む
- tint処理が必要であればDrawableCompat.wrapでラップしておく
- VectorDrawableとVectorDrawableCompatはtintが正常に適用できない端末があるのでハードウエアアクセレレーションを無効にする
でした✌
ちゃんちゃん
(^^)/~~~