LoginSignup
3
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-21

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

Androidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。

SurfaceViewを使う場合、画面の回転はやってくれるので、アスペクト比の調整のみを実装すればよいですが、TextureViewを使う場合、画面の回転はやってくれないので、そちらの実装もやらなければなりません。

本稿ではAndroidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。

実装

レイアウト

SurfaceViewをTextureViewに置き換えます。

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をTextureViewに置き換える -->
        <TextureView
            android:id="@+id/texture_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>

カメラ起動

surfaceHolderをsurfaceTextureに置き換えます。

MyActivity.kt
private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
    override fun onOpened(device: CameraDevice) {
        cameraDevice = device

        cameraDevice?.let { cameraDevice ->
            val surfaceList: ArrayList<Surface> = arrayListOf()

            // surfaceHolderをsurfaceTextureに置き換えます。
            surfaceTexture?.let {
                surfaceList.add(Surface(it))
            }

            try {
                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
    }
}

Camera2 APIにCamera.Parameters.setPreviewDisplay()のようなプレビューサイズを直接的に設定するAPIがないが、TextureView.surfaceTexture.setDefaultBufferSize()でプレビューサイズを設定できます。そこで、まずCameraCharacteristicsからサポートサイズを取得します。

MyActivity.kt
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 {
        // CameraCharacteristicsからカメラのサポートサイズを取得します。
        cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(SurfaceTexture::class.java)?.getOrNull(0)?.let { size ->
            updateSurfaceSize(binding.textureView, size.width, size.height)
        }

        cameraManager.openCamera(cameraId, cameraDeviceStateCallback, null)
    } catch (cameraAccessException: CameraAccessException) {
        cameraAccessException.printStackTrace()
    }
}

TextureView

  1. SurfaceViewをTextureViewに置き換えます。
  2. [注意点]:onSurfaceTextureSizeChangedの中でプレビューサイズと回転角度を設定します。
MyActivity.kt
// SurfaceHolderをSurfaceTextureに置き換える
// private var surfaceHolder: SurfaceHolder? = null
private var surfaceTexture: SurfaceTexture? = null

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    binding = DataBindingUtil.inflate<MainFragmentBinding>(
            inflater,
            R.layout.main_fragment,
            container,
            false
    )

    // SurfaceViewをTextureViewに置き換える
    binding.textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {

        override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
            surfaceTexture = surface
            surfaceWidth = width
            surfaceHeight = height
            checkAndAskPermission()
        }

        override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
            surfaceTexture = surface.apply {
                // [注意点]:ここでプレビューサイズと回転角度を設定する。
                updateSurfaceTexture(this, width, height)
            }
        }

        override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {

        }

        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
            return false
        }
    }

    return binding.root
}

[注意点]:プレイビューサイズと回転角度の設定

  1. 幅と高さを決めます。端末が横の場合、幅=width、高さ=heightになります。端末が縦の場合、幅=height、高さ=widthになります。
  2. プレビューの中心点を軸に、プレビューを回転させ、かつ正しいアスペクト比にする行列を作ります。
  3. 行列をカメラに渡し、プレビューを調整します。
MyActivity.kt
private fun updateSurfaceTexture(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
    // 端末が横の場合、幅=width、高さ=heightになります。
    var newWidth = width
    var newHeight = height

    // 端末が縦の場合、幅=height、高さ=widthになります。
    if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
        newWidth = height
        newHeight = width
    }

    // setDefaultBufferSizeでプレビューサイズを設定する。
    surfaceTexture.setDefaultBufferSize(newWidth, newHeight)

    val transform = Matrix()
    val center = Point(newWidth / 2, newHeight / 2)

    // プレビューの中心点を軸に、プレビューを回転させる行列を作る
    when (activity?.windowManager?.defaultDisplay?.rotation) {
        Surface.ROTATION_90 -> {
            transform.postRotate(270.0f, center.x.toFloat(), center.y.toFloat())
        }
        Surface.ROTATION_180 -> {
            transform.postRotate(180.0f, center.x.toFloat(), center.y.toFloat())
        }
        Surface.ROTATION_270 -> {
            transform.postRotate(90.0f, center.x.toFloat(), center.y.toFloat())
        }
    }

    // 正しいアスペクト比に戻す
    when (activity?.windowManager?.defaultDisplay?.rotation) {
        Surface.ROTATION_90, Surface.ROTATION_270 -> {
            transform.postScale(newWidth.toFloat() / newHeight.toFloat() * newWidth.toFloat() / newHeight.toFloat(),
                    1.0f,
                    center.x.toFloat(),
                    center.y.toFloat())
        }
    }

    // カメラに行列を渡し、プレビューを調整する
    binding.textureView.setTransform(transform)

    surfaceWidth = width
    surfaceHeight = height
}

まとめ

Camera2 API + TextureViewの方法は、Camera2 API + SurfaceView方法とCamera API + TextureView方法よりも自由度が高いですが、プレビュー表示処理がだいぶ複雑になります。
個人的には仕様制限がないのプレビュー表示について
Androidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。

SurfaceViewを使う場合、画面の回転はやってくれるので、アスペクト比の調整のみを実装すればよいですが、TextureViewを使う場合、画面の回転はやってくれないので、そちらの実装もやらなければなりません。

本稿ではAndroidカメラのプレビュー表示(Camera2 API + SurfaceView)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。

実装

レイアウト

SurfaceViewをTextureViewに置き換えます。

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をTextureViewに置き換える -->
        <TextureView
            android:id="@+id/texture_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>

カメラ起動

surfaceHolderをsurfaceTextureに置き換えます。

MyActivity.kt
private val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
    override fun onOpened(device: CameraDevice) {
        cameraDevice = device

        cameraDevice?.let { cameraDevice ->
            val surfaceList: ArrayList<Surface> = arrayListOf()

            // surfaceHolderをsurfaceTextureに置き換えます。
            surfaceTexture?.let {
                surfaceList.add(Surface(it))
            }

            try {
                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
    }
}

Camera2 APIにCamera.Parameters.setPreviewDisplay()のようなプレビューサイズを直接的に設定するAPIがないが、TextureView.surfaceTexture.setDefaultBufferSize()でプレビューサイズを設定できます。そこで、まずCameraCharacteristicsからサポートサイズを取得します。

MyActivity.kt
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 {
        // CameraCharacteristicsからカメラのサポートサイズを取得します。
        cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(SurfaceTexture::class.java)?.getOrNull(0)?.let { size ->
            updateSurfaceSize(binding.textureView, size.width, size.height)
        }

        cameraManager.openCamera(cameraId, cameraDeviceStateCallback, null)
    } catch (cameraAccessException: CameraAccessException) {
        cameraAccessException.printStackTrace()
    }
}

TextureView

  1. SurfaceViewをTextureViewに置き換えます。
  2. [注意点]:onSurfaceTextureSizeChangedの中でプレビューサイズと回転角度を設定します。
MyActivity.kt
// SurfaceHolderをSurfaceTextureに置き換える
// private var surfaceHolder: SurfaceHolder? = null
private var surfaceTexture: SurfaceTexture? = null

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    binding = DataBindingUtil.inflate<MainFragmentBinding>(
            inflater,
            R.layout.main_fragment,
            container,
            false
    )

    // SurfaceViewをTextureViewに置き換える
    binding.textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {

        override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
            surfaceTexture = surface
            surfaceWidth = width
            surfaceHeight = height
            checkAndAskPermission()
        }

        override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
            surfaceTexture = surface.apply {
                // [注意点]:ここでプレビューサイズと回転角度を設定する。
                updateSurfaceTexture(this, width, height)
            }
        }

        override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {

        }

        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
            return false
        }
    }

    return binding.root
}

[注意点]:プレイビューサイズと回転角度の設定

  1. 幅と高さを決めます。端末が横の場合、幅=width、高さ=heightになります。端末が縦の場合、幅=height、高さ=widthになります。
  2. プレビューの中心点を軸に、プレビューを回転させ、かつ正しいアスペクト比にする行列を作ります。
  3. 行列をカメラに渡し、プレビューを調整します。
MyActivity.kt
private fun updateSurfaceTexture(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
    // 端末が横の場合、幅=width、高さ=heightになります。
    var newWidth = width
    var newHeight = height

    // 端末が縦の場合、幅=height、高さ=widthになります。
    if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
        newWidth = height
        newHeight = width
    }

    // setDefaultBufferSizeでプレビューサイズを設定する。
    surfaceTexture.setDefaultBufferSize(newWidth, newHeight)

    val transform = Matrix()
    val center = Point(newWidth / 2, newHeight / 2)

    // プレビューの中心点を軸に、プレビューを回転させる行列を作る
    when (activity?.windowManager?.defaultDisplay?.rotation) {
        Surface.ROTATION_90 -> {
            transform.postRotate(270.0f, center.x.toFloat(), center.y.toFloat())
        }
        Surface.ROTATION_180 -> {
            transform.postRotate(180.0f, center.x.toFloat(), center.y.toFloat())
        }
        Surface.ROTATION_270 -> {
            transform.postRotate(90.0f, center.x.toFloat(), center.y.toFloat())
        }
    }

    // 正しいアスペクト比に戻す
    when (activity?.windowManager?.defaultDisplay?.rotation) {
        Surface.ROTATION_90, Surface.ROTATION_270 -> {
            transform.postScale(newWidth.toFloat() / newHeight.toFloat() * newWidth.toFloat() / newHeight.toFloat(),
                    1.0f,
                    center.x.toFloat(),
                    center.y.toFloat())
        }
    }

    // カメラに行列を渡し、プレビューを調整する
    binding.textureView.setTransform(transform)

    surfaceWidth = width
    surfaceHeight = height
}

まとめ

Camera2 API + TextureView方法は、Camera2 API + SurfaceView方法とCamera API + TextureView方法よりも自由度が高いですが、プレビュー表示処理がだいぶ複雑になります。また、Camera2 APIの使い方もCamera APIより複雑ですし、カメラのプレビューサイズを直接に指定するAPIもありません。

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