20210127のAndroidに関する記事は3件です。

[Android]Activity Result APIを使って画像の選択、撮影機能を実装したい

概要

Androidのアプリを開発していると、ユーザーに画像をセットさせるというコードをActivity Result APIを使って組もうとしたらやけに時間がかかってしまったので戒めとしてメモを残しておこうと思います。

Activity Result APIについて

今までは、画面遷移をして、結果をもらってくる・・・という処理を書く際にはstartActivityForResultを使用していたと思います。しかし現在、startActivityForResultはDeprecatedになっています。その代わりにActivity Result APIを使用することが推奨されています。これを使うことで結構簡潔に書けるようになるようです。まさかstartActivityForResultを使ったことがないなんて言え(ry

つまり、今回はActivity Result APIを用いて、Androidに元から入っているギャラリーやカメラアプリを起動させて、画像データをもらってくる処理を実装するということです。こうすることで権限などを記述する必要がなくなるので、気軽に実装することが出来るようになります。

今回組むアプリ

今回組むアプリはこんな感じです。

screenshot

こんな感じの画面で、selectを押すと

screenshot

となります。画像を選択を押すと

screenshot

撮影するを押すと

screenshot

そして画像の選択や撮影が終わると

screenshot

こんな感じの動作をするアプリです。
ちなみに言語はKotlinで組みます。また、Activityで組む方法を解説しているサイトはちらほら見かけますので、今回はFragmentに処理を実装していきたいと思います。

実際に組んでみる

導入

まずはActivity Result APIを導入しましょう。build.gradle(Module)に以下の内容を追加します。

build.gradle
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から作ってみます。

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の方を変えていきましょう。

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をいじっていきます。

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をいじっていきます。
こんな感じです。

MainFragment.kt
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を使う旨を記述しましょう。こちらを参考にしながらすれば分かりやすいと思います。

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.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の変更はないので、これで動くはずです

それでは見ていきましょう。
```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.kt
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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidアプリが対応しているディープリングを簡単に確認できる方法

事柄の背景

ネット上でディープリングについて調べてみたら、実装方法とディープリングについての解説の記事が圧倒的に多く、端末にインストール済みのアプリがどのディープリングに対応しているかを調べる方法の記事がほぼ皆無で、あってもその調べ方が難しすぎて、汎用性が乏しいです。

そのため、Androidアプリが対応しているディープリングを簡単に確認できる方法の記事を書こうと思います。

確認方法

確認方法は非常に簡単です。まず、端末の

[設定] -> [アプリ] -> 調べたいアプリ

を開きます。そこに[既定で開く]項目が表示されます。

image.png

[既定で開く]項目を選ぶと、アプリリンクという項目が表示されます。

image.png

そこで対応リンクを開くと、そのアプリが対応しているディープリンク一覧が表示されます。

image.png

この調べ方についての考察

メリット

  • 時短、手軽、すぐ調べられる
  • 外部アプリやソフトウェアが不要
  • Android知識やAdb知識が不要
  • プログラミング力が不要

デメリット

  • ディープリンクのスキーマがわからない。ホストしか取得できない

利用場面

ディープリンクの競合を避ける場面
自社サービスがアプリのディープリンク機能を提供するときに、そのディープリンクがほかのアプリで開かれてしまう可能性があるかどうかの調査に、役に立つと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]ConstraintLayoutを使って、画面内からはみ出さずにTextViewを改行させる

はじめに

この記事ではConstraintLayoutとそのヘルパーウィジェットであるflowを使って、下記の図のようなタイトルの長さに合わせて、画面から右端のコンポーネントがはみ出さないように中央のTextViewを改行させるレイアウトを作成するのに色々てこずったので紹介します
簡易説明.001.jpeg

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_widthwrap_contentで指定しているため、右にあるボタンを考慮して改行を行うことができず、親のビューからはみ出てレイアウトされてしまいます。

制約に合わせて大きさを決める

ConstraintLayout内の制約に合わせてコンテンツの大きさを変える方法があります。
layout_width0dpとして指定する方法です。
以下のように中央の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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む