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()
}
}