Androidアプリにカメラ機能を開発する手順をわかりやすく解説!

Androidアプリを開発するとき、カメラや写真を使ったアプリを作りたいというケースはとても多いです。アプリ開発でデバイスの機能へアクセスするときは、初心者だと少しわかりにくい所も多いです。今回は、カメラの機能にアクセスする方法や注意点について解説していきます。

Androidでカメラ機能を使うためには

Androidアプリでカメラの機能を使うためには、以下の2つの方法があります。

  1. 標準で提供されているカメラを利用する方法
  2. 自分でカスタムしたカメラ作ってを利用する

それぞれ、利用するための手順が異なります。それぞれの利用方法を1つずつ紹介していきます。

Camera2 APIの呼び出し方と注意点

AndroidのCamera2 APIを使うためには android.hardware.camera2 をインポートすることで利用することができます。標準カメラを利用する場合と、カスタムしたカメラを利用する場合を紹介していきます。
    import android.hardware.camera2;

標準のカメラ機能の利用

開発しているAndroidアプリで、標準のカメラアプリを呼び出す手順を紹介します。

機能を利用を定義する

Androidアプリがカメラの機能にアクセスするためには、AndroidManifest.xmlに以下のuses-featureを定義します。targetSdkVersion 30以上の場合、パーミッションではなくqueriesを定義する必要があります。
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
             package="com.example.sample_profile">
    
            <uses-feature android:name="android.hardware.camera" />
    
        <!-- queries is needed if targetSdkVersion is equal or over 30 -->
        <queries>
        <!-- Camera -->
            <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
            </intent>
        </queries>
    
        <!-- 省略 -->
    
    </manifest>

コードで標準カメラを呼び出す

開発しているAndroidアプリで標準カメラを利用するには、Intentというクラスを使って呼び出します。以下のコードで、呼び出しの手順を解説します。

    /* カメラの機能が使えるかチェック */
    if (!packageManager && packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
    
        /* 標準カメラを呼び出すためのIntentを作成*/
        Intent intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    
        /* 標準カメラを呼び出すことができるかチェック */
        if(intent && intent.resolveActivity(packageManager)){
    
            /* 標準カメラの呼び出し */
            startActivityForResult(intent, REQUEST_IMAGE_CAPTURE)
        }
    }

カスタムカメラ機能の利用

Androidで提供されているCamera2 APIというものを利用します。Camera2 APIを使ってカメラ機能を実装することで、オリジナルのUIにしたり標準アプリでは提供されていない機能を追加したりすることができるようになります。

パーミッションを追加する

AndroidアプリがCamera2 APIにアクセスするためには、パーミッションを追加する必要があります。パーミッションとは、アプリがカメラの機能を勝手に利用できないように、Android OSがユーザーに許可を取る必要があるように制限をしています。AndroidManifest.xmlに以下の追加することでパーミッションを追加できます。

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
                package="com.example.sample_profile">
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-feature android:name="android.hardware.camera" />
    
        <!-- queries is needed if targetSdkVersion is equal or over 30 -->
        <queries>
            <!-- Camera -->
            <intent>
                <action android:name="android.media.action.IMAGE_CAPTURE" />
            </intent>
            </queries>
    
        <!-- 省略 -->
    </manifest>

作る機能をピックアップ

カメラは次の順番で作っていきます。

  1. Layoutの実装
  2. カメラ有無チェックとアクセス
  3. プレビューの実装
  4. カメラアクティビティの実装

1. Layoutの実装

カメラのプレビューを表示するため、アクティビティとレイアウトを実装していきます。プレビューには、テクスチャと撮影用のボタンを設置しカメラっぽくします。また、Intentを設定することで標準カメラと同様に画面を呼び出せるようにしていきます。

次にCameraActivity名前で、カメラのアクティビティを作っていきましょう。onCreateで先ほど作ったレイアウトファイルを読み込みます。また、後ほどプレビューを起動するためのsetupメソッドを実装しておきます。

    package com.example.sample_profile
    import android.app.Activity
    import android.graphics.SurfaceTexture
    import android.os.Bundle
    import android.view.TextureView
    import androidx.appcompat.app.AppCompatActivity
    
    class CameraActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.camera)
            setup()
        }
    
        fun setup() {
            // Previewの起動処理
        }
    }

2. カメラ有無のチェックとアクセス

ここからは、カメラの処理を実装するためのCameraProcessorクラスを実装していきます。

    /* ... */
    import android.R
    import android.Manifest
    import android.app.AlertDialog
    import android.content.pm.PackageManager
    import androidx.core.app.ActivityCompat
    import android.view.TextureView
    import android.app.Activity
    
    class CameraProcessor {
    
        private var cameraDevice: CameraDevice? = null
    
        private val REQUEST_CAMERA_PERMISSION = 1
    
        private var activity: Activity
        private val textureView: TextureView
    
        constructor(textureView: TextureView, activity: Activity) {
            this.textureView = textureView
            this.activity = activity
        }
    
        private fun showDialogCameraPermission() {
            if(!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA)) {
                ActivityCompat.requestPermissions(
                    activity, arrayOf(Manifest.permission.CAMERA),
                    REQUEST_CAMERA_PERMISSION
                )
                return
            }
    
            AlertDialog.Builder(activity)
                .setMessage("R string request permission")
                .setPositiveButton(R.string.ok) { dialog, which ->
                    ActivityCompat.requestPermissions(
                        activity, arrayOf(Manifest.permission.CAMERA),
                        REQUEST_CAMERA_PERMISSION
                    )
                }
                .setNegativeButton(R.string.cancel) { dialog, which ->  }
                .create()
        }
    
        fun setup() {
            // カメラ搭載の有無をチェック
            if (activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) === false) return
    
            // カメラのアクセス権限を確認して、なければ許可を求める
            val currentPermssion = ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
            if (currentPermssion != PackageManager.PERMISSION_GRANTED) {
                showDialogCameraPermission()
                return
            }
    
            // すでにカメラが起動していたら何もしない
            if (cameraDevice !== null) {
                return
            }
    
            // 背面カメラの取得
        }
    
    }

3. プレビューの実装

アクセスが許可されていない場合は、許可を求める処理を呼び出します。ユーザーの許可が取得できたら、プレビューの初期化を行ってカメラのセッションを起動します。

    /* ... */
    import android.hardware.camera2.*
    
    class CameraProcessor {
        /* ... */
    
        private var mBackgroundThread: HandlerThread? = null
        private var mBackgroundHandler: Handler? = null
    
        /* constructor... */
    
        private fun startBackgroundThread() {
            val thread = HandlerThread("CameraBackground");
    
            mBackgroundHandler = Handler(thread.looper)
            mBackgroundThread = thread
    
            thread.start();
    
        }
    
        private fun stopBackgroundThread() {
            mBackgroundThread?.quitSafely()
    
            mBackgroundThread?.join()
            mBackgroundThread = null
            mBackgroundHandler = null
        }
    
        fun setup() {
    
            /* ... */
    
            // 背面カメラの取得
            val cameraId: String = cameraManager.cameraIdList[0]
            cameraManager.openCamera(cameraId, object: CameraDevice.StateCallback() {
                override fun onOpened(cameraDevice: CameraDevice) {
                    // カメラが取得できたらプレビューを生成する
                    this@CameraProcessor.cameraDevice = cameraDevice
                    createCameraPreviewSession()
                }
    
                override fun onDisconnected(cameraDevice: CameraDevice) {
                    // カメラの取得に失敗したら終了する
                    cameraDevice.close()
                    this@CameraProcessor.cameraDevice = null
                    stopBackgroundThread()
                }
    
                override fun onError(cameraDevice: CameraDevice, error: Int) {
                    // カメラの取得に失敗したら終了する
                    cameraDevice.close()
                    this@CameraProcessor.cameraDevice = null
                    stopBackgroundThread()
                }
            }, mBackgroundHandler)
        }
    
        fun createCameraPreviewSession() {
            // カメラ起動処理
        }
    }

カメラに無事アクセスできた場合は、カメラの映像をテクスチャに表示してプレビューにします。

    import android.os.Handler
    import android.os.HandlerThread
    import android.view.Surface
    import android.content.Context
    import android.media.ImageReader
    import android.graphics.ImageFormat
    import android.hardware.camera2.params.OutputConfiguration
    import android.hardware.camera2.params.SessionConfiguration
    import java.util.*
    
    class CameraProcessor {
        /* ... */
        private var cameraCaptureSession: CameraCaptureSession? = null
    
        private fun createPreviewRequest(cameraDevice: CameraDevice): CaptureRequest {
    
            val surface = Surface(this.textureView.surfaceTexture)
            val imageReader = ImageReader.newInstance(
                this.textureView.width,
                this.textureView.height,
                ImageFormat.JPEG,
                2
            )
    
            // プレビューテクスチャの設定
            val previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            previewRequestBuilder.addTarget(surface)
            previewRequestBuilder.set(
                CaptureRequest.CONTROL_AF_MODE,
                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
            )
    
            return previewRequestBuilder.build()
        }
    
        fun createCameraPreviewSession() {
            // カメラ起動処理
            if (this.cameraDevice === null) return
    
            val cameraDevice = this.cameraDevice!!
    
            // プレビュー用のテクスチャ取得
            val texture = this.textureView.surfaceTexture
    
            val surface = Surface(texture)
            val imageReader = ImageReader.newInstance(
                this.textureView.width,
                this.textureView.height,
                ImageFormat.JPEG,
                2
            )
    
            val surfaces: List = Arrays.asList(surface, imageReader?.surface)
    
            val type = SessionConfiguration.SESSION_REGULAR
            val configurations: List = surfaces.map { OutputConfiguration(it) }
            val executor = this.activity.mainExecutor
    
            val previewRequest = this.createPreviewRequest(cameraDevice)
            val callback = object: CameraCaptureSession.StateCallback() {
                override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
                    // 起動成功時に呼ばれる
                    this@CameraProcessor.cameraCaptureSession = cameraCaptureSession
                    cameraCaptureSession.setRepeatingRequest(previewRequest, null, null)
                }
    
                override fun onConfigureFailed(session: CameraCaptureSession) {
                    // 起動失敗時に呼ばれる
                }
            }
    
            // 起動
            val configuration = SessionConfiguration(type, configurations, executor, callback)
            cameraDevice.createCaptureSession(configuration)
        }
    }

4. カメラアクティビティの実装

撮影をしてBitmapとしてデータを保持する処理を実装します。Bitmapはローカルに保存したり、ImageViewにセットしたりして利用することができます。

    class CameraProcessor {
        /* ... */
    
        fun take() {
            this.cameraCaptureSession?.stopRepeating()
            this.bitmap = this.textureView.bitmap
    
            val request = this.createPreviewRequest(this.cameraDevice!!)
            this.cameraCaptureSession?.setRepeatingRequest(request, null, null)
        }
    }