LoginSignup
3
4

More than 3 years have passed since last update.

Androidカメラのプレビュー表示(Camera2 API + SurfaceView)

Last updated at Posted at 2020-10-20

Camera2 APIによるカメラのプレビュー表示について

Androidカメラのプレビュー表示(Camera API + SurfaceView)をお読みいただければ、Camera APIを使えば、カメラのプレビュー表示が簡単に実装できるとわかるでしょう。しかし、Camera2 APIを使う場合、実装がずっと複雑になります。特に正しい大きさとアスペクト比を表示する実装がかなり大変です。

今回はCamera2 API + SurfaceViewによるカメラのプレビュー表示の実装を紹介します。

実装

レイアウト

Androidカメラのプレビュー表示(Camera API + SurfaceView)に載せたレイアウトと同じです。

main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:keepScreenOn="true">

        <SurfaceView
            android:id="@+id/surface_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

カメラのパーミッション

AndroidManifest.xml

Camera2 APIを使う場合もAndroidManifest.xmlにandroid.permission.CAMERAを追加する必要があります。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx">
    ...
    <uses-permission android:name="android.permission.CAMERA" />
    ...
</manifest>

パーミッション要請

カメラ権限要請の部分もAndroidカメラのプレビュー表示(Camera API + SurfaceView)と同じです。

MyActivity.kt
// カメラ権限があるかどうかを確認し、ある場合はカメラを起動し、ない場合はユーザーに要請します。
private fun checkAndAskCameraPermission(context: Context) {
    if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
        // ユーザーにカメラ権限を要請
        requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
    } else {
        // カメラ権限があるので、カメラを起動
        openCamera()
    } 
}

// カメラ権限要請のコールバック
override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>, grantResults: IntArray
) {
    when (requestCode) {
        PERMISSION_REQUEST_CODE -> {
            if (permissions.isNotEmpty() && grantResults.isNotEmpty()) {
                val resultMap: MutableMap<String, Int> = mutableMapOf()
                for (i in 0..min(permissions.size - 1, grantResults.size - 1)) {
                    resultMap[permissions[i]] = grantResults[i]
                }

                when {
                    resultMap[Manifest.permission.CAMERA] == PackageManager.PERMISSION_GRANTED -> {
                        // ユーザーがカメラ権限を許可したので、カメラを起動
                        openCamera()
                    }
                }
            }
        }
    }
}

カメラ起動

  1. カメラIDを取得します。Camera2 APIではCameraManagerでカメラIDを取得します。また、カメラIDはIntではなく、Stringになります。
  2. [注意点]:SurfaceViewを使う場合、プレビューサイズを設定することができません(TextureViewならできます)。そこで、SurfaceViewの形を正方形にし、幅と高さをもっとも長い辺に合わせることで、アスペクト比が保たれたプレビューを実現します。
  3. カメラに接続します。接続したら、cameraDeviceStateCallbackコールバックが呼ばれます。
  4. カメラのプレビューのリクエストを作ります。
  5. カメラキャプチャーのコールバックを設定します。
  6. カメラのキャプチャーを開始します。
MyActivity.kt
private var cameraManager: CameraManager? = null
private var cameraDevice: CameraDevice? = null
private var captureRequest: CaptureRequest? = null
private var cameraCaptureSession: CameraCaptureSession? = null

// カメラの接続状態のコールバック
private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
    override fun onOpened(device: CameraDevice) {
        cameraDevice = device

        cameraDevice?.let { cameraDevice ->
            // 出力先
            val surfaceList: ArrayList<Surface> = arrayListOf()
            surfaceHolder?.let {
                surfaceList.add(it.surface)
            }

            try {
                // カメラのプレビューを出力するリクエストをここで作っておく。cameraCaptureSessionStateCallbackの中で使う
                captureRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
                    surfaceList.forEach {
                        addTarget(it)
                    }
                }.build()

                // カメラキャプチャーのコールバックを設定
                cameraDevice.createCaptureSession(
                    surfaceList,
                    cameraCaptureSessionStateCallback,
                    null
                )
            } catch (cameraAccessException: CameraAccessException) {
                cameraAccessException.printStackTrace()
            }
        }
    }

    override fun onDisconnected(device: CameraDevice) {
        cameraDevice = null
    }

    override fun onError(device: CameraDevice, error: Int) {
        cameraDevice = null
    }
}

// カメラキャプチャーのコールバック
private val cameraCaptureSessionStateCallback = object : CameraCaptureSession.StateCallback() {
    override fun onActive(session: CameraCaptureSession) {
        super.onActive(session)
        // カメラキャプチャーセッションはリソース解放時に使うので、保存しておく。
        cameraCaptureSession = session
    }

    override fun onClosed(session: CameraCaptureSession) {
        super.onClosed(session)
    }

    override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
        val captureRequest: CaptureRequest = captureRequest ?: return
        try {
            // カメラのキャプチャーを開始
            cameraCaptureSession.setRepeatingRequest(
                captureRequest,
                null,
                null
            )
        } catch (cameraAccessException: CameraAccessException) {
            cameraAccessException.printStackTrace()
        }
    }

    override fun onConfigureFailed(session: CameraCaptureSession) {
    }

    override fun onReady(session: CameraCaptureSession) {
        super.onReady(session)
    }

    override fun onSurfacePrepared(session: CameraCaptureSession, surface: Surface) {
        super.onSurfacePrepared(session, surface)
    }
}

// Camera2 APIではCameraManagerでカメラIDを取得する。
// また、カメラIDはIntではなく、Stringになる
private fun getCameraId(): String? {
    val cameraManager = cameraManager ?: return null

    cameraManager.cameraIdList.firstOrNull { cameraId ->
        cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
    }?.let { cameraId ->
        return cameraId
    }

    return null
}

private fun openCamera() {
    val context = context ?: return
    val cameraManager: CameraManager = cameraManager ?: return
    val cameraId = getCameraId() ?: return

    if (ActivityCompat.checkSelfPermission(
            context,
            Manifest.permission.CAMERA
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        return
    }

    try {
        // [注意点]:SurfaceViewを使う場合、プレビューサイズを設定することができない(TextureViewならできる)。
        // そこで、SurfaceViewの形を正方形にし、幅と高さをもっとも長い辺に合わせることで、アスペクト比が保たれたプレビューを実現する。
        val size = max(binding.surfaceView.width, binding.surfaceView.height)
        updateSurfaceSize(binding.surfaceView, size, size)

        // CameraManager.openCameraでカメラに接続する
        // カメラの接続状態はコールバックに渡される
        cameraManager.openCamera(cameraId, cameraDeviceStateCallback, null)
    } catch (cameraAccessException: CameraAccessException) {
        cameraAccessException.printStackTrace()
    }
}

SurfaceView

基本的にAndroidカメラのプレビュー表示(Camera API + SurfaceView)と同じです。

MyActivity.kt
private lateinit var binding: MainFragmentBinding
private var surfaceWidth = 1
private var surfaceHeight = 1

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    // データバインディングを使ったほうが楽
    binding = DataBindingUtil.inflate<MainFragmentBinding>(
            inflater,
            R.layout.main_fragment,
            container,
            false
    )

    // SurfaceViewにコールバックを設定
    binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder) {
            surfaceHolder = holder

            // Surfaceの生成が終わってから、カメラ権限を確認し、カメラを起動します。
            checkAndAskPermission()
        }

        override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
        ) {
            surfaceWidth = width
            surfaceHeight = height
        }

        override fun surfaceDestroyed(holder: SurfaceHolder) {
        }
    })

    return binding.root
}

[注意点]Surfaceサイズの調整

SurfaceViewの形を正方形にし、幅と高さを同じ値に設定すれば、プレビューのアスペクト比が保たれます。

MyActivity.kt
private fun updateSurfaceSize(view: View, width: Int, height: Int) {
    val param = view.layoutParams.apply {
        if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
            this.width = height
            this.height = width
        } else {
            this.width = width
            this.height = height
        }
    }
    view.layoutParams = param
}

リソース解放

アプリから離れたら時にカメラのリソースを解放します。

MyActivity.kt
override fun onStop() {
    super.onStop()

    cameraCaptureSession?.let {
        try {
            it.stopRepeating()
        } catch (cameraAccessException: CameraAccessException) {
            cameraAccessException.printStackTrace()
        }
        it.close()

        cameraCaptureSession = null
    }

    cameraDevice?.close()
    cameraDevice = null
}

GitHub

参考

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4