#概要
Androidのアプリを開発していると、ユーザーに画像をセットさせるというコードをActivity Result APIを使って組もうとしたらやけに時間がかかってしまったので戒めとしてメモを残しておこうと思います。
#Activity Result APIについて
今までは、画面遷移をして、結果をもらってくる・・・という処理を書く際にはstartActivityForResultを使用していたと思います。しかし現在、startActivityForResultはDeprecatedになっています。その代わりにActivity Result APIを使用することが推奨されています。これを使うことで結構簡潔に書けるようになるようです。まさかstartActivityForResultを使ったことがないなんて言え(ry
つまり、今回はActivity Result APIを用いて、Androidに元から入っているギャラリーやカメラアプリを起動させて、画像データをもらってくる処理を実装するということです。こうすることで権限などを記述する必要がなくなるので、気軽に実装することが出来るようになります。
#今回組むアプリ
今回組むアプリはこんな感じです。
こんな感じの画面で、selectを押すと
となります。画像を選択を押すと
撮影するを押すと
そして画像の選択や撮影が終わると
こんな感じの動作をするアプリです。
ちなみに言語はKotlinで組みます。また、Activityで組む方法を解説しているサイトはちらほら見かけますので、今回はFragmentに処理を実装していきたいと思います。
#実際に組んでみる
###導入
まずはActivity Result APIを導入しましょう。build.gradle(Module)
に以下の内容を追加します。
dependencies{
//Lifecycle用
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//Android Result API用
implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01'
}
前までandroidx.fragmentのrc01版でエラーを吐いていましたが、現在ではどうやら対応されたそうです。正常に動きます。
今回はDefalutLifecycleも使いますのでそれも追加しています。
###Fragmentとかの実装
とりあえずFragmentとかButtonなどの実装をします。しかし、これは趣旨とは離れてます。ですので下にコードがありますのでそちらを参照してください。
https://github.com/Wansuko-cmd/Practice-AndroidResultAPI/tree/e983566514d38e4e24976fc70440486ebb01a77a
といってもMainActivityからMainFragmentを呼び出す処理と、MainFragmentにボタンやimageViewを張り付けているだけなので特に問題はないかと思います。
###Android Result APIを使う場所を用意する
FragmentでAndroid Result APIの処理を書くときはLifecycleObserverを利用することが推奨されています。というわけでそれに従って実装していきます。
まずはDefalutLifecycleObserverを継承したクラスであるImageSetter.kt
から作ってみます。
package 自身のパッケージ名
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class ImageSetter(private val activity: FragmentActivity) : DefaultLifecycleObserver{
override fun onCreate(owner: LifecycleOwner) {
}
fun selectImage(){
Toast.makeText(activity, "Hello World", Toast.LENGTH_SHORT).show()
}
}
内容としてはHello Worldを出力する関数があるだけです。
次にMainFragment.kt
の方を変えていきましょう。
package 自身のパッケージ名
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
class MainFragment : Fragment() {
private lateinit var observer: ImageSetter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observer = ImageSetter(requireActivity())
lifecycle.addObserver(observer)
val button = requireActivity().findViewById<Button>(R.id.button)
button.setOnClickListener {
observer.selectImage()
}
}
}
observerに先ほどのクラスのインスタンスを登録して、ボタンがクリックされたときにselectImage()
を実行するといった感じです。
これでアプリを起動->ボタンを押してみて、Hello Worldが出てきたらOKです。
###画像の選択機能の追加
ようやく本題です。まずはImageSetter.kt
をいじっていきます。
package 自身のパッケージ名
import android.net.Uri
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class ImageSetter(
activity: FragmentActivity,
private val imageView: ImageView
) : DefaultLifecycleObserver{
private lateinit var getContent: ActivityResultLauncher<String>
private val registry = activity.activityResultRegistry
private var uri: Uri? = null
override fun onCreate(owner: LifecycleOwner) {
getContent =
registry.register(
"select-key",
owner,
ActivityResultContracts.GetContent()
){
it?.let{
this.uri = it
imageView.setImageURI(uri)
}
}
}
fun selectImage() {
getContent.launch("image/*")
}
}
このような感じで書いてやることで、selectImage()
実行時にギャラリーを出すことが出来ます。
それではコードを見ていきましょう
private lateinit var getContent: ActivityResultLauncher<String>
private val registry = activity.activityResultRegistry
まずgetContentと、registryを定義しています。この時、getContentの型は何を使うかによって変わってきます。今回のようにギャラリーを出すときはString型が引数となりますので、このように記述します。
registryは、別のアプリを呼び出す際に使います。
getContent =
registry.register(
"select-key",
owner,
ActivityResultContracts.GetContent()
){
it?.let{
this.uri = it
imageView.setImageURI(uri)
}
}
select-key
のところはどうやらREQUEST_CODEのようなもののようで、他のものと被っていると正常に動きません。ですので被らないような名前にしましょう。
また、ActivityResultContracts.GetContent()
で何を実行するかを決めている感じです。今回はギャラリーを使うためこのようにしています。
最後に書かれているラムダ式は、実行後の処理を記述するところです。今回の場合はimageViewにUriをセットしています。
ちなみにこれら画面遷移は非同期で行われます。ですので、このタイミングでimageViewにUriを渡しておかないと、画像が選ばれる前にUriがセットされてしまい、意図した動作と違う動きになります。注意しましょう。
fun selectImage() {
getContent.launch("image/*")
}
selectImage()
ではgetContent
を動かしています。ここでの引数は、何をギャラリーに表示させるのかを制限するのに使います。例えば今回の例だと、ギャラリーに画像ファイル以外が出てきて、それらを選ばれたらまずいので、image/*
としています。こうすることで画像ファイルのみ出すようにしています。
ちなみにこれを使うことでアプリにアクセス権限がないところにある画像でも「一時的に」アクセスが許可されます。したがって、取得したUriを保存して次回起動時に使う・・・ということは権限がないとできないです。ですので、アプリを終了する前に外部ストレージ等といった、権限がある領域に画像をコピーしておきましょう。
それでは次にMainFragment
をいじっていきます。
こんな感じです。
package 自身のパッケージ名
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import androidx.fragment.app.Fragment
class MainFragment : Fragment() {
private lateinit var observer: ImageSetter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imageView = requireActivity().findViewById<ImageView>(R.id.image_view)
val button = requireActivity().findViewById<Button>(R.id.button)
button.setOnClickListener {
observer.selectImage()
}
observer = ImageSetter(requireActivity(), imageView)
lifecycle.addObserver(observer)
}
}
追加したのはimageViewについての記述です。imageViewをobserver、つまり先ほどのImageSetterに渡しています。
これで動かしてみると、ギャラリーから写真を取ってこれるようになるはずです。
###画像の撮影機能を実装する
それでは次に画像の撮影機能を実装しましょう。
こちらでは自分で保存先のUriを用意してやる必要があります。ということでまずはそちらの実装からです。
実装の手順としては、File形式で保存先を用意してやる->Uri形式にするという感じです。
まずはAndroidManifest.xml
にFileProviderを使う旨を記述しましょう。こちらを参考にしながらすれば分かりやすいと思います。
<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/provider_path"/>
</provider>
</application>
また、res
直下にxml/provider_path.xml
を用意して以下を記述します。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>
これでFileProviderが使えるようになりました。つまりFile->Uriへの変換が可能になったということです。
ということで、ImageSetter.kt
に記述していきましょう。
package 自身のパッケージ名
import android.net.Uri
import android.os.Environment
import android.util.Log
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.io.File
import java.util.*
class ImageSetter(
private val activity: FragmentActivity,
private val imageView: ImageView
) : DefaultLifecycleObserver{
private lateinit var getContent: ActivityResultLauncher<String>
private lateinit var dispatchTakePicture: ActivityResultLauncher<Uri>
private val registry = activity.activityResultRegistry
private var uri: Uri? = null
override fun onCreate(owner: LifecycleOwner) {
getContent =
registry.register(
"select-key",
owner,
ActivityResultContracts.GetContent()
){
it?.let{
this.uri = it
imageView.setImageURI(uri)
}
}
dispatchTakePicture =
registry.register(
"take-keys",
owner,
ActivityResultContracts.TakePicture()
){
if (it) {
Log.d("takePicture", "Success")
imageView.setImageURI(uri)
} else {
Log.d("takePicture", "Failed")
}
}
}
fun selectImage(){
//getContent.launch("image/*")
takePicture()
}
private fun takePicture(){
val filename = UUID.randomUUID().toString() + ".jpg"
val path = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, filename)
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", file)
dispatchTakePicture.launch(uri)
Log.d("Uri", uri.toString())
}
}
このような形になります。今回はMainFragment.kt
の変更はないので、これで動くはずです
それでは見ていきましょう。
//先ほどのやつ
private lateinit var getContent: ActivityResultLauncher<String>
//今回のやつ
private lateinit var dispatchTakePicture: ActivityResultLauncher<Uri>
今度はdispatchTakePicture
を定義してやり、そこに書いていく形です。ただ、今度は保存先のUriを引数として求められるので、変数を定義する際もこのようにしてやる必要があります。
dispatchTakePicture =
registry.register(
"take-keys",
owner,
ActivityResultContracts.TakePicture()
){
if (it) {
Log.d("takePicture", "Success")
imageView.setImageURI(uri)
} else {
Log.d("takePicture", "Failed")
}
}
ここは先ほどとは同じkeyをつかわないように、気を付けましょう。
また、今回のやつだと、最後のラムダ式で渡される変数は、成功したかどうかがBoolean型で代入されます。
private fun takePicture(){
val filename = UUID.randomUUID().toString() + ".jpg"
val path = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, filename)
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", file)
dispatchTakePicture.launch(uri)
Log.d("Uri", uri.toString())
}
ここではFileで保存先を作成してから、Uriに変換して、dispatchTakePicture
の引数として渡しています。こうすることで、このUriに撮った写真を保存してくれます。また、filename
を保存しておくことで、次回画像を呼び出すときも同じような手順で呼び出すことが出来ます。
###仕上げ
あとは分岐部分を作ってしまったら終わりです。AlertDialogでささっと作ってしまいましょう!
package 自身のパッケージ名
import android.app.AlertDialog
import android.net.Uri
import android.os.Environment
import android.util.Log
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.io.File
import java.util.*
class ImageSetter(
private val activity: FragmentActivity,
private val imageView: ImageView
) : DefaultLifecycleObserver{
private lateinit var getContent: ActivityResultLauncher<String>
private lateinit var dispatchTakePicture: ActivityResultLauncher<Uri>
private val registry = activity.activityResultRegistry
private var uri: Uri? = null
override fun onCreate(owner: LifecycleOwner) {
getContent =
registry.register(
"select-key",
owner,
ActivityResultContracts.GetContent()
){
it?.let{
this.uri = it
imageView.setImageURI(uri)
}
}
dispatchTakePicture =
registry.register(
"take-keys",
owner,
ActivityResultContracts.TakePicture()
){
if (it) {
Log.d("takePicture", "Success")
imageView.setImageURI(uri)
} else {
Log.d("takePicture", "Failed")
}
}
}
fun selectImage(){
val items = arrayOf("画像を選択", "撮影する")
AlertDialog.Builder(activity)
.setItems(items) { dialog, which ->
Log.d("dialog", dialog.toString())
Log.d("which", which.toString())
when(which){
0 -> getContent.launch("image/*")
1 -> takePicture()
}
}
.show()
}
private fun takePicture(){
val filename = UUID.randomUUID().toString() + ".jpg"
val path = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, filename)
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", file)
dispatchTakePicture.launch(uri)
Log.d("Uri", uri.toString())
}
}
変更部分はここだけですね
fun selectImage(){
val items = arrayOf("画像を選択", "撮影する")
AlertDialog.Builder(activity)
.setItems(items) { dialog, which ->
Log.d("dialog", dialog.toString())
Log.d("which", which.toString())
when(which){
0 -> getContent.launch("image/*")
1 -> takePicture()
}
}
.show()
}
こんな感じで書くことで分岐部分を出すことが出来ます。
これで上記のアプリが完成します。
#最後に
今回書いたコードはすべて合わせて661行となっています。実際に書いた部分の事とかを考えると結構簡単にかけているように思えます。
Activity Result APIは他にも動画を取ったりとかもできるようです。まだまだ情報が少ないように思えますが、これからに期待したいです。
今回書いたコードはここで公開しています。
https://github.com/Wansuko-cmd/Practice-AndroidResultAPI