Android で OpenGL ES 2.0 事始め その弐 〜画面キャプチャ〜
今回は前回の超入門に画面キャプチャ機能を追加してみたいと思います。
GLSurfaceView.draw(canvas) で簡単に画面キャプチャできるかと思っていたのですが、そうは問屋がおろさなかったです。
今回のソースは、
- AndroidManifest.xml
- Activity
- GLSurfaceView
- GLSurfaceView.Renderer
- OpenGL ES 2.0 簡易ユーティリティ
- フォトギャラリーへ書き込むためのユーティリティ
で構成しています。
リファクタリングしながら作っているので一部クラス名に変更があります。
それではご覧ください。
AndroidManifest.xml
今回のサンプルでは画面をキャプチャしてストレージへ保存するので、uses-permission に WRITE_EXTERNAL_STORAGE と READ_EXTERNAL_STORAGE を加えています。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.orangesignal.android.example.gles20" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> <!-- OpenGL ES 2.0 を使用するので API レベル 8 以上とします。 --> <uses-sdk android:minSdkVersion="8" /> <!-- 画像を保存するのでストレージへアクセスすることを宣言します。 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Google プレイ (旧 Android マーケット) からダウンロード可能な端末を OpenGL ES 2.0 を使用可能な端末に制限します。 --> <uses-feature android:glEsVersion="0x00020000" /> <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"> <activity android:name=".ui.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Activity
今回のサンプルでは、メニューから "Capture" を選択した場合にキャプチャの要求をします。
Activity.onCreate 中の mPhotoView.setOnCaptureListener でキャプチャ用のリスナーを設定しています。
そして Activity に PhotoView.OnCaptureListener を実装して onCapture(Bitmap) でキャプチャ後の処理を行っています。
尚 UI からの入力があることから GLSurfaceView のサブクラス (PhotoView) を作成/使用しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.ui; import android.app.Activity; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import com.orangesignal.android.example.gles20.gl.PhotoView; import com.orangesignal.android.media.ImageMediaUtils; /** * このアプリケーションの主たるアクティビティを提供します。 * * @author 杉澤 浩二 */ public final class MainActivity extends Activity implements PhotoView.OnCaptureListener { private PhotoView mPhotoView; ////////////////////////////////////////////////////////////////////////// // ライフサイクル @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPhotoView = new PhotoView(this); mPhotoView.setOnCaptureListener(this); setContentView(mPhotoView); } @Override public void onResume() { super.onResume(); mPhotoView.onResume(); } @Override public void onPause() { super.onPause(); mPhotoView.onPause(); } ////////////////////////////////////////////////////////////////////////// // メニュー private static final int MENU_CAPTURE = 0; @Override public boolean onCreateOptionsMenu(final Menu menu) { menu.add(Menu.NONE, MENU_CAPTURE, Menu.NONE, "Capture"); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case MENU_CAPTURE: mPhotoView.requestCapture(); return true; default: return super.onOptionsItemSelected(item); } } ////////////////////////////////////////////////////////////////////////// // PhotoView.OnCaptureListener @Override public void onCapture(final Bitmap bitmap) { // MediaStore.Images.Media.insertImage で画像を保存すると期待通りの形式や画質にならないので自前で保存します。 final Uri uri = ImageMediaUtils.savePngImage(getApplicationContext(), bitmap); bitmap.recycle(); final StringBuffer sb = new StringBuffer(); if (uri == null) { sb.append("failure"); } else { sb.append("success [").append(uri).append(']'); } Toast.makeText(getApplicationContext(), sb, Toast.LENGTH_LONG).show(); } }
GLSurfaceView
UI イベントを処理して、必要があれば Renderer へ処理を要求するための GLSurfaceView クラスです。
このクラスでは基本的には OnCaptureListener を操作しているだけですが、
別スレッドで動作している Renderer への処理要求に queueEvent を使用していることに注意して下さい。この辺も公式サイト記載のままですね。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import android.content.Context; import android.graphics.Bitmap; import android.opengl.GLSurfaceView; import android.util.AttributeSet; /** * UI からの入力を受け付けて {@link GLSurfaceView.Renderer} へ処理を依頼するための {@link GLSurfaceView} のサブクラスを提供します。 * * @author 杉澤 浩二 */ public final class PhotoView extends GLSurfaceView { /** * 画像データをキャプチャするためのコールバックインタフェースを提供します。 */ public interface OnCaptureListener { /** * 画像を取得した場合に呼び出されます。 * * @param bitmap 画像データの {@link Bitmap} */ void onCapture(Bitmap bitmap); } ////////////////////////////////////////////////////////////////////////// /* package */ PhotoRenderer mRenderer; /** * 画像データをキャプチャするためのコールバックインタフェースを保持します。 */ /* package */ OnCaptureListener mOnCaptureListener; ////////////////////////////////////////////////////////////////////////// // Constructors /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can access the current theme, resources, etc. */ public PhotoView(final Context context) { super(context); initialize(context); } /** * Constructor that is called when inflating a view from XML. * This is called when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. * * @param context The Context the view is running in, through which it can access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. */ public PhotoView(final Context context, final AttributeSet attrs) { super(context, attrs); initialize(context); } private void initialize(final Context context) { setEGLContextClientVersion(2); // OpenGL ES 2.0 を使用するように構成します。 mRenderer = new PhotoRenderer(context); setRenderer(mRenderer); } ////////////////////////////////////////////////////////////////////////// // public method /** * 画像データのキャプチャを要求します。 * * @see {@link #setOnCaptureListener(OnCaptureListener)} */ public void requestCapture() { // GLSurfaceView.Renderer は別スレッドで動作しているので Renderer への操作は GLSurfaceView.queueEvent を使用して行います。 queueEvent(new Runnable() { @Override public void run() { mRenderer.capture(mOnCaptureListener); } }); } ////////////////////////////////////////////////////////////////////////// // setter / getter /** * 画像データをキャプチャするためのコールバックインタフェースを設定します。 * * @param l 画像データをキャプチャするためのコールバックインタフェース * @see {@link #requestCapture()} */ public void setOnCaptureListener(final OnCaptureListener l) { mOnCaptureListener = l; } }
GLSurfaceView.Renderer
前回のソースからの変更は、まずクラス名を SimpleRenderer ⇒ PhotoRenderer へ変更しました。
そしてキャプチャ用のメソッドや変数、UI スレッドへコールバックするための Handler などを追加しています。
変更はクラス名のみであとは追加です。
肝心のキャプチャ処理は glReadPixels でピクセルデータを取得しています。
但しそのまま Android の Bitmap へ変換すると、赤色と青色が逆だったり、上下が逆だったりするので修正しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import java.nio.FloatBuffer; import java.nio.IntBuffer; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.os.Handler; import com.orangesignal.android.example.gles20.R; import com.orangesignal.android.example.gles20.gl.PhotoView.OnCaptureListener; import com.orangesignal.android.opengl.GLES20Utils; /** * ビットマップをテクスチャとして読み込んで {@link GLSurfaceView} の描画領域いっぱいに描画するだけのシンプルな {@link GLSurfaceView.Renderer} を提供します。 * * @author 杉澤 浩二 */ public final class PhotoRenderer implements GLSurfaceView.Renderer { /** * コンテキストを保持します。 */ private final Context mContext; /** * UI スレッドへコールバックを返すための {@link Handler} オブジェクトを保持します。 */ private Handler mHandler = new Handler(); private int mWidth; private int mHeight; /** * 頂点データです。 */ private static final float VERTEXS[] = { -1.0f, 1.0f, 0.0f, // 左上 -1.0f, -1.0f, 0.0f, // 左下 1.0f, 1.0f, 0.0f, // 右上 1.0f, -1.0f, 0.0f // 右下 }; /** * テクスチャ (UV マッピング) データです。 */ private static final float TEXCOORDS[] = { 0.0f, 0.0f, // 左上 0.0f, 1.0f, // 左下 1.0f, 0.0f, // 右上 1.0f, 1.0f // 右下 }; /** * 頂点バッファを保持します。 */ private final FloatBuffer mVertexBuffer = GLES20Utils.createBuffer(VERTEXS); /** * テクスチャ (UV マッピング) バッファを保持します。 */ private final FloatBuffer mTexcoordBuffer = GLES20Utils.createBuffer(TEXCOORDS); private int mProgram; private int mPosition; private int mTexcoord; private int mTexture; private int mTextureId; ////////////////////////////////////////////////////////////////////////// // コンストラクタ /** * コンストラクタです。 * * @param context コンテキスト */ public PhotoRenderer(final Context context) { mContext = context; } ////////////////////////////////////////////////////////////////////////// // オーバーライド メソッド /** * ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード */ private static final String VERTEX_SHADER = "attribute vec4 position;" + "attribute vec2 texcoord;" + "varying vec2 texcoordVarying;" + "void main() {" + "gl_Position = position;" + "texcoordVarying = texcoord;" + "}"; /** * 色描画用のピクセル/フラグメントシェーダのソースコード */ private static final String FRAGMENT_SHADER = "precision mediump float;" + "varying vec2 texcoordVarying;" + "uniform sampler2D texture;" + "void main() {" + "gl_FragColor = texture2D(texture, texcoordVarying);" + "}"; @Override public void onSurfaceCreated(final GL10 gl, final EGLConfig config) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // プログラムを生成して使用可能にします。 mProgram = GLES20Utils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (mProgram == 0) { throw new IllegalStateException(); } GLES20.glUseProgram(mProgram); GLES20Utils.checkGlError("glUseProgram"); // シェーダで使用する変数のハンドルを取得し使用可能にします。 mPosition = GLES20.glGetAttribLocation(mProgram, "position"); GLES20Utils.checkGlError("glGetAttribLocation position"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for position"); } GLES20.glEnableVertexAttribArray(mPosition); mTexcoord = GLES20.glGetAttribLocation(mProgram, "texcoord"); GLES20Utils.checkGlError("glGetAttribLocation texcoord"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for texcoord"); } GLES20.glEnableVertexAttribArray(mTexcoord); mTexture = GLES20.glGetUniformLocation(mProgram, "texture"); GLES20Utils.checkGlError("glGetUniformLocation texture"); if (mTexture == -1) { throw new IllegalStateException("Could not get uniform location for texture"); } // テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります) final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample); mTextureId = GLES20Utils.loadTexture(bitmap); bitmap.recycle(); } @Override public void onSurfaceChanged(final GL10 gl, final int width, final int height) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 mWidth = width; mHeight = height; // ビューポートを設定します。 GLES20.glViewport(0, 0, width, height); GLES20Utils.checkGlError("glViewport"); } @Override public void onDrawFrame(final GL10 gl) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // XXX - このサンプルではテクスチャの簡単な描画だけなので深さ関連の有効/無効や指定は一切していません。 // 背景色を指定して背景を描画します。 GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 背景とのブレンド方法を設定します。 GLES20.glEnable(GLES20.GL_TEXTURE_2D); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); // 単純なアルファブレンド // テクスチャの指定 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); GLES20.glUniform1i(mTexture, 0); GLES20.glVertexAttribPointer(mTexcoord, 2, GLES20.GL_FLOAT, false, 0, mTexcoordBuffer); GLES20.glVertexAttribPointer(mPosition, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glDisable(GLES20.GL_TEXTURE_2D); } ////////////////////////////////////////////////////////////////////////// /* package */ void capture(final OnCaptureListener callback) throws NullPointerException { if (callback == null) { throw new NullPointerException("OnCaptureListener must not be null."); } final Bitmap bitmap = capture(); // コールバックします。 mHandler.post(new Runnable() { @Override public void run() { callback.onCapture(bitmap); } }); } private Bitmap capture() { final int width = mWidth; final int height = mHeight; final int pixels[] = new int[width * height]; final IntBuffer buffer = IntBuffer.wrap(pixels); buffer.position(0); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); return GLES20Utils.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); } }
OpenGL ES 2.0 簡易ユーティリティ
glReadPixels で取得したピクセルデータを処理して Bitmap を作る createBitmap メソッドを追加しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.opengl.GLES20; import android.opengl.GLException; import android.opengl.GLUtils; import android.util.Log; /** * OpenGL ES 2.0 に関するユーティリティを提供します。 * * @author 杉澤 浩二 */ public final class GLES20Utils { /** * ログ出力用のタグです。 */ private static String TAG = "GLES20Utils"; /** * オブジェクトが無効であることを表します。<p> * * @see {@link #createProgram(String, String)} * @see {@link #loadShader(int, String)} */ public static final int INVALID = 0; /** * インスタンス化できない事を強制します。 */ private GLES20Utils() {} /** * 最初の要素の位置です。 */ private static final int FIRST_INDEX = 0; private static final int DEFAULT_OFFSET = 0; private static final int FLOAT_SIZE_BYTES = 4; /** * 指定されたプリミティブ型配列のデータを {@link FloatBuffer} へ変換して返します。 * * @param array バッファデータ * @return 変換されたバッファデータ * @see {@link GLES20#glVertexAttribPointer(int, int, int, boolean, int, java.nio.Buffer)} */ public static FloatBuffer createBuffer(float[] array) { final FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); buffer.put(array).position(FIRST_INDEX); return buffer; } /** * 指定されたバーテックスシェーダとフラグメントシェーダを使用してプログラムを生成します。 * * @param vertexSource ポリゴン描画用バーテックスシェーダのソースコード * @param fragmentSource 色描画用のフラグメントシェーダのソースコード * @return プログラムハンドラまたは {@link #INVALID} * @throws GLException OpenGL API の操作に失敗した場合 */ public static int createProgram(final String vertexSource, final String fragmentSource) throws GLException { // バーテックスシェーダをコンパイルします。 final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == INVALID) { return INVALID; } // フラグメントシェーダをコンパイルします。 final int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == INVALID) { return INVALID; } // プログラムを生成して、プログラムへバーテックスシェーダとフラグメントシェーダを関連付けます。 int program = GLES20.glCreateProgram(); if (program != INVALID) { // プログラムへバーテックスシェーダを関連付けます。 GLES20.glAttachShader(program, vertexShader); checkGlError("glAttachShader"); // プログラムへフラグメントシェーダを関連付けます。 GLES20.glAttachShader(program, pixelShader); checkGlError("glAttachShader"); GLES20.glLinkProgram(program); final int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, DEFAULT_OFFSET); if (linkStatus[FIRST_INDEX] != GLES20.GL_TRUE) { Log.e(TAG, "Could not link program: "); Log.e(TAG, GLES20.glGetProgramInfoLog(program)); GLES20.glDeleteProgram(program); program = INVALID; } } return program; } /** * 指定されたシェーダのソースコードをコンパイルします。 * * @param shaderType シェーダの種類 * @param source シェーダのソースコード * @return シェーダハンドラまたは {@link #INVALID} * @see {@link GLES20#GL_VERTEX_SHADER} * @see {@link GLES20.GL_FRAGMENT_SHADER} */ public static int loadShader(final int shaderType, final String source) { int shader = GLES20.glCreateShader(shaderType); if (shader != INVALID) { GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); final int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, DEFAULT_OFFSET); if (compiled[FIRST_INDEX] == INVALID) { Log.e(TAG, "Could not compile shader " + shaderType + ":"); Log.e(TAG, GLES20.glGetShaderInfoLog(shader)); GLES20.glDeleteShader(shader); shader = INVALID; } } return shader; } /** * 指定された直前の OpenGL API 操作についてエラーが発生しているかどうか検証します。 * * @param op 検証する直前に操作した OpenGL API 名 * @throws GLException 直前の OpenGL API 操作でエラーが発生している場合 */ public static void checkGlError(final String op) throws GLException { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e(TAG, op + ": glError " + error); throw new GLException(error, op + ": glError " + error); } } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @return テクスチャ ID */ public static int loadTexture(final Bitmap bitmap) { return loadTexture(bitmap, GLES20.GL_NEAREST, GLES20.GL_LINEAR); } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。<p> * この実装は簡易なテクスチャの初期化のみで繰り返しの指定をサポートしません。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @param min テクスチャを縮小するときの補完方法 * @param mag テクスチャを拡大するときの補完方法 * @return テクスチャ ID * @see {@link GLES20#GL_TEXTURE_MIN_FILTER} * @see {@link GLES20#GL_TEXTURE_MAG_FILTER} */ public static int loadTexture(final Bitmap bitmap, final int min, final int mag) { final int[] textures = new int[1]; GLES20.glGenTextures(1, textures, DEFAULT_OFFSET); final int texture = textures[FIRST_INDEX]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); // テクスチャを拡大/縮小する方法を設定します。 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, min); GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, mag); return texture; } /* * @see http://www.anddev.org/how_to_get_opengl_screenshot__useful_programing_hint-t829.html */ public static Bitmap createBitmap(final int[] pixels, final int width, final int height, final Bitmap.Config config) { // 取得したピクセルデータは R (赤) と B (青) が逆になっています。 // また垂直方向も逆になっているので以下のように ColorMatrix と Matrix を使用して修正します。 /* * カラーチャネルを交換するために ColorMatrix と ColorMatrixFilter を使用します。 * * 5x4 のマトリックス: [ * a, b, c, d, e, * f, g, h, i, j, * k, l, m, n, o, * p, q, r, s, t * ] * * RGBA カラーへ適用する場合、以下のように計算します: * * R' = a * R + b * G + c * B + d * A + e; * G' = f * R + g * G + h * B + i * A + j; * B' = k * R + l * G + m * B + n * A + o; * A' = p * R + q * G + r * B + s * A + t; * * R (赤) と B (青) を交換したいので以下の様になります。 * * R' = B => 0, 0, 1, 0, 0 * G' = G => 0, 1, 0, 0, 0 * B' = R => 1, 0, 0, 0, 0 * A' = A => 0, 0, 0, 1, 0 */ final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); // R (赤) と B (青) が逆なので交換します。 paint.setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(new float[] { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0 }))); final Bitmap bitmap = Bitmap.createBitmap(width, height, config); final Canvas canvas = new Canvas(bitmap); // 上下が逆さまなので垂直方向に反転させます。 final Matrix matrix = new Matrix(); matrix.postScale(1.0f, -1.0f); matrix.postTranslate(0, height); canvas.concat(matrix); // 描画します。 canvas.drawBitmap(pixels, 0, width, 0, 0, width, height, false, paint); return bitmap; } }
フォトギャラリーへ書き込むためのユーティリティ
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.media; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.provider.MediaStore; import android.provider.MediaStore.Images; /** * * @author 杉澤 浩二 */ public final class ImageMediaUtils { /** * インスタンス化できない事を強制します。 */ private ImageMediaUtils() {} /** * 指定された {@link Bitmap} を {@link MediaStore.Images.Media.EXTERNAL_CONTENT_URI} のコンテンツとして PNG 形式で保存します。 * * @param context コンテキスト * @param bitmap {@link Bitmap} * @return コンテンツの {@link Uri} または <code>null</code> */ public static Uri savePngImage(final Context context, final Bitmap bitmap) { final ContentResolver recolver = context.getContentResolver(); final ContentValues values = new ContentValues(); values.put(Images.Media.MIME_TYPE, "image/png"); // XXX - 外部ストレージがマウントされていない場合に例外が発生するのは呼び出す側の責務で良いか final Uri uri = recolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri != null) { boolean success = false; final Cursor cursor = recolver.query(uri, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { final String filename = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); try { final FileOutputStream out = new FileOutputStream(filename); try { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // PNG なので quality は無視されるのでとりあえず 100 を設定しています。 success = true; } finally { try { out.close(); } catch (final IOException e) {} // 無視する } } catch (final FileNotFoundException e) {} // 無視する break; } } if (!success) { recolver.delete(uri, null, null); return null; } } return uri; } }