カメラをコントロールする

このレッスンでは、フレームワーク API を使用してカメラ ハードウェアを直接制御する方法について説明します。

注: このページでは、サポートが終了した Camera クラスについて言及しています。CameraX(特定のユースケースでは Camera2)の使用をおすすめします。CameraX と Camera2 は、どちらも Android 5.0(API レベル 21)以降に対応しています。

デバイスのカメラを直接制御するには、既存のカメラアプリに画像や動画をリクエストするよりも多くのコードが必要になります。ただし、特殊なカメラアプリや、アプリの UI に完全に統合された機能を作成したい場合のために、このレッスンではその方法をご紹介します。

以下の関連リソースもご覧ください。

Camera オブジェクトを開く

カメラを直接制御するには、まず、Camera オブジェクトのインスタンスを取得します。Android のカメラアプリと同じように、カメラにアクセスする場合は、onCreate() で開始した別のスレッドで Camera オブジェクトを開くことをおすすめします。このアプローチが優れているのは、カメラへのアクセスには時間がかかることがあり、UI スレッドが停止する可能性があるためです。基本的な実装では、カメラを開く処理を onResume() メソッドに任せることで、コードの再利用が容易になり、制御フローをシンプルに保つことができます。

カメラが別のアプリですでに使用されている場合に Camera.open() を呼び出すと例外がスローされるため、ここでは try ブロックにラップします。

Kotlin

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        mCamera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(getString(R.string.app_name), "failed to open Camera")
        e.printStackTrace()
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    mCamera?.also { camera ->
        camera.release()
        mCamera = null
    }
}

Java

private boolean safeCameraOpen(int id) {
    boolean qOpened = false;

    try {
        releaseCameraAndPreview();
        camera = Camera.open(id);
        qOpened = (camera != null);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }

    return qOpened;
}

private void releaseCameraAndPreview() {
    preview.setCamera(null);
    if (camera != null) {
        camera.release();
        camera = null;
    }
}

API レベル 9 以降のカメラ フレームワークは複数のカメラをサポートしています。レガシー API を使用して引数を指定せずに open() を呼び出すと、1 つ目の背面カメラを取得できます。

カメラのプレビューを作成する

写真を撮影する場合、通常、シャッターをクリックする前に被写体のプレビューをユーザーに表示する必要があります。SurfaceView を使用すると、カメラセンサーが捕捉している画像のプレビューを描画できます。

Preview クラス

プレビューの表示を開始するには、Preview クラスが必要です。プレビューには、android.view.SurfaceHolder.Callback インターフェースの実装が必要です。これを使用して、カメラ ハードウェアから取得した画像データをアプリに渡します。

Kotlin

class Preview(
        context: Context,
        val surfaceView: SurfaceView = SurfaceView(context)
) : ViewGroup(context), SurfaceHolder.Callback {

    var mHolder: SurfaceHolder = surfaceView.holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }
    ...
}

Java

class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView surfaceView;
    SurfaceHolder holder;

    Preview(Context context) {
        super(context);

        surfaceView = new SurfaceView(context);
        addView(surfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        holder = surfaceView.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
...
}

ライブ画像のプレビューを開始するには、次のセクションで説明するように、事前に Preview クラスを Camera オブジェクトに渡す必要があります。

プレビューを設定して開始する

カメラ インスタンスとそれに関連するプレビューは、特定の順序で作成する必要があります(カメラ オブジェクトを最初に作成します)。次のスニペットでは、カメラの初期化プロセスをカプセル化しています。これにより、ユーザーがカメラを変更するためになんらかの操作を行ったときに Camera.startPreview()setCamera() メソッドから呼び出されるようになります。また、プレビューの再開は、Preview クラスの surfaceChanged() コールバック メソッドで行う必要があります。

Kotlin

fun setCamera(camera: Camera?) {
    if (mCamera == camera) {
        return
    }

    stopPreviewAndFreeCamera()

    mCamera = camera

    mCamera?.apply {
        mSupportedPreviewSizes = parameters.supportedPreviewSizes
        requestLayout()

        try {
            setPreviewDisplay(holder)
        } catch (e: IOException) {
            e.printStackTrace()
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        startPreview()
    }
}

Java

public void setCamera(Camera camera) {
    if (mCamera == camera) { return; }

    stopPreviewAndFreeCamera();

    mCamera = camera;

    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        supportedPreviewSizes = localSizes;
        requestLayout();

        try {
            mCamera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
}

カメラの設定を変更する

カメラの設定では、ズームレベルや露出補正など、カメラでの撮影方法を変更できます。次の例では、プレビュー サイズのみを変更しています。詳細については、カメラアプリのソースコードを参照してください。

Kotlin

override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
    mCamera?.apply {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        parameters?.also { params ->
            params.setPreviewSize(previewSize.width, previewSize.height)
            requestLayout()
            parameters = params
        }

        // Important: Call startPreview() to start updating the preview surface.
        // Preview must be started before you can take a picture.
        startPreview()
    }
}

Java

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // Now that the size is known, set up the camera parameters and begin
    // the preview.
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(previewSize.width, previewSize.height);
    requestLayout();
    mCamera.setParameters(parameters);

    // Important: Call startPreview() to start updating the preview surface.
    // Preview must be started before you can take a picture.
    mCamera.startPreview();
}

プレビューの向きを設定する

ほとんどのカメラアプリは、ディスプレイを横表示にロックします。これは、横表示がカメラセンサーの標準の向きであるためです。デバイスの向きは EXIF ヘッダーに記録されるため、この設定では縦向きの写真が撮影されないようにすることはできません。setCameraDisplayOrientation() メソッドを使用すると、画像の記録方法に影響を与えずにプレビューの表示方法を変更できます。ただし、API レベル 14 より前の Android では、デバイスの向きを変えてプレビューを再開するには、その前にプレビューを停止する必要があります。

写真の撮影

プレビューを開始したら、Camera.takePicture() メソッドを使用して写真を撮影します。Camera.PictureCallback オブジェクトと Camera.ShutterCallback オブジェクトを作成して、Camera.takePicture() に渡すことができます。

画像を連続して取得する場合は、onPreviewFrame() を実装する Camera.PreviewCallback を作成できます。撮影の合間の処理用に、選択したプレビュー フレームのみをキャプチャしたり、遅延動作を設定して takePicture() を呼び出したりすることも可能です。

プレビューを再開する

写真の撮影後、ユーザーが別の写真を撮影する前にプレビューを再開する必要があります。次の例では、シャッター ボタンをオーバーロードすることによって再開しています。

Kotlin

fun onClick(v: View) {
    previewState = if (previewState == K_STATE_FROZEN) {
        camera?.startPreview()
        K_STATE_PREVIEW
    } else {
        camera?.takePicture(null, rawCallback, null)
        K_STATE_BUSY
    }
    shutterBtnConfig()
}

Java

@Override
public void onClick(View v) {
    switch(previewState) {
    case K_STATE_FROZEN:
        camera.startPreview();
        previewState = K_STATE_PREVIEW;
        break;

    default:
        camera.takePicture( null, rawCallback, null);
        previewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
}

プレビューを停止してカメラを解放する

アプリでカメラを使い終わったら、クリーンアップを行います。具体的には、Camera オブジェクトを解放する必要があります。そうしないと、他のアプリ(独自のアプリの新しいインスタンスを含む)がクラッシュする恐れがあります。

では、どのタイミングでプレビューを停止してカメラを解放すればよいでしょうか。Preview クラスの以下のメソッドの説明にあるように、プレビュー サーフェスを破棄したときにプレビューを停止してカメラを解放することをおすすめします。

Kotlin

override fun surfaceDestroyed(holder: SurfaceHolder) {
    // Surface will be destroyed when we return, so stop the preview.
    // Call stopPreview() to stop updating the preview surface.
    mCamera?.stopPreview()
}

/**
 * When this function returns, mCamera will be null.
 */
private fun stopPreviewAndFreeCamera() {
    mCamera?.apply {
        // Call stopPreview() to stop updating the preview surface.
        stopPreview()

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        release()

        mCamera = null
    }
}

Java

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    // Surface will be destroyed when we return, so stop the preview.
    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();
    }
}

/**
 * When this function returns, mCamera will be null.
 */
private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        mCamera.release();

        mCamera = null;
    }
}

レッスンの前半でも、この手順が setCamera() メソッドに含まれていました。このように、カメラの初期化では必ず、最初にプレビューを停止します。