petitviolet blog

    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