Jetpack Composeで写真を撮影するか画像を選択する

Jetpack Composeを使ったAndroidアプリで、インテントで写真を撮影するか画像を選択して、その画像を表示するアプリを作成します。

カメラの権限を要求する

Jetpack Compose Permissionsをインストールする。

Jetpack Composeのバージョンによって、インストールするバージョンが異なる。
参考:google/accompanist: A collection of extension libraries for Jetpack Compose

  • Compose 1.0 (1.0.x) => 0.20.3
  • Compose 1.1 (1.1.x) => 0.23.1
  • Compose UI 1.2 (1.2.x) => 0.25.1
  • Compose UI 1.3 (1.3.x) => 0.26.1-alpha

今回は、バージョン0.23.1を使用する。

build.gradle(:app)に追加する。

dependencies {
    // 追加
    implementation 'com.google.accompanist:accompanist-permissions:0.23.1'
}

AndroidManifest.xmlを編集して、「uses-permission」を追加する。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    <uses-permission android:name="android.permission.CAMERA"/>

カメラの権限が付与されていなければ、権限を要求する。

val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)

// 注意:バージョンによって書き方が異なる
if (cameraPermissionState.hasPermission) {
    // カメラの権限が付与されているとき
} else {
    // カメラの権限を要求する
    cameraPermissionState.launchPermissionRequest()
}

カメラで撮影した写真を取得する

画像の読み込みに使用するCoilをインストールする。

dependencies {
    // 追加
    implementation 'io.coil-kt:coil-compose:2.1.0'
}

AndroidManifest.xmlを編集して、providerを追加する。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application ...>

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

app/src/main/res/xml/にprovider_path.xmlファイルを追加する。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="my_images" path="images/" />
</paths>

カメラで撮影した写真ファイルを保存するファイルのURIを取得する関数を作成する。

private fun getImageUri(context: Context): Uri {
    val imagePath = File(context.cacheDir, "images")
    imagePath.mkdirs()

    val date = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val file = File(imagePath, "$date.jpg")
    return FileProvider.getUriForFile(
        context,
        context.packageName + ".fileprovider",
        file
    )
}

カメラで撮影した写真を保存する一時的なファイルのURIと、表示する画像ファイルのURIの変数を用意する。

    /**
     * カメラで撮影した写真を保存する一時的なファイルのURI
     */
    var uri: Uri? = null

    /**
     * 表示する画像ファイルのURI
     */
    var imageUri by remember { mutableStateOf<Uri?>(null) }

カメラで写真を撮影したら、表示する画像ファイルのURIを更新する。

    /**
     * カメラを起動して写真を撮影する
     */
    val cameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture(),
        onResult = { success ->
            if (success) {
                imageUri = uri
            }
        }
    )

写真撮影ボタンを押すとカメラを起動して写真を撮影する。

Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = {
        if (cameraPermissionState.hasPermission) {
            // 撮影した写真の保存場所
            uri = getImageUri(context = context)
            // カメラを起動して写真を撮影する
            cameraLauncher.launch(uri)
        } else {
            // カメラの権限を要求する
            cameraPermissionState.launchPermissionRequest()
        }
    }) {
    Text(text = "写真撮影")
}

写真があれば表示する。

if (imageUri != null) {
    AsyncImage(
        model = imageUri,
        contentDescription = "My Image"
    )
}

ここまでのソースコードは以下のようになる。

import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.FileProvider
import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import java.io.File
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainContent()
                }
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MaterialTheme {
        MainContent()
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MainContent() {
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    val context = LocalContext.current

    /**
     * カメラで撮影した写真を保存する一時的なファイルのURI
     */
    var uri: Uri? = null

    /**
     * 表示する画像ファイルのURI
     */
    var imageUri by remember { mutableStateOf<Uri?>(null) }
    val imagePicker =
        rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
            imageUri = uri
        }

    /**
     * カメラを起動して写真を撮影する
     */
    val cameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture(),
        onResult = { success ->
            if (success) {
                imageUri = uri
            }
        }
    )

    Column(modifier = Modifier.fillMaxSize()) {
        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = {
                if (cameraPermissionState.hasPermission) {
                    // 撮影した写真の保存場所
                    uri = getImageUri(context = context)
                    // カメラを起動して写真を撮影する
                    cameraLauncher.launch(uri)
                } else {
                    // カメラの権限を要求する
                    cameraPermissionState.launchPermissionRequest()
                }
            }) {
            Text(text = "写真撮影")
        }
        if (imageUri != null) {
            AsyncImage(
                model = imageUri,
                contentDescription = "My Image"
            )
        }
    }
}

/**
 * カメラで撮影した写真ファイルを保存するファイルのURIを取得する
 */
private fun getImageUri(context: Context): Uri {
    val imagePath = File(context.cacheDir, "images")
    imagePath.mkdirs()

    val date = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val file = File(imagePath, "$date.jpg")
    return FileProvider.getUriForFile(
        context,
        context.packageName + ".fileprovider",
        file
    )

写真を撮影するか画像を選択する

写真を撮影するか画像を選択するには、Intent.createChooserを使用して、Intentを作成する。

/**
 * 写真を撮影するか画像を選択するインテントを作成する
 */
private fun createChooser(uri: Uri): Intent {
    // ファイルの選択
    val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "image/*"
    }
    // カメラ撮影
    val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    imageCaptureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)

    val chooserIntent = Intent.createChooser(getContentIntent, "写真の選択")
    chooserIntent.putExtra(
        Intent.EXTRA_INITIAL_INTENTS,
        arrayOf<Parcelable>(imageCaptureIntent)
    )
    return chooserIntent
}

写真を撮影するか画像を選択するインテントで撮影または選択された写真を受け取る。
注意:個々のコードは自信がない。

    /**
     * 写真を撮影するか画像を選択する
     */
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult(),
        onResult = { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK) {
                imageUri = result.data?.data ?: uri
            }
        }
    )

「写真撮影または選択」ボタンを押したときに、写真を撮影するか画像を選択するインテントを実行する。

Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = {
        if (cameraPermissionState.hasPermission) {
            // カメラの権限が付与されているとき
            val tmpUri = getImageUri(context = context)
            uri = tmpUri
            launcher.launch(createChooser(tmpUri))
        } else {
            // カメラの権限を要求する
            cameraPermissionState.launchPermissionRequest()
        }
    }) {
    Text(text = "写真撮影または選択")
}

ここまでのソースコード。

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.FileProvider
import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import java.io.File
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainContent()
                }
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MaterialTheme {
        MainContent()
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MainContent() {
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    val context = LocalContext.current

    /**
     * カメラで撮影した写真を保存する一時的なファイルのURI
     * 注意:アクティビティが破棄されるとuriの値を失う。対策が必要。
     */
    var uri: Uri? = null

    /**
     * 表示する画像ファイルのURI
     */
    var imageUri by remember { mutableStateOf<Uri?>(null) }

    /**
     * カメラを起動して写真を撮影する
     */
    val cameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture(),
        onResult = { success ->
            if (success) {
                imageUri = uri
            }
        }
    )

    /**
     * 写真を撮影するか画像を選択する
     */
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult(),
        onResult = { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK) {
                imageUri = result.data?.data ?: uri
            }
        }
    )

    Column(modifier = Modifier.fillMaxSize()) {
        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = {
                if (cameraPermissionState.hasPermission) {
                    // カメラの権限が付与されているとき
                    val tmpUri = getImageUri(context = context)
                    uri = tmpUri
//                    cameraLauncher.launch(tmpUri)
                    launcher.launch(createChooser(tmpUri))
                } else {
                    // カメラの権限を要求する
                    cameraPermissionState.launchPermissionRequest()
                }
            }) {
            Text(text = "写真撮影または選択")
        }
        if (imageUri != null) {
            AsyncImage(
                model = imageUri,
                contentDescription = "My Image"
            )
        }
    }
}

/**
 * カメラで撮影した写真ファイルを保存するファイルのURIを取得する
 */
private fun getImageUri(context: Context): Uri {
    val imagePath = File(context.cacheDir, "images")
    imagePath.mkdirs()

    val date = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val file = File(imagePath, "$date.jpg")
    return FileProvider.getUriForFile(
        context,
        context.packageName + ".fileprovider",
        file
    )
}

/**
 * 写真を撮影するか画像を選択するインテントを作成する
 */
private fun createChooser(uri: Uri): Intent {
    // ファイルの選択
    val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "image/*"
    }
    // カメラ撮影
    val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    imageCaptureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)

    val chooserIntent = Intent.createChooser(getContentIntent, "写真の選択")
    chooserIntent.putExtra(
        Intent.EXTRA_INITIAL_INTENTS,
        arrayOf<Parcelable>(imageCaptureIntent)
    )
    return chooserIntent
}

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください