- 投稿日:2021-01-27T18:18:34+09:00
[Android]Activity Result APIを使って画像の選択、撮影機能を実装したい
概要
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)に以下の内容を追加します。build.gradledependencies{ //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から作ってみます。ImageSetter.ktpackage 自身のパッケージ名 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の方を変えていきましょう。MainFragment.ktpackage 自身のパッケージ名 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をいじっていきます。ImageSetter.ktpackage 自身のパッケージ名 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をいじっていきます。
こんな感じです。MainFragment.ktpackage 自身のパッケージ名 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を使う旨を記述しましょう。こちらを参考にしながらすれば分かりやすいと思います。AndroidManifest.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/provider_path"/> </provider> </application>また、
res直下にxml/provider_path.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に記述していきましょう。ImageSetter.ktpackage 自身のパッケージ名 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の変更はないので、これで動くはずですそれでは見ていきましょう。
```kotlin
//先ほどのやつ
private lateinit var getContent: ActivityResultLauncher//今回のやつ private lateinit var dispatchTakePicture: ActivityResultLauncher<Uri>今度は`dispatchTakePicture`を定義してやり、そこに書いていく形です。ただ、今度は保存先のUriを引数として求められるので、変数を定義する際もこのようにしてやる必要があります。 ```kotlin 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でささっと作ってしまいましょう!
ImageSetter.ktpackage 自身のパッケージ名 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
- 投稿日:2021-01-27T14:48:23+09:00
Androidアプリが対応しているディープリングを簡単に確認できる方法
事柄の背景
ネット上でディープリングについて調べてみたら、実装方法とディープリングについての解説の記事が圧倒的に多く、端末にインストール済みのアプリがどのディープリングに対応しているかを調べる方法の記事がほぼ皆無で、あってもその調べ方が難しすぎて、汎用性が乏しいです。
そのため、Androidアプリが対応しているディープリングを簡単に確認できる方法の記事を書こうと思います。
確認方法
確認方法は非常に簡単です。まず、端末の
[設定] -> [アプリ] -> 調べたいアプリ
を開きます。そこに[既定で開く]項目が表示されます。
[既定で開く]項目を選ぶと、アプリリンクという項目が表示されます。
そこで対応リンクを開くと、そのアプリが対応しているディープリンク一覧が表示されます。
この調べ方についての考察
メリット
- 時短、手軽、すぐ調べられる
- 外部アプリやソフトウェアが不要
- Android知識やAdb知識が不要
- プログラミング力が不要
デメリット
- ディープリンクのスキーマがわからない。ホストしか取得できない
利用場面
ディープリンクの競合を避ける場面
自社サービスがアプリのディープリンク機能を提供するときに、そのディープリンクがほかのアプリで開かれてしまう可能性があるかどうかの調査に、役に立つと思います。
- 投稿日:2021-01-27T08:59:08+09:00
[Android]ConstraintLayoutを使って、画面内からはみ出さずにTextViewを改行させる
はじめに
この記事ではConstraintLayoutとそのヘルパーウィジェットであるflowを使って、下記の図のようなタイトルの長さに合わせて、画面から右端のコンポーネントがはみ出さないように中央のTextViewを改行させるレイアウトを作成するのに色々てこずったので紹介します
flowを使って並べる
flowとは
ConstraintLayoutのflowとはConstraintLayout内のViewの整列に便利なヘルパーウィジェットです。
詳しくは過去に私が書いたテックブログをご覧ください(※ConstraintLayout2.0で追加されるFlowを使ってタグを実装する)必要なウィジェットをConstraintLayout内に配置する
絵文字を表示するTextView、コンテンツを表示するTextView, 常にコンテンツの右隣に配置されるButtonの3つとヘルパーウィジェットであるflowをconstraintLayout内に配置します。
activity_main.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.constraintlayout.helper.widget.Flow android:layout_width="match_parent" android:layout_height="match_parent" /> <TextView android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="?" android:textSize="30sp" /> <TextView android:id="@+id/contentTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="タイトル" android:textSize="32sp" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="決定" /> </androidx.constraintlayout.widget.ConstraintLayout>現段階では何も制約がないため、全てのコンポーネントが左上に固まっています。
横に整列させる
コンポーネントを整列するのにflowが活躍します。
flowのAttributesを以下のように指定しますactivity_main.xml<androidx.constraintlayout.helper.widget.Flow android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" app:constraint_referenced_ids="icon, contentTextView, button" app:flow_horizontalBias="0" app:flow_horizontalGap="6dp" app:flow_horizontalStyle="packed" app:flow_maxElementsWrap="3" app:flow_verticalAlign="center" app:flow_wrapMode="none" /> ... </androidx.constraintlayout.widget.ConstraintLayout>それぞれのAttributesについては
-android:orientation- 整列方向
-app:constraint_referenced_ids- 整列させるViewのid
-app:flow_horizontalBias- 水平方向の重心(今回は左寄せにしたいので0を指定)
-app:flow_horizontalGap- View間の間隔
-app:flow_maxElementsWrap- 1列あたりの要素の最大個数
-app:flow_verticalAlign- 垂直方向のViewの合わせ方
-app:flow_wrapMode- 整列方法(今回は改行させる必要がないのでnoneを指定)flowを使って整列させることで、プレビュー内のViewでは想定どおり表示されていることが確認できます。
改行を考慮する
テキストが改行の不要な範囲内では正しくレイアウトできていることができました。
では、テキストが改行が必要になる程度大きくなった場合はどうなるでしょうか?
ボタンが消えた?
そうです。
中央のTextViewのlayout_widthをwrap_contentで指定しているため、右にあるボタンを考慮して改行を行うことができず、親のビューからはみ出てレイアウトされてしまいます。制約に合わせて大きさを決める
ConstraintLayout内の制約に合わせてコンテンツの大きさを変える方法があります。
layout_widthを0dpとして指定する方法です。
以下のように中央のTextViewのAttributeを変更するとどうなるでしょうか?activity_main.xml<TextView android:id="@+id/contentTextView" android:layout_width="0dp" android:layout_height="wrap_content" android:text="タイトル" android:textSize="32sp" />
通常 タイトルが長い場合 タイトルが長い場合はこれで対応することができますが、タイトルが短い場合はボタンが左寄せになっておらず、意図したようにレイアウトされていません。
これは、中心のTextViewが制約通りの横幅を保持するため、文字数にかかわらず領域を保有してしまうためです。あと少しで表示できそうなのに、、、
そんな心の声が聞こえてきそうですが、ここで使用するのがlayout_constraintWidth_defaultというattributesです。
制約のによる領域の確保を最小にする
基本的に
layout_width="0dp"のように指定した場合、横幅はレイアウトが許す限り最大の大きさを保有しようとします。
ただ、最大まで保有するかどうかはlayout_constraintWidth_defaultというattributesによって変更することができます。(デフォルトでは"spread"となっており、最大まで広がるようになっている)そこで制約による領域の確保を最小にするため、
app:layout_constraintWidth_default = "wrap"
と指定することで中心のTextViewの横幅を必要最小限にすることができます。activity_main.xml<TextView android:id="@+id/contentTextView" android:layout_width="0dp" android:layout_height="wrap_content" android:text="タイトル" android:textSize="32sp" app:layout_constraintWidth_default = "wrap" />このようにTextViewのattributesを変更する
通常 タイトルが長い場合 これで意図した通りにレイアウトを作成できました?
最終的なコード
最後に最終的なコードを以下にまとめます。
activity_main.xml<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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.constraintlayout.helper.widget.Flow android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" app:constraint_referenced_ids="icon, contentTextView, button" app:flow_horizontalBias="0" app:flow_horizontalGap="6dp" app:flow_horizontalStyle="packed" app:flow_maxElementsWrap="3" app:flow_verticalAlign="center" app:flow_wrapMode="none" /> <TextView android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="?" android:textSize="30sp" /> <TextView android:id="@+id/contentTextView" android:layout_width="0dp" android:layout_height="wrap_content" android:text="タイトル" android:textSize="32sp" app:layout_constraintWidth_default="wrap" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="決定" /> </androidx.constraintlayout.widget.ConstraintLayout>














