この記事は?
Android O 向けビルド(targetSdkVersion>=26)で Canvas.setBitmap() の挙動が少し変わりました。
Canvas.setBitmap() を使って Canvas に Bitmap をセットすると、それ以前に実行した Canvas.translate() が効力を失います。また、Canvas.scale() なども同様です。
より正確に言うと、setBitmap() によって Canvas にセットした変換行列がリセットされるようになりました。
この点についてサンプルアプリを作成し、実際の挙動を確認して記事にまとめました。
また、挙動が変わる原因となった Android 側の修正についても解説しています。
まとめ
- Android O 未満向けビルド(targetSdkVersion<26)の場合
- Canvas.setBitmap() で Bitmap をセットしても、それ以前の translate() の値は保持されていた
- すなわち Canvas にセットした変換行列はリセットされなかった
- Android O 向けビルド(targetSdkVersion>=26)の場合
- Canvas.setBitmap() で Bitmap をセットすると、translate() の値は破棄される
- すなわち Canvas にセットした変換行列がリセットされるようになった
- この挙動の変更は 2017/11/28 時点で Apps targeting Android 8.0 では特に触れられていません。
この挙動はすべて Android 8.0.0 以上の OS で動作させた場合のものです。
実際の挙動
アプリ内にある画像を Bitmap にして Canvas を使って並行移動するアプリを作成しました(CanvasSetBitmapTester)。
[Translate] というボタンを押すと画像を右下に並行移動するアプリです。

これを使って Android O 向けにビルドした場合と、そうでない場合で挙動を比較しています。
端末
- Nexus 5X, Android 8.0.0
コード
Canvas を使って Bitmap を変換する部分のコードは次のとおりです。
private Bitmap translateImage(@DrawableRes int targetId) {
Bitmap src = BitmapFactory.decodeResource(getResources(), targetId);
Bitmap dst = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
// 1. translate() を実行
// 2. setBitmap() で書き込み用の Bitmap をセット
// 3. drawBitmap() で書き込み
canvas.translate(200, 200);
canvas.setBitmap(dst);
canvas.drawBitmap(src, 0, 0, new Paint());
return dst;
}
ポイントは setBitmap() の前に translate() している点です。
結果
targetSdkVersion<26 の場合
Android O 未満向けビルドでは build.gradle の targetSdkVersion を 25 にしています。
compileSdkVersion は 26 のままであることに注意が必要です。
android {
compileSdkVersion 26
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "com.hjm.canvassetbitmaptester"
minSdkVersion 19
targetSdkVersion 25 // 25 にする
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
...
}
この条件でビルドを行うと、[Translate] を押すと、右下に 200px だけ画像が平行移動します。
押す前

押した後

Android O 未満向けビルドでは setBitmap() 前に実行した translate() が有効であると分かります。
targetSdkVersion>=26
Android O 未満向けアプリでは次のように targetSdk を 26 にしています。
android {
compileSdkVersion 26
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "com.hjm.canvassetbitmaptester"
minSdkVersion 19
targetSdkVersion 26 // 変更
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
...
}
この条件でビルドを行うと、[Translate] を押しても画像が右下に平行移動しません。
押す前

押した後

Android O 向けビルドでは setBitmap() の前に行った translate() が無効になっていることが分かります。
修正
問題は setBitmap() すると変換行列がリセットされてしまうです。
そのため setBitmap() のあとに translate() を呼ぶことでこの問題は解決します
private Bitmap translateImage(@DrawableRes int targetId) {
Bitmap src = BitmapFactory.decodeResource(getResources(), targetId);
Bitmap dst = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
// 1. setBitmap() で書き込み用の Bitmap をセット
// 2. translate() を実施
// 3. drawBitmap() で書き込み
canvas.setBitmap(dst);
canvas.translate(200, 200);
canvas.drawBitmap(src, 0, 0, new Paint());
return dst;
}
ただし、ここまでくると setBitmap() を使わずに new Canvas() に Bitmap を渡してしまうのが正解だと思います。
解説
Canvas.setBitmap()
Canvas のソースコードを見ながらこのような挙動の違いが発生する原因を解説します。
まず、Canvas.setBitmap() の Javadoc には次のように書かれています。
/**
* Specify a bitmap for the canvas to draw into. All canvas state such as
* layers, filters, and the save/restore stack are reset. Additionally,
* the canvas' target density is updated to match that of the bitmap.
*
* Prior to API level {@value Build.VERSION_CODES#O} the current matrix and
* clip stack were preserved.
*
* @param bitmap Specifies a mutable bitmap for the canvas to draw into.
* @see #setDensity(int)
* @see #getDensity()
*/
Prior to API level Build.VERSION_CODES#O the current matrix and clip stack were preserved.
と書かれています。ここから、この挙動の違いはバグではなく Android として意図したものであることが分かります。
次に、実際にコードを見てみます。
public void setBitmap(@Nullable Bitmap bitmap) {
if (isHardwareAccelerated()) {
throw new RuntimeException("Can't set a bitmap device on a HW accelerated canvas");
}
Matrix preservedMatrix = null;
if (bitmap != null && sCompatibilitySetBitmap) { // (1)
preservedMatrix = getMatrix();
}
// (2)
if (bitmap == null) {
nSetBitmap(mNativeCanvasWrapper, null);
mDensity = Bitmap.DENSITY_NONE;
} else {
if (!bitmap.isMutable()) {
throw new IllegalStateException();
}
throwIfCannotDraw(bitmap);
nSetBitmap(mNativeCanvasWrapper, bitmap);
mDensity = bitmap.mDensity;
}
// (3)
if (preservedMatrix != null) {
setMatrix(preservedMatrix);
}
mBitmap = bitmap;
}
(1) の部分で sCompatibilitySetBitmap が true の場合、ローカル変数(preservedMatrix)に行列を保持しています。(2) で実際に Bitmap をセットする処理を native 側で行ったあと、(3) で prevervedMatrix に値があったら行列を元に戻しています。
sComaptibilitySetBitmap が true の時だけ setBitmap() 前の行列の値を保持し復元していることが見てとれます。
sComatibilitySetBitmap
最後に処理の分岐に使われている sComatibilitySetBitmap の値を決めるコードを見てみます。
この値は View.java のコンストラクタで次のように決定されています。
final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
...
Canvas.sCompatibilitySetBitmap = targetSdkVersion < Build.VERSION_CODES.O;
まさに targetSdkVersion の値によって、setBitmap() の挙動を変えていることが分かります。
小話
Bitmap.createBitmap() への影響
今回のサンプルアプリでは Canvas を使って Bitmap を変換するという処理を行っています。
実は Bitmap クラスに同じようなことをする static ファクトリーメソッドがあります。
ソースとなる Bitmap と、変換行列を渡すことで変換後の Bitmap を返してくれるメソッドです。
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height, @Nullable Matrix m, boolean filter)
実はこのメソッドも内部的には Canvas を使っています。
...
Canvas canvas = new Canvas(bitmap);
canvas.translate(-deviceR.left, -deviceR.top);
canvas.concat(m);
canvas.drawBitmap(source, srcR, dstR, paint);
canvas.setBitmap(null);
...
実はこのメソッドも今回の挙動の変更に影響を受けていました。
このファイルのコミット履歴を見ると、今回の挙動の変更に対して修正を入れていることが分かります。
translate() などの処理をする前にコンストラクタで Bitmap をセットするように修正されています。
Modify createBitmap w/ crop and matrix to not rely on Canvas.setBitmap
commit 29cd3e922612afff4cd5fa9694013e5e8ae93661
drWulf committed on 16 Mar
小話2:Android O プレビュー版での変更
この挙動の変更は Android O Developer Preview 2 で最初に加えられたようです。
しかし、その時点では targetSdkVersion をチェックせず、すべての targetSdkVersion で setBitmap()
によって変換行列がリセットするように修正されていました。
その後、Android O Developer Preview 3 にて targetSdkVersion によって挙動が変わるように変更されたようです。
一連の修正の痕跡は「コミット」⇒「リバート」⇒「再コミット」⇒「targetSdk チェックの追加」という形でコミットログから見ることができます。
Add an O-release targetAPI check for Canvas.setBitmap.
drWulf committed on 6 MayChange behavior of setBitmap to cleanly reset the canvas
drWulf committed on 12 AprRevert "Change behavior of setBitmap to cleanly reset the canvas - id…
Tony Mantler committed on 15 MarChange behavior of setBitmap to cleanly reset the canvas
reed-at-google committed on 2 Mar
おそらくは Developer Preview でのレビューで影響をうけるアプリが多数あり、一度リバートして targetSdkVersion をチェックするように再度修正されたのだと思われます。
(実際、Android の Bitmap.java も影響を受けていました)