AndroidのCanvasを使いこなす! - PorterDuff

有山 圭二
73

みなさんこんにちは。有山圭二です。

今年も、Google I/Oの季節がやってきました。

Google I/Oは、Google社がアメリカのサンフランシスコで開催する年次の開発者会議です。今年は5月28日と29日の二日間に予定されています。
基調講演ではGoogleの今後の方針や、新しいテクノロジー・プロダクトが発表されます。その他にも、各種テクノロジーを担当するGooglerに会って直に意見や要望を言えたり、I/Oに参加する世界中の開発者と意見が交換できるのも魅力の一つです。

今年のGoogle I/Oで何が発表されるのか、まだ不透明な部分が多いのが実際です。個人的には、Androidアプリ開発に使える新しい言語、例えばGolangやKotlinに正式対応するなどがあれば面白いなと思います。

ともあれ、この記事は引き続き、古くて新しいCanvasについてもっと知ろうと言う趣向でお送りします。
前回はCanvasの使い方全般を一通り解説したので、今回のテーマは予定通り"PorterDuff"です。

1.1 PorterDuffとは

PorterDuffは、2枚の画像を合成する際、それぞれのピクセルについてどのように処理するのか、操作の種類(Mode)ごとに実際の各ピクセルに対する処理内容を規定したものです。Canvasと同じく、AndroidのAPI Level 1の頃からあるとても古いAPIです。
PorterDuffを使うと、Canvasで画像や図形を描画する際、既に表示している画像の一部をくり抜いて下の表示内容を見えるようにしたり、四角い画像を角丸に整形したりできます。
なお、PorterDuffは、考案者であるThomas PorterとTom Duffのそれぞれの名前に由来します。

PorterDuffの使いどころ

Androidアプリを開発する上で、どのような場合にPorterDuffを使うのか。

例えば、カメラのプレビューにSurfaceViewをオーバーレイする際に「透明で塗りつぶす」と言う処理を実装したことはないでしょうか。
SurfaceViewで得られるCanvasは、前の描画がリセットされないので、必要に応じてCanvasを塗りつぶす必要があります。しかし、白色や黒色などで塗りつぶしてしまうと、SurfaceViewの下にあるカメラプレビューが見えなくなってしまいます。
この場合、PorterDuff.ModeのCLEARを使ってCanvasを透明に戻すという手法が一般的です(リスト1.1)。

リスト1.1: PorterDuff.Modeを指定している
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

ここでは実は、PorterDuff.ModeのCLEARを指定することで、描画色が有効な範囲(この場合はCanvas全体)を透明にする処理を実行しています。

なお、CanvasのdrawColorメソッドの第2引数を指定しないと、PorterDuff.ModeはSRC_OVERになります。つまり、ここで指定した色でキャンバスを塗りつぶすという意味になり、リスト1.1の場合、第2引数を省略するとCanvasは想定通りにはなりません。

その他にも、移動する枠で画像の一部を見せる。撮影した顔写真を用意したフレームの形に合うように切り出すなど、様々な場所でPorterDuffは活用できます。

規定された操作(モード)

Androidが用意しているPorterDuffのモードは、以下の通りです。

SRCとDST

PorterDuffは、2枚の画像を合成する処理する方法ですが、一部のモードでは2枚の画像をそれぞれSRCDSTに分けて考えるものがあります。

例えば、前述のリストで括弧書きで(SRC/DST)としているものがそうです。
これらはPorterDuff.Modeの定数でそれぞれ、SRC_OVER, DST_OVERのように分けられます。

DSTはDestination。つまり合成の大元になる画像です。
一方、SRCはSource。DSTに対して合成する画像です。

例えば、画像Aと画像Bを合成する場合、PorterDuffMode.SRC_OVER(A, B)と、PorterDuffMode.SRC_OVER(B, A)で得られる画像は、それぞれ異なり、PorterDuffMode.SRC_OVER(A, B)とPorterDuffMode.DST_OVER(B, A)の結果は同じになります。

1.2 PorterDuff.Modeの処理内容

各モードの処理内容は、表1.1に示すとおりです1)http://developer.android.com/reference/android/graphics/PorterDuff.Mode.html

表1.1: PorterDuff.Mode.
フラグ 処理内容 HW SW
CLEAR [0, 0]
SRC_OVER [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc]
DST_OVER [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]
SRC_IN [Sa * Da, Sc * Da]
DST_IN [Sa * Da, Sa * Dc]
SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
DST_OUT [Da * (1 - Sa), Dc * (1 - Sa)]
SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
DST_ATOP [Sa, Sa * Dc + Sc * (1 - Da)]
XOR [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
DARKEN [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] ×
LIGHTEN [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] ×
ADD Saturate(S + D)
MULTIPLY [Sa * Da, Sc * Dc]
SCREEN [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
OVERLAY ×

このテーブルだけ見ても筆者には何がどうなるかさっぱりわからないので、次からそれぞれのPorterDuffのモードについて、実例を挙げながら解説していきます。

1.3 PorterDuffモードの実例

SRCとDST

これから紹介する実例で、SRCとDSTそれぞれに指定する画像は図1.1に示す通りです。

図1.1: SRCに指定する画像(左)とDSTに指定する画像(右)
図1.1: DSTに指定する画像(左)とSRCに指定する画像(右)

今回は、SRCは青い丸。DSTは赤い十字の画像です。
それぞれ同じ大きさの画像で、黒枠内の格子模様は透明なピクセルを表します。

リスト1.2: PorterDuff.Modeの利用
int sc = canvas.saveLayer(x, y, x + W, y + H, paint, Canvas.MATRIX_SAVE_FLAG
                                        | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG
                                        | Canvas.CLIP_TO_LAYER_SAVE_FLAG
        );

        // DST
        canvas.drawBitmap(mCross, 0, 0, paint);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

        // SRC
        canvas.drawBitmap(mCircle, 0, 0, paint);

        paint.setXfermode(null);
        canvas.restoreToCount(sc);

リスト1.2は、PorterDuff.Modeをつかって2枚の画像を合成するサンプルソースです。
setXfermodeメソッドで、使用するPorterDuff.Modeを指定しています。
setXfermodeメソッドでPorterDuff.Modeを指定する前に描画している内容がDST(今回の場合は赤い十字)、setXfermodeメソッドの後に描画する内容がSRC(青い丸)として合成されます。

SRC(Source)という名前から、理解が混乱するかも知れませんが、ここを間違えると後々大変なので、注意してください。

CLEAR

CLEARは、DSTのピクセルを削除する(透明にする)モードです。

図1.2: 丸をDSTに指定した場合、十字と重なる部分のピクセルが透明になっている
図1.2: 丸をDSTに指定した場合、十字と重なる部分のピクセルが透明になっている - 本来の仕様とは異なる?
図1.3: 十字をDSTに指定した場合、丸と重なる部分のピクセルが透明になっている
図1.3: 十字をDSTに指定した場合、丸と重なる部分のピクセルが透明になっている - 本来の仕様とは異なる?

なお、PorterDuffのモードCLEARは、ハードウェア・アクセラレーションが有効の状態と無効な状態で挙動が異なるという現象が、筆者の手元で確認されています。

仕様上は、ハードウェア・アクセラレーションが無効な状態が正しい(ピクセルを全て透明にする)のですが、なぜこのような違いが発生しているのかについては、調査中です。

Android 4.0(API Level 14)以上では、ハードウェア・アクセラレーションは標準で有効になっている2)http://developer.android.com/guide/topics/graphics/hardware-accel.htmlます。
もし、ハードウェア・アクセラレーションを無効にしたい場合は、リスト1.3に示すとおり、個々のViewについて設定できます。

リスト1.3: ViewのコンストラクタでHardwareAccelerationを明示的に無効化する
setLayerType(View.LAYER_TYPE_SOFTWARE, mPaint);

OVER(SRC/DST)

OVERは、2枚の画像のどちらかをもう一方の上に重ねる(Over)モードです。
DST_OVERとSRC_OVERがあり、DSTとSRCどちらを上にするかを指定します。

図1.4: DST_OVER: 丸(DST)が十字(SRC)の上に重なっている
図1.4: DST_OVER: 丸(DST)が十字(SRC)の上に重なっている
図1.5: SRC_OVER: 十字(SRC)が丸(DST)の上に重なっている
図1.5: SRC_OVER: 十字(SRC)が丸(DST)の上に重なっている

IN(SRC/DST)

INは、2枚の画像の重なる部分だけを残してあとは透明にするモード(交差)です。
DST_INとSRC_INがあり、どちらの画像を主体にするのかを指定します。

図1.6: DST_IN: 丸(DST)から十字(SRC)の画像と交差しない部分を透明にしている
図1.6: DST_IN: 丸(DST)から十字(SRC)の画像と交差しない部分を透明にしている
図1.7: SRC_IN: 十字(SRC)から丸(DST)の画像と交差しない部分を透明にしている
図1.7: SRC_IN: 十字(SRC)から丸(DST)の画像と交差しない部分を透明にしている

OUT(SRC/DST)

OUTは、一方の画像からもう一方の画像の重なる部分を透明にするモード(型抜き)です。OUTを指定して全面塗りつぶしをすることで、CLEARと同じ働きをします。
DST_INとSRC_INがあり、どちらの画像を主体にするのかを指定します。

図1.8: DST_OUT: 丸(DST)を十字(SRC)の画像で型抜きしている
図1.8: DST_OUT: 丸(DST)を十字(SRC)の画像で型抜きしている
図1.9: SRC_OUT: 十字(SRC)を丸(DST)の画像で型抜きしている
図1.9: SRC_OUT: 十字(SRC)を丸(DST)の画像で型抜きしている

ATOP(SRC/DST)

ATOPは、一方の画像の上にもう一方の画像のIN(交差)の結果を重ね合わせるモードです。
DST_ATOPとSRC_ATOPがあり、どちらの画像を主体にするのかを指定します。

図1.10: DST_ATOP: 十字(SRC)にDST_INの結果を重ねている
図1.10: DST_ATOP: 十字(SRC)にDST_INの結果を重ねている
図1.11: SRC_ATOP: 丸(DST)にSRC_INの結果を重ねている
図1.11: SRC_ATOP: 丸(DST)にSRC_INの結果を重ねている

XOR

XORは、2枚の画像の重なる部分だけを透明にするモード(排他的論理和)です。
SRC_OUTとDST_OUTを重ねた結果になります。

図1.12: XOR: 丸(DST)と十字(SRC)の画像の重なる部分だけ透明になっている
図1.12: XOR: 丸(DST)と十字(SRC)の画像の重なる部分だけ透明になっている

ADD

ADDは、2枚の画像のそれぞれのピクセルの値を加算するモードです。

図1.13: ADD: 丸(DST)と十字(SRC)の画像の重なる部分のピクセルが加算されている
図1.13: ADD: 丸(DST)と十字(SRC)の画像の重なる部分のピクセルが加算されている

その他

PorterDuff.Modeは他にもDARKEN, LIGHTEN, MULTIPLY, SCREEN, OVERLAYがありますが、筆者の経験上、利用頻度が余り高くないのでここでは説明を省略します。
DARKENとLIGHTENは、日光や陰のような色味を表現するには適しているのですが、ハードウェア・アクセラレーションはこれらのモードをサポートしません3)http://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported(図1.14, 図1.15)。OVERLAYも同様です。

図1.14: DARKEN: ハードウェア・アクセラレーション有効時(左)、無効時(右)
図1.14: DARKEN: ハードウェア・アクセラレーション有効時(左)、無効時(右)
図1.15: LIGHTEN: ハードウェア・アクセラレーション有効時(左)、無効時(右)
図1.15: LIGHTEN: ハードウェア・アクセラレーション有効時(左)、無効時(右)

どうしてもこれらのモードを使いたい場合は、リスト1.4のように、Viewで個別にハードウェア・アクセラレーションを無効にすることで機能します。

リスト1.4: ViewのコンストラクタでHardwareAccelerationを明示的に無効化
setLayerType(View.LAYER_TYPE_SOFTWARE, mPaint);

しかし、ハードウェア・アクセラレーションを無効にした場合、描画パフォーマンスが低下します。

また、CLEARモードの挙動が変わる(図1.16)という現象が、筆者の環境で発生しています。

図1.16: CLEAR: ハードウェア・アクセラレーション有効時(左)、無効時(右)
図1.16: CLEAR: ハードウェア・アクセラレーション有効時(左)、無効時(右)

本来の仕様的にはハードウェア・アクセラレーションが無効になっているときの動作が正しいようで、有効時の動作はSRC_OUTやDST_OUTと同じになっています。
CLEARを使わなくても同様のことは実現可能(OUTを使ってピクセル削除)であること、パフォーマンスの低下に見合うメリットがあるのか、ハードウェア・アクセラレーションの無効化は慎重に検討してください。

1.4 まとめ

いかがでしたか?

今回は、不透明度(アルファ値)が全て最大値(完全に不透明)の画像を扱いましたが、透明度のある画像同士の合成が出来るようになれば、さらに強力な味方になることは間違いありません。

PorterDuffは、あまり目立たない機能です。

しかし、ひとたび使いこなせれば複雑な画像素材を何枚も用意しなくても、一つのカスタムビューで柔軟な表示ができるようになる、大変便利な機能です。

是非、試してみてください。