読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Android の Canvas#saveLayer メソッドと xfermode について

Android アプリ

Android アプリ開発に関して Canvas クラスの saveLayer メソッドPaint の xfermode について調べたのでまとめておきます。

Canvas#saveLayer メソッド

saveLayer メソッドのドキュメントには、『This behaves the same as save(), but in addition it allocates and redirects drawing to an offscreen bitmap.』 と書かれています。

  • save メソッドと基本的には同じ。
  • 異なる点は、キャンバス外 (offscreen) のビットマップを用意し、以降の描画処理をそちらにリダイレクトするようにする、ということ。

save メソッドは何をするのか

じゃあ save メソッドは何をするのか調べましょう。 save メソッドのドキュメントには、『Saves the current matrix and clip onto a private stack. Subsequent calls to translate, scale, rotate, skew, concat or clipRect, clipPath will all operate as usual, but when the balancing call to restore() is made, those calls will be forgotten, and the settings that existed before the save() will be reinstated.』 と書かれています。

  • 呼びだし時点の座標変換行列とクリッピングの設定を保存する。
  • それ以降に、座標変換行列を変化させるメソッド (translatescale など) やクリッピング設定を変化させるメソッド (clipRectclipPath) が呼ばれると通常通り適用される。
  • 対応する restore メソッドが呼ばれると save メソッド呼び出し前の状態に戻される。

つまり、描画される位置を決定するための情報が保存され、あとから復元することができるようになる、という感じですね。 描画されているビットマップの情報が保存されるわけではないので注意しましょう。

ちなみに、save(int) メソッドを使い、引数として Canvas.MATRIX_SAVE_FLAGCanvas.CLIP_SAVE_FLAG を渡すことで、保存・復元の対象を座標変換行列だけにしたり、クリッピングの設定だけにしたりできます。 (が、両方を保存・復元の対象にする方が単純で速いので、できるだけ使わない方がいいみたいです。)

何度も save した後、指定のところまで一気に復元する

save メソッドを何度も呼び出した場合、restore メソッドを同じ回数呼び出すことで元の状態に戻せます。 しかし何度も呼ぶのは面倒ですね。 save 時の返り値を保持しておき、復元時にその値を restoreToCount メソッドに渡すと、その状態まで一気に復元できます。

// 返り値を保持しておく。
final int sc = canvas.save(); // 位置 A

/* ... ここで何度も save メソッドを呼ぶ。 ... */

// 途中で何度 save メソッドを呼んでいたとしても、位置 A のときの状態まで復元される。
canvas.restoreToCount(sc);

キャンバス外のビットマップに描かれたものはどこへ?

さて、saveLayer メソッドの話に戻りましょう。 save レイヤーと同じように座標変換行列とクリッピングの設定の保存ができることについては説明は不要だと思います。

問題は、saveLayer メソッド呼び出し後の描画がキャンバス外のビットマップにリダイレクトされて、最終的にそのビットマップに描かれたものがどうなるのか、です。 メソッドの説明には次のように書かれています。

Only when the balancing call to restore() is made, is that offscreen buffer drawn back to the current target of the Canvas (either the screen, it's target Bitmap, or the previous layer).

restore メソッドの呼び出しが行われて初めてターゲットとなるキャンバス (スクリーンだったり、Bitmap だったり、より前に作られたレイヤーだったりする) に描き戻されるわけですね!

引数の Paint オブジェクトの属性は描き戻し時に適用される

saveLayer メソッドは引数として Paint オブジェクトを受け取ります。 この Paint オブジェクトの属性のアルファ値と xfermode、そして ColorFilterstore メソッドが呼ばれて描き戻される際に適用されます。 Xfermode については下で説明します。

Xfermode と PorterDuffXfermode

Xfermode について

Xfermode クラスのドキュメントには 『Xfermode is the base class for objects that are called to implement custom "transfer-modes" in the drawing pipeline.』 と書かれています。 「xfermode」 は transfer-mode を表すみたいですね。 (これが世間一般での命名なのか Android の世界だけの命名なのかよくわかりません。)

ちゃんとドキュメントには書かれていませんが、2 つの画像を合成する場合や、既に何かが描かれているところに新たに描画する際に、どのように合成するのかを表すもののようです。

PorterDuffXfermode

昔 Porter さんと Duff さんが画像合成の 12 通りのルールを論文にしたそうで、それが Porter-Duff ルールと呼ばれているそうです。

具体的にどういうものかは上のページを見るとわかりやすいです。 例となる画像があります。

これらのルールは、Android SDK では PorterDuffXfermode クラスで表現されます。 (Android SDK では 12 通り以上のモードが定義されているので、Porter-Duff ルールに含まれないものも入っているのかも?)

例えば、次のページに書かれているように PorterDuff.Mode.CLEAR を使うことで対象となるキャンバスの内容 (destination) も描くもの (source) も両方消去することができます (つまり消しゴムにできる)。

ちなみに、ImageView#onDraw メソッドなどに渡されてくる Canvas オブジェクトは背景が透明ではないので、透明にしようとしても透明にならないようです。 (真っ黒になった。)

saveLayer メソッドと xfermode のサンプルコード

赤い四角をもとのキャンバス (destination) に描き、青い円を新しく用意したレイヤー (source) に描き、Porter-Duff の Overlay モードで合成するサンプルコードです。

// 必要な import 文。
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;

// 使用する Paint オブジェクトの用意。
Paint redPaint = new Paint();
redPaint.setAntiAlias(true);
redPaint.setColor(getResources().getColor(android.R.color.holo_red_light));
Paint bluePaint = new Paint();
bluePaint.setAntiAlias(true);
bluePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
Paint xfermodePaint = new Paint();
xfermodePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.OVERLAY));
// Canvas 用意。
Bitmap bm = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
bm.setDensity(DisplayMetrics.DENSITY_XXHIGH);
Canvas c = new Canvas(bm);
// 四角を描画。
c.drawRect(0, 0, 210, 210, redPaint);
// 新しいレイヤーを準備。
int sc = c.saveLayer(null, xfermodePaint, Canvas.CLIP_TO_LAYER_SAVE_FLAG);
// 新しいレイヤーに円を描画。
c.drawCircle(180, 180, 120, bluePaint);
// 新しいレイヤーに描かれたものを描き戻す。
// (saveLayer 時に指定した xfermodePaint の xfermode である Porter-Duff の OVERLAY モードで。)
c.restoreToCount(sc);
// 表示してみる。 (imageView は android.widget.ImageView オブジェクト。)
imageView.setImageBitmap(bm);

結果は以下の画像のようになります。

f:id:nobuoka:20150612002459p:plain

DST_ATOPSRC_ATOPSRC_OUT などを使うとマスク処理みたいなこともできますし、面白いですね。

備考

  • Xfermode を指定して画像の合成をする方法はいろいろあって、Canvas#saveLayer メソッドを使う必要は必ずしもありません。