ファイル選択orカメラ撮影で画像を選択できるようにしたい

こういうやつ。

chooser

カメラで撮った写真を使う場合と端末のファイルを利用する場合でそれぞれ単体の実装は以下の記事を参照。

実装

自作ActivityResultContract

ボトムシートとしてファイル選択、カメラ撮影から選択できるようにするためのActivityResultContractを作成する。

class ChooserActivityResultContract(private val context: Context?) : ActivityResultContract<Unit, Uri?>() {
  // 端末のファイルを選択するためのIntent
  private val getContentIntent
    get() = context?.let {
      ActivityResultContracts.OpenDocument().createIntent(
        it,
        arrayOf("image/jpeg", "image/png")
      )
    }

  // カメラ撮影するためのIntent
  private val takePictureIntent
    get() = context?.let {
      ActivityResultContracts.TakePicture().createIntent(
        it,
        cacheUri ?: Uri.EMPTY
      )
    }

  // カメラ撮影したファイルの保存先を表すUri
  private var cacheUri : Uri? = null

  override fun createIntent(context: Context, input: Unit): Intent {
    cacheUri = FileProvider.getUriForFile(
        context,
        "${BuildConfig.APPLICATION_ID}.provider",
        context.cacheDir.toPath().resolve("cache.jpg").toFile())

    // OpenDocumentとTakePictureから選択できるChooserを起動する
    return Intent.createChooser(getContentIntent, "選択").apply {
      // 端末にカメラがあればTakePictureの選択肢を追加
      val hasCameraFeature = context.packageManager
          ?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
          ?: false
      if (hasCameraFeature) {
          putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(takePictureIntent))
      }
    }
  }

  override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
    return if (context == null || resultCode != Activity.RESULT_OK) {
      null
    } else {
      // OpenDocumentで選択した場合は intent?.data に、
      // TakePicutreでカメラ撮影して保存されたファイルはcacheUriに、
      // content://〜 形式のUriが入っている
      intent?.data ?: cacheUri
    }
  }
}

cacheディレクトリカメラで撮影した画像ファイルを保存できるようにする

自作ActivityResultContractでカメラで撮影したファイルをキャッシュフォルダに保存するようにしたので、 キャッシュフォルダにカメラアプリがファイルを保存できるように許可する必要がある。

参考: 端末で写真を撮って表示する

AndroidManifest.xmlcacheフォルダへのファイルの保存を許可設定を追加する。

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

許可するフォルダは以下の設定で行なう。

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

Fragmentで自作ActivityResultContractを呼び出す

自作ActivityResultContractからregisterForActivityResultでlauncherを作成し、 ボタンクリック時に起動する。

class FirstFragment : Fragment() {
  // 自作ActivityResultContractから作成されるlauncherを保存するフィールド
  private var chooser: ActivityResultLauncher<Unit>? = null

  override fun onCreateView(
      inflater: LayoutInflater,
      container: ViewGroup?,
      savedInstanceState: Bundle?
  ): View? {
    ...

    // 自作ActivityResultContractからlauncherを作成する
    chooser = registerForActivityResult(ChooserActivityResultContract(context)) { result ->
        val uri = result ?: return@registerForActivityResult
        val context = context ?: return@registerForActivityResult
        viewModel.loadImage(uri, context)
    }

    ...
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ボタンクリック時に立ち上げるようにする
    binding.button.setOnClickListener {
        chooser?.launch(Unit)
    }
  }
}

動作確認

動作確認