6
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-19

Androidのカメラプレビュー

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

  1. カメラを開く
  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()
                    }
                }
            }
        }
    }
}

カメラ起動

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

GitHub

参考

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4