Androidのカメラプレビュー
Androidでカメラのプレビューを表示するのに、大きく分けて2ステップがあります。
- カメラを開く
- カメラ画像をディスプレイに表示する
カメラを開く方法は2つあります。
- Camera API(android.hardware.Camera)
- Camera2 API(android.hardware.camera2)
カメラ画像をディスプレイに表示する方法も2つあります。
- SurfaceView
- TextureView
つまり、Androidのカメラプレビューを実現するのに、合計4つの方法があるというわけです。ネットでは4つの方法をまとめて紹介する記事がなかなか見つからないので、ここで紹介させていただきたいと思います。
本稿では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
カメラを使うのに、カメラのパーミッションが必要です。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>
パーミッション要請
ユーザーにカメラ権限を要請します。
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()
					}
				}
			}
		}
	}
}
カメラ起動
- カメラIDを特定します。
- カメラ出力先をSurfaceViewのsurfaceHolderに指定します。
- [注意点]:端末の回転角度に合わせて、カメラの回転角度を設定します。
- カメラのパラメータを設定します。
- カメラのプレビューを開始します。
- [注意点]:プレビューサイズに合わせて、SurfaceViewのサイズを調整します。
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の生成が終わってから、カメラ権限を確認し、カメラを起動します。
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サイズの調整
表示のアスペクト比を調整し、画面いっぱいに表示します。
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
}
リソース解放
アプリから離れたら時にカメラを解放しましょう。
MyActivity.kt
override fun onStop() {
	super.onStop()
	camera?.let {
		it.stopPreview()
		it?.release()
	}
}

