Androidアプリケーション開発において、白色のBitmapを描画した際に、他の色では問題なく表示されるにもかかわらず、白色だけが期待通りに表示されず黒く反転してしまうという現象に遭遇することがあります。
問題の背景と基本的な描画コード
以下は、カスタムView内で白色のBitmapを作成し、画面に描画する基本的な実装例です。このコードでは、Viewの測定されたサイズに基づいて、内部で白色のBitmapを生成し、それをCanvasに描画しています。
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class CustomBitmapView extends View {
private Bitmap mDisplayBitmap;
private Paint mDrawingPaint;
public CustomBitmapView(Context context) {
this(context, null);
}
public CustomBitmapView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomBitmapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeView();
}
/**
* Viewの初期設定を行います。
*/
private void initializeView() {
mDrawingPaint = new Paint(Paint.DITHER_FLAG);
// 通常は描画フィルターなどに使用されるが、ここでは単純に白色を設定
mDrawingPaint.setColor(Color.WHITE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// ビューのサイズが確定したらBitmapを生成、またはサイズ変更時に再生成
if (mDisplayBitmap == null || mDisplayBitmap.getWidth() != getMeasuredWidth() || mDisplayBitmap.getHeight() != getMeasuredHeight()) {
mDisplayBitmap = createFilledBitmap(getMeasuredWidth(), getMeasuredHeight(), Color.WHITE);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDisplayBitmap != null) {
// 生成したBitmapをCanvasに描画
canvas.drawBitmap(mDisplayBitmap, 0, 0, mDrawingPaint);
}
}
/**
* 指定された幅、高さ、色で塗りつぶされたBitmapを生成します。
* @param targetWidth 生成するBitmapの幅
* @param targetHeight 生成するBitmapの高さ
* @param fillColor Bitmapを塗りつぶす色
* @return 塗りつぶされたBitmapインスタンス
*/
private Bitmap createFilledBitmap(int targetWidth, int targetHeight, int fillColor) {
Bitmap outputBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
outputBitmap.eraseColor(fillColor); // 指定色でBitmap全体を塗りつぶす
return outputBitmap;
}
}
このコードをAndroidデバイスで実行した際に、システムのダークモード(またはナイトモード)が有効になっていると、上記の白色のBitmapが黒く表示されることがあります。これはAndroid Q (API 29) 以降に導入された「Force Dark(強制ダーク)」機能による挙動です。アプリが明示的にダークテーマをサポートしていない場合、システムはUIの視認性を高めるために、自動的にライトテーマの色をダークテーマに合うように変換しようとします。この変換プロセスの一環として、白色が黒色、または非常に濃い色に反転されることがあります。
解決策:アプリの強制ダークモード適用を無効にする
この問題を回避するためには、システムによるアプリへの強制ダークモード適用を無効にする必要があります。
1. コードによる対応
Viewのコンストラクタ、またはActivityのライフサイクルメソッド(例: onCreate)で、システム設定に追従しないように設定を追加します。
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
public class CustomBitmapView extends View {
private Bitmap mDisplayBitmap;
private Paint mDrawingPaint;
public CustomBitmapView(Context context) {
this(context, null);
}
public CustomBitmapView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomBitmapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeView();
// ここから強制ダークモード適用対策
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android Q (API 29) 以降での「Force Dark」をこのViewに対して無効にする
setForceDarkAllowed(false);
}
// アプリ全体のナイトモード設定を上書きし、ライトモードに固定する
// 通常はApplicationクラスやActivityクラスのonCreateで設定するのが推奨される
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
/**
* Viewの初期設定を行います。
*/
private void initializeView() {
mDrawingPaint = new Paint(Paint.DITHER_FLAG);
mDrawingPaint.setColor(Color.WHITE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mDisplayBitmap == null || mDisplayBitmap.getWidth() != getMeasuredWidth() || mDisplayBitmap.getHeight() != getMeasuredHeight()) {
mDisplayBitmap = createFilledBitmap(getMeasuredWidth(), getMeasuredHeight(), Color.WHITE);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDisplayBitmap != null) {
canvas.drawBitmap(mDisplayBitmap, 0, 0, mDrawingPaint);
}
}
private Bitmap createFilledBitmap(int targetWidth, int targetHeight, int fillColor) {
Bitmap outputBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
outputBitmap.eraseColor(fillColor);
return outputBitmap;
}
}
setForceDarkAllowed(false)メソッドは、Android Q (API 29) 以降で「Force Dark」を特定のViewに対して無効にするために使用されます。また、AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)は、アプリ全体でナイトモード(ダークモード)の挙動を制御し、強制的にライトモードで動作させるための設定です。この設定は通常、アプリケーションクラスやメインのActivityクラスのonCreateメソッドで一度だけ呼び出すことが推奨されます。
2. テーマファイル (themes.xml) による対応
アプリ全体でダークモードの強制適用を無効にしたい場合は、アプリケーションのテーマ設定をthemes.xmlファイルで変更するのが一般的で効果的な方法です。
まず、アプリのベーステーマがTheme.AppCompat.Lightなどのライトテーマを継承していることを確認します。その上で、以下のアイテムをテーマに追加します。
<!-- res/values/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- アプリケーションの基本テーマ -->
<style name="Theme.YourAppName" parent="Theme.AppCompat.Light.NoActionBar">
<!-- その他のテーマカスタマイズ項目 -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Android Q (API 29) 以降でForce Darkを無効にする設定 -->
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
</style>
</resources>
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>をアプリのテーマに追加することで、システムはこのテーマを使用するActivityやViewに対して「Force Dark」を適用しなくなります。これにより、白色のBitmapが意図せず黒く表示される問題を根本的に解決できます。