Edited at

アダプティブアイコン画像がOreoで表示されなかったので対応を考えた


概要

Android 8でアプリのアイコン画像を表示しようとしたところ、表示されませんでした。理由は、Oreoで導入されたアダプティブアイコンを、Bitmapオブジェクトに変換できなかったためでした。ここでは、その対策についていくつかまとめてみました。


事象

確認した動作環境は、Android 8.1、ビルド時のtargetSdkVersionは27です。

ここでは、"test_channel"というIDをもつNotificationChannelが、すでに作成されているものとします。

AndroidStudioで新規にプロジェクトを作成し、Notificationを作成する際、以下のようにNotificationを作成したところ、Android 8環境で通知のLargeIconに画像が表示されませんでした。Android 8未満では表示されます。

なお、R.mipmap.ic_launcherは、アプリ用のランチャアイコンとして利用しているリソースとなります。

Notification notification = new NotificationCompat.Builder(context, "test_channel")

.setContentTitle("Title")
.setContentText("Message")
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
.build();

表示された通知内容

本来表示して欲しい通知内容


原因

BitmapFactory.decodeResourceメソッドが、アダプティブアイコンに対応しておらず、nullを返すためです。

Android8から導入されたアダプティブアイコンに真面目に対応すると、リソースの構成は以下のようになると思います。少なくとも、Android Studio 3.2で作成されたプロジェクトはこのような構成になります。

ic_launcher.pngはAndroid 8未満で使用されるアイコン用のラスタ画像リソース、ic_launcher.xmlはAndroid 8以降で使用されるアダプティブアイコン用のXMLリソースになります。AndroidManifest.xmlにリソース指定する関係上、この2種類のファイルは同一のファイル名になるかと思います。その結果、Android8未満と8以上で、参照されるリソースが異なるため、問題の事象が起こります。

確認のため、以下のようなコードで、リソースからDrawableオブジェクトを作成してみます。

Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.mipmap.ic_launcher, null);

すると、Android 8未満ではBitmapDrawableが、8以降ではAdaptiveIconDrawableが返ってくるのがわかります。ちなみに、API 21以降でベクタ画像を使用している場合は、VectorDrawableが返ってくるかと思います。


対策

旧アイコンリソースであるic_launcher.pngを直接参照する方法があれば話は簡単だったのですが、そういう手段は見つからなかったので、恐らく次の何れかの対策になるかと思います。


1. アイコン画像を別途用意する

別ファイル名のアイコン用画像リソースを用意します。とても単純かつ、実直なやり方です。普通はこれで良いと思います。アイコン画像の変更漏れが心配だったり、データの2重管理絶対に許すまじ、という思想の場合は向かないと思いますが。


2. Bitmapに変換する

アダプティブアイコンをBitmapに変換します。具体的には、以下のようなメソッドを用意し、AdaptiveIconをBitmapに変換します。なお、アイコンにベクタ画像を設定している場合は、別途VectorDrawable向けの処理が必要かと思います。

参考

Convert AdaptiveIconDrawable to Bitmap in Android O preview - Stack Overflow

https://stackoverflow.com/questions/44447056/convert-adaptiveicondrawable-to-bitmap-in-android-o-preview

@RequiresApi(api = Build.VERSION_CODES.O)

public static Bitmap getAppIcon(PackageManager mPackageManager, String packageName) {

try {
Drawable drawable = mPackageManager.getApplicationIcon(packageName);

if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
} else if (drawable instanceof AdaptiveIconDrawable) {
Drawable backgroundDr = ((AdaptiveIconDrawable) drawable).getBackground();
Drawable foregroundDr = ((AdaptiveIconDrawable) drawable).getForeground();

Drawable[] drr = new Drawable[2];
drr[0] = backgroundDr;
drr[1] = foregroundDr;

LayerDrawable layerDrawable = new LayerDrawable(drr);

int width = layerDrawable.getIntrinsicWidth();
int height = layerDrawable.getIntrinsicHeight();

Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(bitmap);

layerDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
layerDrawable.draw(canvas);

return bitmap;
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}

return null;
}

毎回変換処理をしないように、アイコン画像はどこかに保持しておきましょう。


3. foregroundの画像を使う

これは、利用できる場合と利用できない場合があります。

アダプティブアイコンを設定するic_launcher.xmlは、具体的には以下のような内容になっています。

<?xml version="1.0" encoding="utf-8"?>

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

実際の表示は、backgroundとforegroundの2種類のリソースを重ね合わせて構成します。リソースには、ラスタ画像、ベクタ画像、単色などが指定できます。foregroundに従来のラスタ画像を指定し、LargeIconの画像としてic_launcherではなく、そのラスタ画像を指定します。

この方法の欠点は、foregroundがラスタ画像以外の場合は使えないこと(実際、Android Studioでプロジェクト作成した直後のサンプルはベクタ画像になっています)と、アイコンデザインの関係上、foregroundが必ずしも適切な画像になっていないことです。例えば、foregroundが文字だけで、backgroundの背景と重ね合わせて初めて本来のデザインとなるアイコンもあるかと思いますが、その場合は文字だけしか表示されません。


4. Iconオブジェクトを使う

NotificationCompat.Builderではなく、Notification.Builderを使用する人向けの方法です。Notification.Builderには、LargeIconの設定にIconオブジェクトを利用することができます。以下のコードにより、アダプティブアイコンもIconオブジェクトとして利用することができるため、簡単にLargeIconとして利用することができます。

Icon icon = Icon.createWithResource(this, R.mipmap.ic_launcher);

IconオブジェクトはAPI 23以降の対応となるので、古いバージョンをサポートする場合は別途対応が必要になります。


出力の違い

挙げた対策のうち3つについて実際に出力させると、表示されるアイコンも微妙に異なっていることがわかります。参考までに。

1 アイコン画像を別途用意する

2 Bitmapに変換する

4 Iconオブジェクトを使う


まとめ

とりあえず、 アダプティブアイコンの描画について思いつく限りでまとめてみましたが、他に何か対応策があれば教えていただけると嬉しいです。

Android Oreoで導入されたアダプティブアイコンは、思ったよりも厄介な代物でした。しかし、これに対応しないとランチャアイコンが、白枠のついた小さな画像(環境・設定による)で表示されてしまうという嫌がらせ(仕様)を受けるので、単純に無視するわけにもいきません。しかし、対応すると今度はアプリ内におけるアイコンの表示に面倒が出てきてしまい、新手のいじめかと思いました。

加えて、NotificationのSmallIconの方は、アダプティブアイコンのリソースID指定でも表示されるというのが実に嫌らしい…。

今回事象として挙げたNotificationへのアイコン表示は、思った通りにアイコンが出なくて、結局1時間以上悩んでいたと思います。そのような無駄な時間を過ごす人が一人でも減るように、今回このような記事を作成してみた次第です。

余談ですが、BitmapFactory.decodeResourceメソッドは、デコードに失敗すると例外は出さずにnullを返すため、アプリによってはAndorid 8対応の改造によって、アイコンが表示されなくなったことに気づかずに放置されているアプリがあるかもしれません。もし心当たりがある場合は、一度アプリを見直してみてはいかがでしょうか。