blog.petitviolet.net

Androidでcwac-cameraを使って写真撮影する

2014-08-26

QiitaAndroid

ざっくり概要

  • commonsguy/cwac-cameraというカメラ用のライブラリを使う
  • Android で写真を撮る - 動作設定をする - 起動・撮影・保存する
  • 撮影した写真をあとで使う - イベントバスのOttoを使う

カメラの動作設定

基本的には cwac-camera/SimpleCameraHost.java

をベースに手を加えれば良さそう。

SingleShotMode

SingleShotMode とは、一般的なカメラアプリのように何枚も撮影するのではなく、 写真を一枚だけ撮影するモードのことで、useSingleShotModeによって切り替えられる。 SingleShotMode の場合であれば、撮影した後にその撮影した写真を撮影領域に貼ってくれる。 そうでない場合は、また普通に撮影領域が動き出して、繰り返し撮影できる状態になっている。 例えば SingleShotMode しか使わないのであれば、SimpleCameraHost#useSingleShotModeを Override して

@Override
public boolean useSingleShotMode() {
    return true;
    // return super.useSingleShotMode();
    // デフォルトではfalse
}

のようにすれば、SingleShotMode に固定することも出来る。 また、SimpleCameraHostBuilder パターン - Wikipediaでインスタンスを生成できるようになっているため、new SimpleCameraHost.Builder(this).useSingleShotMode(false).build();等としても良い。

onAutoFocus 時の音

デフォルトだと AutoFocus 時に音がなってうるさいため、

@Override
public void onAutoFocus(boolean success, Camera camera) {
    // super.onAutoFocus(success, camera);
}

とでもしておけばいい なお、シャッター時の音は消し方が分からなかった。

写真のサイズ

SimpleCameraHost#getPictureSizeによって変更できる

デフォルトだと

public Camera.Size getPictureSize(PictureTransaction xact, Camera.Parameters parameters) {
    return(CameraUtils.getLargestPictureSize(this, parameters));
}

となっており、メソッド名からして最大サイズの画像を撮ろうとしている。 普通のカメラアプリとか、画質が求められるようなアプリケーションであればこのままでいいはず。

汚くてもいいから画像サイズを小さくしたいという場合に、ひとまず写真のサイズを 画面サイズに合わせたものにする

dm77/barcodescannerCameraPreview#getOptimalPreviewSizeを大いに参考にさせてもらった。 参考、というよりそのまま拝借している。 barcodescanner/LICENSEにあるように Apache License2.0 なので問題ないはず…

@Override
public Camera.Size getPictureSize(PictureTransaction xact, Camera.Parameters parameters) {
    List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
    Point screenResolution = DisplayUtils.getScreenResolution(getContext());
    int w = screenResolution.x;
    int h = screenResolution.y;

    final double ASPECT_TOLERANCE = 0.1;
    double targetRatio = (double) w / h;
    if (sizes == null) return null;

    Camera.Size optimalSize = null;
    double minDiff = Double.MAX_VALUE;

    int targetHeight = h;

    for (Camera.Size size : sizes) {
        double ratio = (double) size.width / size.height;
        if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
        if (Math.abs(size.height - targetHeight) < minDiff) {
            optimalSize = size;
            minDiff = Math.abs(size.height - targetHeight);
        }
    }

    if (optimalSize == null) {
        minDiff = Double.MAX_VALUE;
        for (Camera.Size size : sizes) {
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
    }
    return optimalSize;
}

写真を保存するパス, ファイル名

  • ディレクトリ - getPhotoDirectory
  • ファイル名 - getPhotoFilename

を Override すれば良い。

@Override
protected File getPhotoDirectory() {
    // SDカード内のこのアプリ用ディレクトリを取得し、その中にphotoというディレクトリを作成する
    return context.getExternalFilesDir("photo");
}

@Override
protected String getPhotoFilename() {
    // TimeZoneを合わせておく
    String ts=
            new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.JAPAN).format(new Date());
    return("myapp_photo_" + ts + ".jpg");
}

写真を保存する

saveImageメソッドを利用する

public void saveImage(com.commonsware.cwac.camera.PictureTransaction xact, android.graphics.Bitmap bitmap);

public void saveImage(com.commonsware.cwac.camera.PictureTransaction xact, byte[] image);

の 2 つが用意されており、写真撮影時にBitmapbyte[]を選択することで上記のsaveImageが適切に呼ばれるが、デフォルトではbyte[]を使うようになっている上に、Bitmap用のsaveImageは以下の様な実装になっている。

@Override
public void saveImage(PictureTransaction xact, Bitmap bitmap) {
    // no-op
}

加工することも考えるとBitmapを使ったほうがいいような気もするので、

@Override
public void saveImage(PictureTransaction xact, Bitmap bitmap) {
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
    byte[] image = stream.toByteArray();
    super.saveImage(xact, image);
}

とでもすれば動く。 ちなみに、bitmap.compressの第二引数は圧縮率なので、ここで 100 より小さい値を与えれば小さく出来る。 Bitmap | Android Developers

撮影する

カメラの起動

Activity上でCameraFragmentのインスタンスをcommitすれば良い。 なお、カメラの撮影領域のレイアウト id はphoto_frameとしてある。

manager = getFragmentManager();
cameraFragment = new CameraFragment();
cameraFragment.setHost(new CustomCameraHost(this, manager));
manager.beginTransaction().add(R.id.photo_frame, cameraFragment).commit();

これだけでphoto_frame部分がカメラとして動き出す。

AutoFocus

private void autoFocus() {
    if (cameraFragment != null && cameraFragment.isVisible()) {
        // if (!cameraFragment.isAutoFocusAvailable()) {
        //     cameraFragment.cancelAutoFocus();
        // }
        cameraFragment.cancelAutoFocus();
        cameraFragment.autoFocus();
    }
}

こんな感じでautoFocusメソッドを用意しておく。 cameraFragment.autoFocusする前にcancelAutoFocusを呼んでいるが、 フォーカスしている最中に再度フォーカスしようとすると落ちるためである。

findViewById(R.id.photo_frame).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        autoFocus();
    }
});

とでもしておけば、撮影領域をタップするとフォーカスするようになる。

takePicture

private void takePicture() {
    if (cameraFragment != null && cameraFragment.isVisible()) {
        // cameraFragment.takePicture(boolean needBitmap, boolean needByteArray);
        cameraFragment.takePicture(true, false);  // bitmapで保存
        // cameraFragment.takePicture(false, true);  // byte[]で保存
    }
}

このtakePictureメソッドを適当にButtononClickで実行されるようにでもしておけば良い。 cameraFragment.takePictureの引数によって、上で説明したsaveImageのどちらが呼ばれるか変わる。

SingleShotMode の場合

SingleShotMode である場合、cameraFragment.takePictureが呼ばれると、撮影領域が撮影した写真に置き換わって確認画面となる。 もとに戻すためにはcameraFragment.restartPreview()を呼べば良い。 撮影領域が再度撮影モードに切り替わる。

なお、確認画面にはなるものの写真はしっかりと保存されている。 なぜ、この確認画面で保存するかどうか選べるような設計になっていないのか…

撮影した写真の扱い

保存された写真のパスを親となるActivityで使いたい場合に、いい感じにするにはどうやればいいか分からなかった。 そこでOttoを使った。 saveImageが呼ばれたタイミングでgetPhotoPath()の結果を post するようにした。

otto の Bus を拡張する

saveImageの際にBuspostしようとすると CameraHost が Main の Thread ではない、というエラーが出るので、 http://stackoverflow.com/questions/15431768/how-to-send-event-from-service-to-activity-with-otto-event-bus を参考にして

MainThreadBus.java
public class MainThreadBus extends Bus {
    private static final String TAG = MainThreadBus.class.getSimpleName();
    private final Bus mBus;
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    public MainThreadBus(final Bus bus) {
        if (bus == null) {
            throw new NullPointerException("bus must not be null");
        }
        mBus = bus;
    }

    @Override
    public void register(Object obj) {
        mBus.register(obj);
    }

    @Override
    public void unregister(Object obj) {
        mBus.unregister(obj);
    }

    @Override
    public void post(final Object event) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            mBus.post(event);
        } else {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mBus.post(event);
                }
            });
        }
    }
}

を用意しておく。

EventBus 用のシングルトンも用意する。

EventBusHolder.java
public final class EventBusHolder {
    public static final MainThreadBus EVENT_BUS = new MainThreadBus(new Bus());
}

イベント用のクラスを実装する

SaveImageEvent.java
public class SaveImageEvent {
    private final File photoPath;

    public SaveImageEvent(File photoPath) {
        this.photoPath = photoPath;
    }
    public File getPhotoPath() {
        return photoPath;
    }
}

写真のパスを Bus に post する

SimpleCameraHost#saveImage
EventBusHolder.EVENT_BUS.post(new SaveImageEvent(getPhotoPath()));

とでもするだけで良い。

post された写真のパスを受け取る

撮影したActivity
@Subscribe
public void onSaveImage(SaveImageEvent event) {
    Toast.makeText(getApplicationContext(), event.getPhotoPath().toString(),Toast.LENGTH_SHORT).show();
    photoPath = event.getPhotoPath();
}

これだけで良い。 こうすると保存された写真のパスがToastで表示される。

感想

Android でカメラ使う時、みんなどうしているんだろう。 今回使った cwac-camera について日本語で情報がなかったし、もしかしたら他の便利なライブラリがあるんだろうか。

from: https://qiita.com/petitviolet/items/c8b5396935a24d77e69d