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

# Camera2 APIによるカメラのプレビュー表示について
[Androidカメラのプレビュー表示（Camera2 API + SurfaceView）](https://qiita.com/Rei_2020/items/63fe4216835141d03c4a)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。

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

本稿では[Androidカメラのプレビュー表示（Camera2 API + SurfaceView）](https://qiita.com/Rei_2020/items/63fe4216835141d03c4a)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。

# 実装
## レイアウト
SurfaceViewをTextureViewに置き換えます。

```xml: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に置き換えます。

```kotlin: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からサポートサイズを取得します。

```kotlin: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の中でプレビューサイズと回転角度を設定します。

```kotlin: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. 行列をカメラに渡し、プレビューを調整します。

```kotlin: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）](https://qiita.com/Rei_2020/items/63fe4216835141d03c4a)に述べたように、Camera APIをCamera2 APIに変えると、カメラプレビューの実装が難しくなります。特にCamera2 APIには、Camera.Parameters.setPreviewDisplay()のようなプレビューサイズを設定するAPIがなく、Camera.setDisplayOrientationのようなカメラを回転させるAPIもないので、プレビューを正しく表示するのが難しいところです。

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

本稿では[Androidカメラのプレビュー表示（Camera2 API + SurfaceView）](https://qiita.com/Rei_2020/items/63fe4216835141d03c4a)に述べた実装との差をわかりやすく説明するために、実装の差分のみを述べます。

# 実装
## レイアウト
SurfaceViewをTextureViewに置き換えます。

```xml: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に置き換えます。

```kotlin: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からサポートサイズを取得します。

```kotlin: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の中でプレビューサイズと回転角度を設定します。

```kotlin: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. 行列をカメラに渡し、プレビューを調整します。

```kotlin: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
* [https://github.com/Rei2020GitHub/MyPublicProject/tree/master/Camera2_TextureView](https://github.com/Rei2020GitHub/MyPublicProject/tree/master/Camera2_TextureView)

# 参考

* [Androidカメラのプレビュー表示（Camera API + SurfaceView）](https://qiita.com/Rei_2020/items/b1bb2e9fbec7d9cc0830)
* [Androidカメラのプレビュー表示（Camera API + TextureView）](https://qiita.com/Rei_2020/items/2ed5accba5916bca00a3)
* [Androidカメラのプレビュー表示（Camera2 API + SurfaceView）](https://qiita.com/Rei_2020/items/63fe4216835141d03c4a)