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

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

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

# 実装
## レイアウト
[Androidカメラのプレビュー表示（Camera API + SurfaceView）](https://qiita.com/Rei_2020/items/b1bb2e9fbec7d9cc0830)に載せたレイアウトと同じです。

```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
            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を追加する必要があります。

```xml: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）](https://qiita.com/Rei_2020/items/b1bb2e9fbec7d9cc0830)と同じです。

```kotlin: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. カメラのキャプチャーを開始します。

```kotlin: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）](https://qiita.com/Rei_2020/items/b1bb2e9fbec7d9cc0830)と同じです。

```kotlin: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の形を正方形にし、幅と高さを同じ値に設定すれば、プレビューのアスペクト比が保たれます。

```kotlin: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
}
```

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

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

# 参考

* [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 + TextureView）](https://qiita.com/Rei_2020/items/05630308ae8385433505)