5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHONE APPLIAdvent Calendar 2023

Day 7

Camera2 APIを使ってカメラアプリを作ってみた(Kotlin)

Last updated at Posted at 2023-11-26

起動編

カメラを起動させるためにまずは権限を確認する

permissionをすべて調べて許可されていなければポップアップを出して、許可を求めるようにしている

private val requiredPermissions = arrayOf(Manifest.permission.CAMERA)

companion object {
        private const val CAMERA_PERMISSION_REQUEST_CODE = 1001
    }

    (中略)
    //権限の確認を行いすべての権限が付与されていればtrueを返す
    private fun allPermissionsGranted() =
        requiredPermissions.all {
            ActivityCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
        }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        //カメラの権限が許可されていればカメラにアクセスし、されていなければ許可を求める
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
            if (allPermissionsGranted()) {
                startCamera()
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }

カメラを開く

カメラの許可を確認したのち、カメラにアクセスして開く

フェイスカメラとバックカメラのidを取得し、アクセスする

private val cameraManager by lazy {
        getSystemService(Context.CAMERA_SERVICE) as CameraManager
    }
    
 private fun getCameraId(wannaId: Int): String? {
        val cameraIds = cameraManager.cameraIdList
        for (cameraId in cameraIds) {
            val characteristics = cameraManager.getCameraCharacteristics(cameraId)
            val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
            //取得したカメラIDのリストの中から背面カメラ、フェイスカメラのIDと一致していればIDをカメラを起動させるメソッドに返却する
            when (wannaId) {
                0 -> {
                    if (facing == CameraCharacteristics.LENS_FACING_BACK) {
                        return cameraId
                    }
                }

                1 -> {
                    if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
                        return cameraId
                    }
                }
            }
        }
        return null
    }

カメラ起動

 private fun startCamera() {
        if (cameraView.isAvailable) {
            openCamera()
        } else {
            cameraView.surfaceTextureListener = surfaceTextureListener
        }
    }

private fun openCamera() {
        val backCameraId = getCameraId(0)
        val frontCameraId = getCameraId(1)
        if (backCameraId.isNullOrEmpty() || frontCameraId.isNullOrEmpty()) {
            // Handle case where back camera is not available
            return
        }
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.CAMERA
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }
        var openCameraId = ""
        when (switchCameraValue) {
            //select use camera
            0 -> openCameraId = backCameraId
            1 -> openCameraId = frontCameraId
        }
        cameraDevice?.close()

        cameraManager.openCamera(openCameraId, object : CameraDevice.StateCallback() {
            override fun onOpened(CameraDevice: CameraDevice) {
                cameraDevice = CameraDevice
                createCameraPreview()
            }

            //disconnect or error happen, close camera
            override fun onDisconnected(CameraDevice: CameraDevice) {
                cameraDevice?.close()
                cameraDevice = null
            }

            override fun onError(CameraDevice: CameraDevice, p1: Int) {
                cameraDevice?.close()
                cameraDevice = null
            }
        }, handler)
        //isOpen flag turn into true
        cameraIsOpen = true
    }

カメラビューをテクスチャに反映させる

    private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
        override fun onSurfaceTextureAvailable(
            surefaceTexture: SurfaceTexture,
            width: Int,
            height: Int
        ) {
            openCamera()
        }

        override fun onSurfaceTextureSizeChanged(
            surefaceTexture: SurfaceTexture,
            width: Int,
            height: Int
        ) {
        }

        override fun onSurfaceTextureDestroyed(surefaceTexture: SurfaceTexture): Boolean {
            return false
        }

        override fun onSurfaceTextureUpdated(surefaceTexure: SurfaceTexture) {
        }
    }

        private fun createCameraPreview() {

        val texture =
            cameraView.surfaceTexture ?: throw NullPointerException("texture has not found.")
        val viewSize = Point(
            getString(R.string.image_view_width).toInt(),
            getString(R.string.image_view_height).toInt()
        )
        texture.setDefaultBufferSize(viewSize.x, viewSize.y)
        val surface = Surface(texture)
        val rotation = CameraCharacteristics.SENSOR_ORIENTATION
        //need converting to getCameraCharacteristics
        cameraDevice?.let {
            val previewRequestBuilder =
                it.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            previewRequestBuilder.addTarget(surface)
            it.createCaptureSession(
                listOf(surface, imageReader.surface),
                object : CameraCaptureSession.StateCallback() {
                    override fun onConfigured(cameraCaptureSessionP0: CameraCaptureSession) {
                        cameraCaptureSession = cameraCaptureSessionP0
                        cameraCaptureSession.setRepeatingRequest(
                            previewRequestBuilder.build(),
                            null,
                            null
                        )
                    }

                    override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {
                    }
                },
                null
            )
        }
    }

動作編

写真を保存する

  private fun saveImage() {
        val timeStamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.JAPAN).format(Date())
        val fileName = "IMG$timeStamp.jpg"
        val file = File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
            "FolderName"
        )
        if (file.exists().not()) {
            file.mkdir()
        }
        val mediaFile = File(file.path + File.separator + fileName)
        imageReader.setOnImageAvailableListener(object : ImageReader.OnImageAvailableListener {
            override fun onImageAvailable(imageReader: ImageReader) {
                val opStream = FileOutputStream(mediaFile)
                val image = imageReader.acquireLatestImage()
                val buffer = image?.planes?.get(0)?.buffer
                val bytes = buffer?.let { ByteArray(it.remaining()) }
                buffer?.get(bytes)
                opStream.write(bytes)
                Log.d("FileSaved", "$file saved")
                opStream.close()
                image.close()
                Toast.makeText(this@MainActivity, "Image captured", Toast.LENGTH_SHORT)
                    .show()
            }
        }, handler)
    }

このコードで写真が撮られたときにトーストと呼ばれる浮かび上がる表示を出して動作したことを明示的に示す

 Toast.makeText(this@MainActivity, "Image captured", Toast.LENGTH_SHORT)
                    .show()

シャッター音を流す

今回は汎用性を高めるため拡張関数として作成した
import android.media.MediaPlayer

fun playSound(mediaPlayer: MediaPlayer) {
    mediaPlayer.apply {
        isLooping = false
        start()
    }
}

カメラの切り替え

//カメラ切り替え用のボタンを用意してクリックされたら呼び出したいカメラを切り替える

 override fun onClick(view: View) {
        when (view) {
            binding.switchCamera -> {
                changeSPCamera()
            }
    }
    
    private fun changeSPCamera() {
        //0 is back camera
        //1 is front camera
        switchCameraValue = when (switchCameraValue) {
            0 -> 1
            1 -> 0
            else -> {
                return
            }
        }
        openCamera()
    }

UI編

main activity xml.png

画面下部にボタンを設置

左からカメラ切り替えボタン、シャッターボタン、フォルダーボタンを設置する

シャッターボタンがクリックされたときに音源再生処理と写真保存処理を呼び出す

 override fun onClick(view: View) {
        when (view) {
          
            shutter -> {
            //シャッター音を流すメゾットは拡張関数として作ってある
                playSound(MediaPlayer.create(this, R.raw.camera_shutter))
                saveImage()
            }
    }


ツールバーの中に戻るボタンを実装

override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> {
                moveActivities(OpeningActivity::class.java)
                return true
            }
        }
        return super.onOptionsItemSelected(item)
    }
    

他のアプリとの移動・スマホのロックに関すること

アプリを中断しているときにカメラを閉じる
   private fun closeCamera() {
        cameraDevice?.close()
        cameraDevice = null
        cameraIsOpen = false
    }

    //this function run when move to other app or home screen.
    override fun onPause() {
        super.onPause()
        closeCamera()
    }

アプリを再開したら再びカメラにアクセスする

 override fun onResume() {
        super.onResume()
        //if camera is close
        if (cameraIsOpen.not()) {
            if (cameraView.isAvailable) {
                openCamera()
            } else {
                cameraView.surfaceTextureListener = surfaceTextureListener
            }
        }
    }

これをしないと顔認証でスマホのカメラが使えない。アプリがカメラを占有してしまい、androidのシステムが使用できない。(割り込み処理ができるような優先度は設定されていないようだ)

反省点

カメラで映している映像をテクスチャに投影することはでき、撮影も成功しているが、撮影した写真が縦向きに回転しておらず横倒しの状態になっていた。

IMG20231126145115_0.jpg

ジャイロセンサーから情報を読み取って回転させたりMatrixを使う方法を検討している。

編集履歴

2023/12/16

  • メソッドをメゾットと表記していた部分があったため修正
5
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?