以下の内容はhttps://android-java.hatenablog.jp/entry/android-camerax-applicationより取得しました。


Camera Xでカメラアプリを作ってみよう(言語:Kotlin)~ 完全版 ~

今回は Android Studio と Kotlin 言語を使って静止画と動画が撮影できる基本的なカメラアプリの作り方をご紹介します。

ベースは Android Studio の公式サイトの「Camera X」サンプルコードを参考にしていますが、時間をかけて調べなければわからない部分も解決済みとなっていますので、開発にあまり時間をかけたくない方や、カメラアプリ開発の初級者の方におすすめします。

なお、Android 端末は非常に多種多様ですので、相性が悪くすんなりと動かない可能性もありますが、エミュレーターおよび2種類の実機での動作確認はできていますので、万が一正しく動かない場合でも少しの修正で動くようになるはずです。

~ もくじ ~

~ 開発環境 ~

開発:Android Studio (2024.2.1 Patch 3)
言語:Kolin
使用API:Camera X
動作確認:OPPO RENO7 A(Android 13)、SHARP S5-SH(Android 11)

~ 開発の流れ ~

※編集するファイルは、MainActivity.ktactivity_main.xmlbuild.gradle.kts(:app)strings.xmlAndroidManifest.xml の5種類のみです。

1.プロジェクト作成

2.初期設定

3.gradle ファイル編集

4.マニフェストファイル編集

5.レイアウトファイル編集

6.string.xml ファイル編集

7.MainActivity.kt を編集する(8工程)

8.動作テスト

~ アプリ画面 ~

背面カメラからの映像の上に静止画撮影と動画撮影のボタンがあるシンプルなカメラアプリです。

<動作仕様>

実行すると背面カメラのプレビュー画面と2つのボタンが表示されます。そして「PHOTO」ボタンを押すと静止画像が撮影され、「VIDEO」ボタンを押すと動画の撮影がスタートします。動画の撮影がスタートすると「VIDEO」ボタンのラベルが「STOP」に変わり、「STOP」ボタンを押すと動画の撮影が停止します。

~ 詳細解説 ~

1.プロジェクトの作成

プロジェクトの作成画面で「Empty Views Activity」を選択して「Next」ボタンをクリックします。

プロジェクト名は自由です。開発言語は「Kotlin」を選択します。プロジェクト名の入力と開発言語の選択が終わったら「Finish」をクリックして、Android Studio がプロジェクトを作成するのを少し待ちます。

2.初期設定

プロジェクトの作成が終わったら「File」⇒「Project Structure」をクリックしてプロジェクトの設定画面を開きます。そして、右上のメニューから「Propertys」をクリックして、その中にある「Compile Sdk Version」を最新のバージョン(現在は35)に変更します。これをしないとエラーが出て実行できませんので必ず変更してください。

<ポイント>

Android Studio の下部には進捗状況を表す青いバーがあります。このバーが表示されている時は Android Studio が忙しい状態なので、コードの編集はなるべく控えた方が良いでしょう。

3.gradle ファイルの編集

「build.gradle.kts (Module:app)」ファイル内の android { } 内に buildFeatures 項目を追加します。公式サイトでは「viewBinding ture」と間違って記述されていますが、以下の記述「viewBinding = true」が正解です。

android {

    ...省略...

    buildFeatures {
        viewBinding = true
    }
}

続けて dependencies { } 内に次の6つの項目を追加します。

dependencies {

    ...省略...

 

    //追加
    implementation("androidx.camera:camera-core:1.4.1")
    implementation("androidx.camera:camera-camera2:1.4.1")
    implementation("androidx.camera:camera-lifecycle:1.4.1")
    implementation("androidx.camera:camera-video:1.4.1")
    implementation("androidx.camera:camera-view:1.4.1")
    implementation("androidx.camera:camera-extensions:1.4.1")
}

追加が終わったら編集画面の上部に表示される「Sync Now…」をクリックしてインポート処理が終わるのを待ちます。

4.Manifest ファイルの編集

次に Manifest ファイルに次の4項目を追加します。

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

5.レイアウトファイルの編集

レイアウトファイル ( activity_main.xml ) にプレビュー映像を表示するために必要な PreviewView と、静止画用の撮影ボタンと動画用の撮影ボタンを追加します。また、translationZ で重なりの優先順位を設定しています。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- プレビュー表示用 -->
    <androidx.camera.view.PreviewView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:translationZ="0dp"
        android:layout_height="match_parent" />

    <!-- 動画撮影ボタン -->
    <Button
        android:id="@+id/video"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="VIDEO"
        android:translationZ="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <!-- 静止画撮影ボタン -->
    <Button
        android:id="@+id/photo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="PHOTO"
        android:translationZ="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

<画面イメージ>

6.strings.xml ファイルの編集

res フォルダ内にある values フォルダ内の string.xml ファイルで、今回作るカメラアプリのアプリ名、静止画撮影ボタンと動画撮影ボタンのラベル名を設定します。

<resources>
   
<!-- アプリ名 -->
    <string name="app_name">Camera X Sample</string>
    <!-- 静止画撮影ボタンのラベル -->
    <string name="take_photo">PHOTO</string>
    <!-- 動画撮影ボタンのラベル -->
    <string name="start_capture">VIDEO</string>
    <!-- 動画撮影ストップボタンのラベル -->
    <string name="stop_capture">STOP</string>
</resources>

7.MainActivity.kt ファイルの編集

① 権限チェックコード追加

ユーザーに権限を取得するコードを記述します。このカメラアプリに必要な権限は以下の3つです。

  • カメラ
    Manifest.permission.CAMERA
  • 録音
    Manifest.permission.RECORD_AUDIO
  • 外部ストレージ書き込み
    Manifest.permission.WRITE_EXTERNAL_STRAGE
    ※WRITE_EXTERNAL_STRAGE は Android Pie (API Level 28) Version 9 以下の端末で必要になる権限です。

権限チェック処理(1/3)、起動した直後に必要な権限をチェックします。※このコードは onCreate() の { } の中に記述します。

if (allPermissionsGranted()) {
    //プレビュー開始
    startPreview()
} else {
    //必要な権限がない場合は権限の確認処理
    ActivityCompat.requestPermissions(
        this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
    )
}

権限チェック処理(2/3)、これは権限チェック処理(1/3)から呼ばれるメソッドです。必要な権限を取得すると「true」になります。※このコードは onCreate() の { } の外に記述します。

private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
    ContextCompat.checkSelfPermission(
        baseContext, it) == PackageManager.PERMISSION_GRANTED
}

権限チェック処理(3/3)、権限取得ダイアログに反応するファンクションです。ユーザーが必要な権限すべてを許可すると「startPreview()」が呼び出されてプレビュー画面が表示されますが、それ以外の場合はアプリを終了します。※このコードも onCreate() の { } の外に記述します。

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    if (requestCode == REQUEST_CODE_PERMISSIONS) {
        if (allPermissionsGranted()) {
            //プレビュー開始
            startPreview()
        } else {
            //必要な権限が取得できない場合はアプリを終了
            finish()
        }
    }
}

companion object { } の中にファイル名のフォーマット、リクエストコードと、取得する必要がある権限を記述します。

companion object {
    //ファイル名フォーマット
    private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
    //リクエストコード
    private const val REQUEST_CODE_PERMISSIONS = 10

    //必要な権限(2または3権限)
    private val REQUIRED_PERMISSIONS =
        mutableListOf (
            Manifest.permission.CAMERA ,
            Manifest.permission.RECORD_AUDIO
        ).apply {
            if ( Build.VERSION.SDK_INT <= Build.VERSION_CODES.P ) {
                add( Manifest.permission.WRITE_EXTERNAL_STORAGE )
            }
        }.toTypedArray()
}

② プレビュー用コード追加

背面カメラからの映像をレイアウトファイルの「preview(androidx.camera.view.PreviewView)」に表示してプレビューします。

private fun startPreview() {

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({

        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.preview.surfaceProvider)
                }

            //静止画撮影用
            imageCapture = ImageCapture.Builder().build()

            
            val recorder = Recorder.Builder()
                .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
                .build()
            //動画撮影用
            videoCapture = VideoCapture.withOutput(recorder)

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()

                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture,
                    videoCapture
                )

            } catch (e: Exception) {
                Log.d("Camera X sample","エラーが発生しました", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

③ 静止画撮影コード追加

静止画を撮影するコードです。画像の保存先は任意で決めることができますが「DCIM」フォルダ内にすると撮影した画像がスムーズに認識されます。画像のフォーマットは「jpeg」です。

private fun takePhoto() {

    val imageCapture = this.imageCapture ?: return

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.ROOT)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/MyImage")
        }
    }

    val outputOptions = ImageCapture.OutputFileOptions.Builder(
        contentResolver,
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    ).build()

    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exception: ImageCaptureException) {
                Log.d("Camera X sample","撮影エラー(静止画)")
            }
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                val msg = "撮影成功(静止画)"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
            }
        }
    )
}

④ 動画撮影コード追加

動画の撮影コードです。静止画の撮影コードより少し複雑になっています。動画の保存形式は「mp4」です。保存先は「Movies」フォルダ内をおすすめします。

private fun captureVideo() {

    val videoCapture = this.videoCapture ?: return
    viewBinding.startCapture.isEnabled = false

    val curRecording = recording
    if (curRecording != null) {
        curRecording.stop()
        recording = null
        return
    }

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.ROOT)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/MyVideo")
        }
    }

    val mediaStoreOutputOptions = MediaStoreOutputOptions
        .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        .setContentValues(contentValues)
        .build()
    recording = videoCapture.output
        .prepareRecording(this, mediaStoreOutputOptions)
        .apply {
            if (PermissionChecker.checkSelfPermission(this@MainActivity,
                Manifest.permission.RECORD_AUDIO) ==
                PermissionChecker.PERMISSION_GRANTED)
            {
                withAudioEnabled()
            }
        }
        .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
            when (recordEvent) {
                is VideoRecordEvent.Start -> {
                    viewBinding.startCapture.apply {
                        text = getString(R.string.stop_capture)
                        isEnabled = true
                    }
                }
                is VideoRecordEvent.Finalize -> {
                    if (!recordEvent.hasError()) {
                        val msg = "撮影成功(動画)"
                        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    } else {
                        recording?.close()
                        recording = null
                        Log.d("Camera X sample","撮影エラー(動画)")
                    }
                    viewBinding.startCapture.apply {
                        text = getString(R.string.start_capture)
                        isEnabled = true
                    }
                }
            }
        }
}

⑤ 完成(MainActivity.kt)

MainActivity.kt の完成形です。

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import smart.and.small.software.java.gr.jp.cameraxsample.databinding.ActivityMainBinding
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

 

class MainActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityMainBinding
    private var imageCapture: ImageCapture? = null
    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null
    private lateinit var cameraExecutor: ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        //権限チェック(1/3)
        if (allPermissionsGranted()) {
            startPreview()   //プレビュー開始
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }

        //静止画撮影ボタン(クリックリスナー)
        viewBinding.takePhoto.setOnClickListener { takePhoto() }

        //動作撮影ボタン(クリックリスナー)
        viewBinding.startCapture.setOnClickListener { captureVideo() }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    //権限チェック(2/3)
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    //権限チェック(3/3)
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startPreview()   //プレビュー開始
            } else {
                //必要な権限が取得できない場合はアプリを終了する
                finish()
            }
        }
    }

    //静止画撮影
    private fun takePhoto() {

        val imageCapture = this.imageCapture ?: return

        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.ROOT)
            .format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/Image")
            }
        }

        val outputOptions = ImageCapture.OutputFileOptions.Builder(
            contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ).build()

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exception: ImageCaptureException) {
                    Log.d("Camera X sample","撮影エラー(静止画)")
//                    Toast.makeText(baseContext, "Error", Toast.LENGTH_SHORT).show()
                }
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    val msg = "撮影成功(静止画)"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                }
            }
        )
    }

    //動画撮影
    private fun captureVideo() {

        val videoCapture = this.videoCapture ?: return
        viewBinding.startCapture.isEnabled = false

        val curRecording = recording
        if (curRecording != null) {
            curRecording.stop()
            recording = null
            return
        }

        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.ROOT)
            .format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/Video")
            }
        }

        val mediaStoreOutputOptions = MediaStoreOutputOptions
            .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
            .setContentValues(contentValues)
            .build()
        recording = videoCapture.output
            .prepareRecording(this, mediaStoreOutputOptions)
            .apply {
                if (PermissionChecker.checkSelfPermission(this@MainActivity,
                    Manifest.permission.RECORD_AUDIO) ==
                    PermissionChecker.PERMISSION_GRANTED)
                {
                    withAudioEnabled()
                }
            }
            .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
                when (recordEvent) {
                    is VideoRecordEvent.Start -> {
                        viewBinding.startCapture.apply {
                            text = getString(R.string.stop_capture)
                            isEnabled = true
                        }
                    }
                    is VideoRecordEvent.Finalize -> {
                        if (!recordEvent.hasError()) {
                            val msg = "撮影成功(動画)"
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        } else {
                            recording?.close()
                            recording = null
                            Log.d("Camera X sample","撮影エラー(動画)")
                        }
                        viewBinding.startCapture.apply {
                            text = getString(R.string.start_capture)
                            isEnabled = true
                        }
                    }
                }
            }
    }

    //プレビュー開始
    private fun startPreview() {

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({

            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            //プレビュー
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.preview.surfaceProvider)
                }

            //静止画撮影
            imageCapture = ImageCapture.Builder().build()

            //動画撮影
            val recorder = Recorder.Builder()
                .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
                .build()
            videoCapture = VideoCapture.withOutput(recorder)

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()

                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture,
                    videoCapture
                )

            } catch (e: Exception) {
                Log.d("Camera X sample","エラーが発生しました", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        //ファイル名フォーマット
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        //リクエストコード
        private const val REQUEST_CODE_PERMISSIONS = 10
        //取得する権限
        private val REQUIRED_PERMISSIONS =
            mutableListOf(
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }
}

 8.動作テスト

アプリを実行すると背面カメラのプレビュー画面と2つのボタンが表示されます。

そして「PHOTO」ボタンを押すと静止画像が撮影され、「VIDEO」ボタンを押すと動画の撮影がスタートします。動画の撮影がスタートすると「VIDEO」ボタンのラベルが「STOP」に変わり、その「STOP」ボタンを押すと動画の撮影が停止します。

撮影した画像と動画は Android 標準の Google フォトアプリから閲覧することができます。

~ まとめ・備考 ~

今回は、カメラ映像のプレビューを表示して静止画と動画の撮影ができるまでを説明しましたが、今後は Camera X での「露出補正」「マニュアルズーム」「ピンチズーム」「マニュアルフォーカス」「タッチフォーカス」「マニュアル露出」など、カメラアプリに必要な機能の作り方をご説明する予定です。

END




以上の内容はhttps://android-java.hatenablog.jp/entry/android-camerax-applicationより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14