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

# Androidのカメラプレビュー
Androidでカメラのプレビューを表示するのに、大きく分けて２ステップがあります。

1. カメラを開く
2. カメラ画像をディスプレイに表示する

カメラを開く方法は２つあります。

* Camera API（android.hardware.Camera）
* Camera2 API（android.hardware.camera2）

カメラ画像をディスプレイに表示する方法も２つあります。

* SurfaceView
* TextureView

つまり、Androidのカメラプレビューを実現するのに、合計4つの方法があるというわけです。ネットでは４つの方法をまとめて紹介する記事がなかなか見つからないので、ここで紹介させていただきたいと思います。

本稿ではCamera API + SurfaceViewの実装方法と注意点を重点的に紹介します。

# 実装
## レイアウト

```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
カメラを使うのに、カメラのパーミッションが必要です。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>
```

### パーミッション要請
ユーザーにカメラ権限を要請します。

```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を特定します。
2. カメラ出力先をSurfaceViewのsurfaceHolderに指定します。
3. <b>[注意点]：端末の回転角度に合わせて、カメラの回転角度を設定します。</b>
4. カメラのパラメータを設定します。
5. カメラのプレビューを開始します。
6. <b>[注意点]：プレビューサイズに合わせて、SurfaceViewのサイズを調整します。</b>


```kotlin:MyActivity.kt
private var camera: Camera? = null
private var cameraParam: Camera.Parameters? = null

// カメラIDを特定
private fun getCameraId(): Int? {
	val cameraInfo = CameraInfo()
	for (i in 0 until Camera.getNumberOfCameras()) {
		Camera.getCameraInfo(i, cameraInfo)
		// 背面カメラ：Camera.CameraInfo.CAMERA_FACING_BACK
		// 前面カメラ：Camera.CameraInfo.CAMERA_FACING_FRONT
		if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
			return i
		}
	}

	return null
}

// カメラを開く
private fun openCamera() {
	val context = context ?: return
	val windowManager = activity?.windowManager ?: return

	// カメラIDを特定
	val cameraId = getCameraId() ?: return

	// カメラを取得
	camera = Camera.open(cameraId)
	camera?.let { camera ->
		try {
			// カメラ出力先を指定（ここではSurfaceViewのsurfaceHolderにする）
			camera.setPreviewDisplay(surfaceHolder)
		} catch (ioException: IOException) {
			ioException.printStackTrace()
		}

		// [注意点]：端末の回転角度に合わせて、カメラの回転角度を設定
		camera.setDisplayOrientation(
				when (windowManager.defaultDisplay.rotation) {
					Surface.ROTATION_0 -> 90
					Surface.ROTATION_90 -> 0
					Surface.ROTATION_180 -> 270
					Surface.ROTATION_270 -> 180
					else -> 0
				}
		)

		// カメラのパラメータを設定
		cameraParam = camera.parameters.apply {
			// プレビュー可能サイズを取得
			val size = supportedPreviewSizes.firstOrNull()
			size?.let { size ->
				// プレビューサイズを設定
				setPreviewSize(size.width, size.height)
			}
		}

		// カメラのパラメータを設定
		camera.parameters = camera.parameters.apply {
			val size = supportedPreviewSizes.firstOrNull()
			size?.let { size ->
				setPreviewSize(size.width, size.height)
			}
		}

		// カメラプレビューを開始
		camera.startPreview()

		// [注意点]：プレビューサイズに合わせて、SurfaceViewのサイズを調整する。
		// この処理が抜けると、プレビューのアスペクト比がおかしくなる可能性がある
		updateSurfaceSize(
			binding.surfaceView,
			camera.parameters.previewSize.width,
			camera.parameters.previewSize.height.
			surfaceWidth,
			surfaceHeight
		)
	}
}
```

## SurfaceView
SurfaceViewにコールバックを設定します。
Surfaceの生成が終わってから、カメラ権限を確認し、カメラを起動します。

```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

			camera?.let { camera ->
				updateSurfaceSize(
					binding.surfaceView,
					camera.parameters.previewSize.width,
					camera.parameters.previewSize.height.
					surfaceWidth,
					surfaceHeight
				)
			}
		}

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

	return binding.root
}
```

### [注意点]：Surfaceサイズの調整
表示のアスペクト比を調整し、画面いっぱいに表示します。

```kotlin:MyActivity.kt
private fun updateSurfaceSize(view: View, width: Int, height: Int, surfaceWidth: Int, surfaceHeight: Int) {
	val param = view.layoutParams
	var newWidth = width
	var newHeight = height

	// [注意点]：端末が縦の場合、設定する横幅と縦幅は逆になる
	if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
		newWidth = height
		newHeight = width
	}

	val scale1 = surfaceWidth / newWidth.toFloat()
	val scale2 = surfaceHeight / newHeight.toFloat()
	val scale = if (newWidth < surfaceWidth || newHeight < surfaceHeight) {
		max(scale1, scale2)
	} else if (newWidth < surfaceWidth) {
		scale1
	} else if (newHeight < surfaceHeight) {
		scale2
	} else {
		1.0f
	}

	// [注意点]：入力された横幅と縦幅を使う（SurfaceViewの幅と高さだと、アスペクト比が合わないことがある）
	param.width = (newWidth.toFloat() * scale).toInt()
	param.height = (newHeight.toFloat() * scale).toInt()
	view.layoutParams = param
}
```

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

```kotlin:MyActivity.kt
override fun onStop() {
	super.onStop()

	camera?.let {
		it.stopPreview()
		it?.release()
	}
}
```

# GitHub
* [https://github.com/Rei2020GitHub/MyPublicProject/tree/master/Camera_SurfaceView](https://github.com/Rei2020GitHub/MyPublicProject/tree/master/Camera_SurfaceView)

# 参考

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