【Kotlin/Android】カメラで写真撮影機能を実装する方法!

【Kotlin/Android】カメラで写真撮影機能を実装する方法!

この記事からわかること

  • Kotlin/Androidカメラ機能実装方法
  • CameraXライブラい使い方
  • 写真動画撮影する方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Androidアプリでカメラの写真撮影機能を実装する方法

Androidアプリでカメラを使用した写真撮影機能を実装する方法は大きく分けると2つあります。それぞれに一長一短があるので開発したいアプリの要件に応じて選定する必要があります。

Activity Result APIでカメラアプリで写真・動画を撮影する

Activity Result API」を使用した方法では端末にあるカメラアプリを起動して写真・動画を撮影することができます。この方法の場合はカメラ機能のカスタマイズ性はなく、ただ写真を撮影して画像だけ取得したい場合に使用します。

実装方法はIntentと「Activity Result API」を使用します。既にIntentとしてMediaStore.ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTUREが定義されており、これをregisterForActivityResult(Activity / Fragment) / rememberLauncherForActivityResult(Compose)を使用することで結果を取得することができます。

公式リファレンス:Camera Intents

写真撮影の実装方法

Jetpack ComposeでUIを実装していた場合はrememberLauncherForActivityResultを使用してランチャーを構築します。StartActivityForResultを指定しIntent(MediaStore.ACTION_IMAGE_CAPTURE)を渡すことでカメラアプリが起動し取得した画像がresult.data?.extras?.get("data")からBitmap型で取得することができます。ただしこの方法では解像度の低い画像しか取得できないので注意してください。

val capturedImage = viewModel.capturedImage.collectAsState()

val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val bitmap = result.data?.extras?.get("data") as? Bitmap
        viewModel.onImageCaptured(bitmap)
    }
}

// 撮影した画像を表示
capturedImage.value?.let { bitmap ->
    Image(
        bitmap = bitmap.asImageBitmap(),
        contentDescription = "撮影した写真",
        modifier = Modifier.size(200.dp)
    )
}

// カメラ起動ボタン
Button(onClick = {
    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    launcher.launch(intent)
}) {
    Text("カメラを起動")
}

解像度の高い画像(写真)を取得する

解像度の高い画像(写真)を取得したい場合はTakePictureを使用します。TakePictureを使用した場合は画像を直接取得できるわけではなくURI(Uniform Resource Identifier)経由での取得になります。URI(=参照)を取得するのでcontentResolverを介して実際のデータを取得する処理を実装する必要があります。rememberLauncherForActivityResultを使用するのは変わりませんが、Bitmap型へのキャストは以下のようになります。

var photoUri: Uri? = null
val context = LocalContext.current

val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.TakePicture()
) { success ->
    if (success) {
        val source = ImageDecoder.createSource(context.contentResolver, photoUri!!)
        val bitmap = ImageDecoder.decodeBitmap(source)
        viewModel.onImageCaptured(bitmap)
    }
}

Button(
    onClick = {
        // 一時保存ファイルを作成
        val photoFile = File(context.cacheDir, "photo_${System.currentTimeMillis()}.jpg")

        // FileProviderからURIを取得
        photoUri = FileProvider.getUriForFile(
            context,
            "${context.packageName}.provider", // AndroidManifestで設定したauthorities
            photoFile
        )
        // カメラ起動
        launcher.launch(photoUri)
    }
) {
    Text("カメラを起動")
}

またlaunchで実行する際には一時保存する先となるURIを指定する必要があります。そのため「AndroidManifest.xml」にも以下のように追記し、res/xml/file_paths.xmlを追加しておきます。


<application ...>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

<paths>
    <cache-path name="cache" path="." />
</paths>

またAndroidのカメラ機能はエミュレーターでも動作確認することが可能です。実際にカメラが映るわけではなくデモ画像が撮影できるだけですが、撮影すると画像として取得することができるので機能開発の段階では問題ありません。

動画撮影

動画撮影も写真撮影と同じでStartActivityForResultCaptureVideoの2つの方法があります。FileProviderの設定は同じように必要です。

private var videoUri: Uri? = null

// 動画撮影ランチャー
private val captureVideoLauncher =
    registerForActivityResult(ActivityResultContracts.CaptureVideo()) { success: Boolean ->
        if (success && videoUri != null) {
            // 撮影された動画を使う
            findViewById<VideoView>(R.id.videoView).apply {
                setVideoURI(videoUri)
                start()
            }
        }
    }

private fun dispatchCaptureVideo() {
    val videoFile = File(cacheDir, "video_${System.currentTimeMillis()}.mp4")
    videoUri = FileProvider.getUriForFile(
        this,
        "${packageName}.provider",
        videoFile
    )
    // 動画撮影開始
    captureVideoLauncher.launch(videoUri)
}

CameraXライブラリ

公式リファレンス:Choose Camera Library

Androidアプリでカスタマイズ性の高いカメラアプリやカメラ機能を実装したい場合はライブラリを使用する方法になります。ライブラリは複数用意されており、それぞれのざっくりとした違いは以下のようになっています。

低レベル層のカメラ制御を行うようなリッチなカメラアプリなら「Camera2」で、それ以外なら基本的に「CameraX」ライブラリを使用すれば問題ないです。「Camera」はサポート自体が終了しているので古いアプリとかで残っている処理をリプレースする際に見るくらいかもしれません。

CameraXでカメラアプリを実装する

CameraXでカメラアプリを実装する方法をまとめていきます。

依存関係の追加

CameraXを使用するためには依存関係を追加する必要があります。最新のバージョンや依存関係の詳細は「こちら(公式)」を参考にしてください。


val camerax_version = "1.5.0-rc01" 

// Camera機能を利用するためのCoreライブラリ
implementation("androidx.camera:camera-core:${camerax_version}") 
// CameraX を Android の camera2 API 上で動かすためのライブラリ 
// Android が提供する低レベルの camera2 API を内部的に利用して動作する。 
implementation("androidx.camera:camera-camera2:${camerax_version}") 

// ================== 以下は任意 ==================

// CameraX のライフサイクル連動ライブラリ 
// CameraX を Activity や Fragment のライフサイクルに自動でバインドしてくれる。 
// onStart でカメラ起動、onStop でカメラ解放、などを自動化できる。
implementation("androidx.camera:camera-lifecycle:${camerax_version}") 

// 動画録画用ライブラリ 
// 動画キャプチャ機能を提供する。録画開始・停止・保存ができるようになる。 
// 静止画だけなら不要。
implementation("androidx.camera:camera-video:${camerax_version}") 

// CameraX 用の UI コンポーネント(CameraView)を提供 
// SurfaceView / TextureView を使わず、CameraX 専用の View を使ってプレビュー表示できる。 
implementation("androidx.camera:camera-view:${camerax_version}") 

// CameraX と ML Kit Vision の統合ライブラリ 
// ML Kit(Google の画像認識ライブラリ)と簡単に連携できる。 
// バーコードスキャン・顔検出・テキスト認識などをリアルタイムで処理するのに便利。
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}") 

// CameraX 拡張機能ライブラリ 
// メーカー提供の拡張機能(ポートレートモード、HDR、夜景モードなど)を簡単に利用できる。 
// 端末依存なので、機能が使えるかは端末次第。
implementation("androidx.camera:camera-extensions:${camerax_version}")

パーミッションの宣言

必要なパーミッションを「AndroidManifest.xml」に宣言します。


<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera.any"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

写真撮影機能の実装

写真撮影機能をRepositoryとしてまとめて実装してみました。

class CameraRepository(
    private val context: Context
) {
    private var imageCapture: ImageCapture? = null

    /**
     * プレビューを PreviewView にバインドする
     */
    suspend fun startCamera(previewView: PreviewView) {
        val cameraProvider = getCameraProvider(context)
        val preview = Preview.Builder().build().also {
            it.surfaceProvider = previewView.surfaceProvider
        }

        imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 高速撮影
            .build()

        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        cameraProvider.unbindAll()
        cameraProvider.bindToLifecycle(
            previewView.context as androidx.lifecycle.LifecycleOwner,
            cameraSelector,
            preview,
            imageCapture
        )
    }

    /**
     * 写真を撮影して Uri を返す
     */
    suspend fun takePhoto(outputDir: File): Uri = suspendCancellableCoroutine { cont ->
        val imageCapture = imageCapture ?: return@suspendCancellableCoroutine cont.resumeWithException(
            IllegalStateException("ImageCapture not initialized")
        )

        val file = File(
            outputDir,
            SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
                .format(System.currentTimeMillis()) + ".jpg"
        )

        val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
        val executor: Executor = ContextCompat.getMainExecutor(context)

        imageCapture.takePicture(
            outputOptions,
            executor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    cont.resumeWithException(exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    cont.resume(Uri.fromFile(file))
                }
            }
        )
    }

    /**
     * CameraProvider を非同期で取得
     */
    private suspend fun getCameraProvider(context: Context): ProcessCameraProvider =
        suspendCancellableCoroutine { cont ->
            val future = ProcessCameraProvider.getInstance(context)
            future.addListener(
                { cont.resume(future.get()) },
                ContextCompat.getMainExecutor(context)
            )
        }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index