Androidアプリ開発 Anroid P Preview 画像処理 ImageDecoderでBitmap処理をおこなう

2018年03月26日(編集2018年03月26日)
このエントリーをはてなブックマークに追加

Androidアプリ開発のBitmap変換処理は、AndroidPからBitmapFactoryクラスではなく、新しく導入されたImageDecoderクラスを推奨しています。

この記事は、ImageDecoderクラスを使ってBitmap処理を実装する方法を記載した記事です。

環境はAndroid P プレビュー (API level 28) です。

注意

Android P プレビューは現在デベロッパー プレビュー版です。プレビューなので、まだ利用できないメソッドやバグがたくさんあります。つまり、現段階では、この記事は未来のアプリのための検証記事です。

記述内容や実装方法も今後変更になる可能性があります。
当然記事自体も未完成となるので、随時更新していく予定です。

また、様々な変更が予想されるので、正式リリースまでは日本語のみとなります。

環境

  • macOS Sierra
  • android P Preview sdk 28
  • Oracle jdk version 1.8.0_72
  • Android Studio 3.0.1
  • kotlin 1.2.30

難易度

中級者向け

サンプルコード

Android-Media-Demo

ImageDecoder

Android Pからは、BitmapFactoryの代わりに、ImageDecoderを利用してBitmapの生成や操作をおこないます。Android P 以降は、これまで利用していたBitmapFactoryはサポート終了になります。

ImageDecoderは、バイト バッファ、ファイル、URI からBitmapオブジェクトやDrawableオブジェクトを生成できます。さらに、画像サイズの変換やcropも可能です。

インストール

Android Pを利用するには、{project_folder}/app/build.gradleを下記のコードのように変更します。

{project_folder}/app/build.gradle
android {
      compileSdkVersion "android-P"
      buildToolsVersion rootProject.buildToolsVersion
      defaultConfig {
          applicationId "java_lang_programming.com.android_media_demo"
          minSdkVersion 16
          targetSdkVersion "P"
          versionCode 1
          versionName "1.0"
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
}
          

compileSdkVersionを"android-P"、targetSdkVersionを"P"に変更します。

ビルドしてコンパイルエラーが発生しなければ準備完了です。

アプリ設計

端末のギャラリーで選択した画像を、Bitmapに変換して画面に表示します。

fig1. アプリ設計 Bitmap画像表示

BitmapFactoryを使う実装

ImageDecoderを使った実装をする前に、BitmapFactoryの実装方法をおさらいしておきます。
BitmapFactoryを使ったUriからBitmapへの変換処理は、以下の実装手順となります。

  1. getContentResolverでuriからParcelFileDescriptorを取得
  2. ParcelFileDescriptorからFileDescriptorを取得
  3. BitmapFactory.Optionsを生成し、画像の向きや画像の大きさを計算をして設定
  4. BitmapFactory.decodeFileDescriptorやBitmap.createBitmapでBitmapを作成

結果としてコード量は 50 - 100行 にも及び、慣れないうちはOutOfMemoryが多発する難易度の高い実装になります。

ImageDecoderを使う実装

Android PからはImageDecoderでUriからBitmapへの変換処理をおこないます。
ImageDecoderは、BitmapFactoryの反省から作成されたからか、非常にシンプルな実装が可能です。

言語はJavaを使います。

{project_folder}/app/src/main/ImageDecoderActivity.java
Bitmap bitmap = null;
// P 以降
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
    Log.d("ImageDecoderActivity", "ImageDecoder");
    try {
        bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()));
    } catch (IOException e) {
        e.printStackTrace();
    }
// 既存処理
} else {
    Log.d("ImageDecoderActivity", "BitmapFactory");
    bitmap = getBitmap(getApplicationContext(), data.getData());
}
      

コードがたった1行になりました。

■ 実装の解説

1. ImageDecoder.createSource

ImageDecoder.createSourceメソッドでImageDecoder.Sourceオブジェクトを生成します。

ImageDecoder.createSource(getContentResolver(), data.getData())

createSourceメソッドの引数には、ContentResolverと画像のUriを渡します。

2. ImageDecoder.decodeBitmap

ImageDecoder.decodeBitmapメソッドは、ImageDecoder.SourceオブジェクトをBitmapに変換します。

ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()));

decodeBitmapメソッドの引数には、ImageDecoder.Sourceオブジェクトを渡します。decodeBitmapメソッドもcreateSourceメソッドと同様、staticメソッドです。ImageDecoderクラスはインスタンスを生成(new)できないことに注意しましょう。

実行

上記のコードを実行します。

fig2. ImageDecoder実行

Bitmapの生成に成功しました。

画像情報取得

ImageDecoderは他にも便利なメソッドが用意されています。

まずは画像情報を取得してみましょう。取得可能な画像情報は、画像のサイズ、種類、アニメーション実行の有無です。実装方法は、ImageDecoder.OnHeaderDecodedListenerインターフェースを使ってcallbackで画像情報を受け取ります。

{project_folder}/app/src/main/ImageDecoderActivity.java
Bitmap bitmap = null;
// P 以降
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
    Log.d("ImageDecoderActivity", "ImageDecoder");
    StringBuilder msg = new StringBuilder();
    try {
        bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()), new ImageDecoder.OnHeaderDecodedListener() {
            @Override
            public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
                msg.append("ImageDecoderでbitmapに変換しました。
");
                msg.append("画像サイズ : " + imageInfo.getSize() + "
");
                msg.append("画像種別 : " + imageInfo.getMimeType() + "
");
                msg.append("アニメーション : " + imageInfo.isAnimated() + "
");
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
    selectedImageInfo.setText(msg.toString());
// 既存処理
} else {
    Log.d("ImageDecoderActivity", "BitmapFactory");
    bitmap = getBitmap(getApplicationContext(), data.getData());
    selectedImageInfo.setText("BitmapFactoryでbitmapに変換しました。");
}
      

■ 実装の解説

1. ImageDecoder.OnHeaderDecodedListener

ImageDecoder.OnHeaderDecodedListenerインターフェースをdecodeBitmapメソッドの引数に渡します。

ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()), 
        new ImageDecoder.OnHeaderDecodedListener() {
});

ImageDecoder.OnHeaderDecodedListenerインターフェースはonHeaderDecodedメソッドを宣言しています。なので、無名クラスでonHeaderDecodedメソッドを実装します。

bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()), 
        new ImageDecoder.OnHeaderDecodedListener() {
            @Override
            public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
                msg.append("ImageDecoderでbitmapに変換しました。
");
                msg.append("画像サイズ : " + imageInfo.getSize() + "
");
                msg.append("画像種別 : " + imageInfo.getMimeType() + "
");
                msg.append("アニメーション : " + imageInfo.isAnimated() + "
");
            }
});

実行

上記のコードを実行します。

fig3. ImageDecoder.OnHeaderDecodedListener imageInfo

画像情報を取得できているのが確認できました。

画像サイズ変更

ImageDecoderは画像サイズの変更も可能です。BitmapFactoryのようにOptionsで色々実装する必要はありません。

画像サイズの変更は、画像情報取得と同じように、ImageDecoder.OnHeaderDecodedListenerインターフェースを使ってcallbackでImageDecoderオブジェクトを受け取ります。

{project_folder}/app/src/main/ImageDecoderActivity.java
Bitmap bitmap = null;
// P 以降
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
    Log.d("ImageDecoderActivity", "ImageDecoder");
    StringBuilder msg = new StringBuilder();
    try {
        bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()), new ImageDecoder.OnHeaderDecodedListener() {
            @Override
            public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
                if (checkedResize.isChecked()) {
                    Size size = imageInfo.getSize();
                    imageDecoder.setResize(size.getWidth() * 2, size.getHeight() * 2);
                }
                msg.append("ImageDecoderでbitmapに変換しました。
");
                msg.append("画像サイズ : " + imageInfo.getSize() + "
");
                msg.append("画像種別 : " + imageInfo.getMimeType() + "
");
                msg.append("アニメーション : " + imageInfo.isAnimated() + "
");
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
    selectedImageInfo.setText(msg.toString());
// 既存処理
} else {
    Log.d("ImageDecoderActivity", "BitmapFactory");
    bitmap = getBitmap(getApplicationContext(), data.getData());
    selectedImageInfo.setText("BitmapFactoryでbitmapに変換しました。");
}
      

■ 実装の解説

1. imageDecoder.setResize

ImageDecoder#setResizeで画像サイズを変更します。

Size size = imageInfo.getSize();
imageDecoder.setResize(size.getWidth() * 2, size.getHeight() * 2);

setResizeメソッドに画像のwidthとheightを設定します。上記は、ImageDecoder.ImageInfoで取得したwidthとheightのサイズを2倍にして設定しています。

実行

上記のコードを実行します。

fig4. ImageDecoder.OnHeaderDecodedListener setResize

画像を拡大して表示しているのが確認できました。
画像が拡大していてもImageInfoの情報が上書きされていない点も頭に入れておきましょう。

画像Crop

ImageDecoderは画像cropも可能です。画像cropとは、画像の切り取りです。

画像情報取得や画像サイズの変更と同じように、ImageDecoder.OnHeaderDecodedListenerインターフェースを使ってcallbackでImageDecoderオブジェクトを受け取ります。

{project_folder}/app/src/main/ImageDecoderActivity.java
Bitmap bitmap = null;
// P 以降
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
    Log.d("ImageDecoderActivity", "ImageDecoder");
    StringBuilder msg = new StringBuilder();
    try {
        bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(getContentResolver(), data.getData()), new ImageDecoder.OnHeaderDecodedListener() {
            @Override
            public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
                if (checkedResize.isChecked()) {
                    Size size = imageInfo.getSize();
                    imageDecoder.setResize(size.getWidth() * 2, size.getHeight() * 2);
                }

                if (checkedCrop.isChecked()) {
                    Size size = imageInfo.getSize();
                    imageDecoder.setCrop(new Rect(0, 0, size.getWidth(), size.getHeight() / 2));
                }

                msg.append("ImageDecoderでbitmapに変換しました。
");
                msg.append("画像サイズ : " + imageInfo.getSize() + "
");
                msg.append("画像種別 : " + imageInfo.getMimeType() + "
");
                msg.append("アニメーション : " + imageInfo.isAnimated() + "
");
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
    selectedImageInfo.setText(msg.toString());
// 既存処理
} else {
    Log.d("ImageDecoderActivity", "BitmapFactory");
    bitmap = getBitmap(getApplicationContext(), data.getData());
    selectedImageInfo.setText("BitmapFactoryでbitmapに変換しました。");
}
      

■ 実装の解説

1. imageDecoder.setCrop

ImageDecoder#setCropで画像をcropします。
引数にはRectオブジェクトを渡します。

Size size = imageInfo.getSize();
imageDecoder.setCrop(new Rect(0, 0, size.getWidth(), size.getHeight() / 2));

Rectクラスは座標位置を設定できます。setCropメソッドは、Rectクラスで設定した範囲をcropします。
Rectのleftとtopが画像の左辺と上辺の位置です。すなわち0が左辺と上辺ピッタリの位置になります。rightとbottomはleftとtopからの距離になります。

上記は、bottomを高さの半分にしています。つまり、画像の上半分をcropして表示します。

実行

上記のコードを実行します。

fig5. ImageDecoder.OnHeaderDecodedListener setCrop

画像をcropして表示しているのが確認できました。
setResizeメソッドとsetCropメソッドは同じオブジェクトで操作しているので、どちらのメソッドも影響を受けます。一方で、ImageInfoの情報は上書きされないので、整合性に問題がある気がします。

結論

これまで画像処理は、外部ライブラリの使用が多かったのですが、今後は純正クラスの実装も考慮に入ってきそうです。
ImageDecoderは、正式に使えるようになるのが待ち遠しいクラスです。とはいえ、sdk28以上だとプロジェクトへの導入は難しいので、support library化に期待です。

関連記事

タグ検索で調べてみよう

AndroidP 画像処理