- 投稿日:2021-09-01T22:42:17+09:00
ウィジェット一覧を作る ~ホームアプリ(ランチャーアプリ)の作り方~
ホームアプリ(ランチャーアプリ)の作り方シリーズ 今回は端末にインストールされているウィジェット一覧を表示させてみます。 超シンプルなホームアプリを作る ~ホームアプリ(ランチャーアプリ)の作り方~ システム壁紙を制御する ~ホームアプリ(ランチャーアプリ)の作り方~ ウィジェット一覧を作る ~ホームアプリ(ランチャーアプリ)の作り方~ ←イマココ ※シリーズが続くとは言っていない ※壁紙の関係で文字の可読性が滅茶苦茶低いのはご容赦を インストールされているウィジェット情報の取得 ウィジェット情報にアクセスするにはAppWidgetManagerを利用します。 インストールされているウィジェット情報は以下のメソッドで取得することができます。 public List<AppWidgetProviderInfo> getInstalledProviders() 一覧情報は超シンプルなホームアプリを作る ~ホームアプリ(ランチャーアプリ)の作り方~で作ったアプリ一覧と同様に作っていきますが、ウィジェットの情報として表示したいものを考えてデータクラスを作ってみます。 data class WidgetInfo( val appWidgetProviderInfo: AppWidgetProviderInfo, val label: String, val previewImage: Drawable?, val minWidth: Int, val minHeight: Int, val packageName: String, ) appWidgetProviderInfoは今回は使いませんが、念のため持たせておきます。 また、このウィジェットを提供しているアプリの情報も欲しいところですね、以下のように定義しておきます。 data class WidgetPackageInfo( val label: String, val icon: Drawable?, val packageName: String, val widgets: List<WidgetInfo>, ) アプリの情報に、先ほどのウィジェットの情報を持たせる形にしています。 これをAppWidgetManagerから取得して返還する処理を以下のように実装してみます。 object WidgetPackageInfoList { private var defaultIcon: Drawable? = null private fun getDefaultIcon(context: Context): Drawable { return defaultIcon ?: AppCompatResources.getDrawable(context, R.drawable.ic_android) ?.also { defaultIcon = it }!! } fun create(context: Context): List<WidgetPackageInfo> = AppWidgetManager.getInstance(context) .installedProviders .map { it.toWidgetInfo(context) } .groupBy { it.packageName } .map { it.toWidgetPackageInfo(context) } private fun AppWidgetProviderInfo.toWidgetInfo(context: Context): WidgetInfo { val label = loadLabel(context.packageManager) ?: "" val image = loadPreviewImage(context, 0) val packageName = provider.packageName return WidgetInfo(this, label, image, minWidth, minHeight, packageName) } private fun Map.Entry<String, List<WidgetInfo>>.toWidgetPackageInfo(context: Context): WidgetPackageInfo { val packageManager = context.packageManager val packageInfo = packageManager.getPackageInfo(key, 0) val applicationInfo = packageInfo.applicationInfo val label = applicationInfo.loadLabel(packageManager).toString() val icon = applicationInfo.loadIcon(packageManager) ?: getDefaultIcon(context) return WidgetPackageInfo(label, icon, packageInfo.packageName, value) } } AppWidgetProviderInfoにはlabelやicon、previewImageはそれぞれのアプリのリソースIDだけが入っています。 DrawableやStringなどの具体的データはそれぞれのアプリから読み出します。それぞれloadメソッドが用意されていますのでそれを利用します。 また、アプリの情報は起動アプリ一覧の時とは異なり、ActivityInfoではなく、PackageInfoを利用します。 ちょっと戸惑うかもしれないのは loadPreviewImage(context, 0)の第二引数でしょうか?ここにはdensityDpiの値を指定して、読み出すリソースのdensityを指定することができます。拡大して表示する場合などに、端末のdensityより大きな値を指定して、高解像度のリソースを読み出すとかができます。0を指定すると端末のdensityで読み出されるので通常0を指定します。 ウィジェット一覧をRecyclerViewに表示する 比較的簡単に一覧情報を取得し、表示したい要素を持ったdata classのリストを作ることができたので、これを表示するだけです。 一つのアプリが複数のウィジェットを持っているため、アプリ情報の下にそのアプリの持っているウィジェット一覧を表示する形にしてみます。 まずは、アプリ情報はアイコンとアプリ名とパッケージ名を表示します。 item_widget_header.xml <?xml version="1.0" encoding="utf-8"?> <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:id="@+id/root" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" > <ImageView android:id="@+id/icon" android:layout_width="24dp" android:layout_height="24dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@drawable/ic_launcher" /> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" android:shadowColor="@android:color/black" android:shadowRadius="4" android:textColor="@color/app_name" android:textSize="@dimen/text_size_widget_list_app_title" app:layout_constraintBottom_toTopOf="@+id/packageName" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/icon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/packageName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:ellipsize="end" android:maxLines="1" android:shadowColor="@android:color/black" android:shadowRadius="4" android:textColor="@color/app_name" android:textSize="@dimen/text_size_widget_list_app_package" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/icon" app:layout_constraintTop_toBottomOf="@+id/label" /> </androidx.constraintlayout.widget.ConstraintLayout> ウィジェットも同様に、プレビューイメージとウィジェット名とサイズ情報を表示させます item_wdget_entity.xml <?xml version="1.0" encoding="utf-8"?> <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:id="@+id/root" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" > <ImageView android:id="@+id/preview" android:layout_width="144dp" android:layout_height="96dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@drawable/ic_launcher" /> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" android:shadowColor="@android:color/black" android:shadowRadius="4" android:textColor="@color/app_name" android:textSize="@dimen/text_size_widget_list_widget_title" app:layout_constraintBottom_toTopOf="@+id/size" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/preview" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/size" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:ellipsize="end" android:maxLines="1" android:shadowColor="@android:color/black" android:shadowRadius="4" android:textColor="@color/app_name" android:textSize="@dimen/text_size_widget_list_widget_package" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/preview" app:layout_constraintTop_toBottomOf="@+id/label" /> </androidx.constraintlayout.widget.ConstraintLayout> 入れ子構造のリストを一つのAdapterで作るのは結構面倒ですが、ConcatAdapterを使えば簡単ですね。 以下のように、一つのアプリ分を作るAdapterを作ります。 class WidgetPackageAdapter( private val layoutInflater: LayoutInflater, private val info: WidgetPackageInfo ) : Adapter<WidgetViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WidgetViewHolder = if (viewType == 0) { WidgetHeaderViewHolder.create(layoutInflater, parent) } else { WidgetEntryViewHolder.create(layoutInflater, parent) } override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) { holder.apply(info, position - 1) } override fun getItemViewType(position: Int): Int = if (position == 0) 0 else 1 override fun getItemCount(): Int = info.widgets.size + 1 } abstract class WidgetViewHolder(itemView: View) : ViewHolder(itemView) { abstract fun apply(info: WidgetPackageInfo, position: Int) } class WidgetHeaderViewHolder( private val binding: ItemWidgetHeaderBinding ) : WidgetViewHolder(binding.root) { override fun apply(info: WidgetPackageInfo, position: Int) { binding.icon.setImageDrawable(info.icon) binding.label.text = info.label binding.packageName.text = info.packageName } companion object { fun create(inflater: LayoutInflater, parent: ViewGroup): WidgetHeaderViewHolder = WidgetHeaderViewHolder(ItemWidgetHeaderBinding.inflate(inflater, parent, false)) } } class WidgetEntryViewHolder( private val binding: ItemWidgetEntryBinding ) : WidgetViewHolder(binding.root) { override fun apply(info: WidgetPackageInfo, position: Int) { val widget = info.widgets[position] binding.preview.setImageDrawable(widget.previewImage) binding.label.text = widget.label binding.size.text = "${widget.minWidth} x ${widget.minHeight}" } companion object { fun create(inflater: LayoutInflater, parent: ViewGroup): WidgetEntryViewHolder = WidgetEntryViewHolder(ItemWidgetEntryBinding.inflate(inflater, parent, false)) } } あとは、各アプリごとに上記Adapterをつくって、ConcastAdapterでまとめて表示させます class WidgetDrawerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityWidgetDrawerBinding.inflate(layoutInflater) setContentView(binding.root) val list = WidgetPackageInfoList.create(this) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = ConcatAdapter().also { adapter -> list.forEach { adapter.addAdapter(WidgetPackageAdapter(layoutInflater, it)) } } ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { _, insets -> val systemInsets = insets.getInsets(Type.systemBars()) binding.recyclerView.setPadding(0, systemInsets.top, 0, systemInsets.bottom) insets } } } 以上で端末内のウィジェットの一覧表示をさせるところまでが実装できます。 ところで、ウィジェットを作ったことがあれば分かると思いますが、android:minWidthやandroid:minHeightはdpで指定しますし、ここでやったようにAppWidgetProviderInfoに格納されているminimumWidthやminimumHeightはpixelサイズです。 広く使われているホームアプリではだいたいブロック数でサイズが表示されていますが、minimumWidthやminimumHeightをそれぞれのホームアプリのブロックサイズ何個分に相当するかを計算して表示しているわけですね。 以上です。
- 投稿日:2021-09-01T17:50:43+09:00
FlutterでAndroidEmulatorに won't run without Google Play services と表示されGoogleMapが表示できない
■前提 Flutterの開発中、AndroidEmulator上に地図を出したいと思い、 GoogleMapを表示するサンプルを試しました。 https://codelabs.developers.google.com/codelabs/google-maps-in-flutter#0 Android用のキーを取得して、AndroidManifestに設定しと進めていったところ、 無事実機ではGoogleMapが表示されることを確認。 ところがEmulator上で表示しようとしたところ、 googlemap_sample won't run without Google Play services, which are not supported by your device. と表示され地図が出ません。 AndroidStudioのログには W/GooglePlayServicesUtil( 3773): Google Play Store is missing. W/Gralloc4( 3773): allocator 3.x is not supported というような怪しいログも出ています。 ・環境 ・OS macOS Big Sur バージョン11.5.2 64bit ・実機 Pixel 55 Android 11 ・エミュレータ Pixel 5 API 30 ・IDE Android Studio Arctic Fox | 2020.3.1 ■解決方法 64bit用のSystemImage(Google APIs Intel x86 Atom_64 System Image)をインストールする AVDマネージャーからAVDを編集、SystemImageを64bit向けのものに変更 この状態で起動すればGoogleMapが起動します。 VirtualDeviceを追加するときに、SystemImageを選択する箇所があるのですが、 ここのRecommended(お勧め)がx86(32bit)なんですよね。 これをそのまま選択するとハマってしまう罠となっています。 大事なのは AVDのSystemImageを64bit向けのものに変更 することです。 インストールしても設定しないと意味がない ので注意してください。
- 投稿日:2021-09-01T15:37:39+09:00
ListViewでCheckedTextViewを実装したい
前回、ListViewでCheckedTextViewを実装する方法をまとめました。 しかしその後の調査で、前回まとめた方法がツッコミどころ満載の方法だったということが分かりました。 これ以上セルフツッコミはしたくない...!ということで再びListViewについてまとめます。 前回の記事はこちらです。 CheckedTextViewの基本と、ListViewへのツッコミ力が得られるかと思います。 そして、前回実装したListViewにセルフツッコミした部分は以下の2点です。 1. ListViewの特徴であるViewの再利用ができていない 2. 要素が画面外に出た時に初期化されてしまう 今回は、前回まとめたソースコードを元に、実装を行います。 ◯実行環境 Android Studio:4.2.2 Kotlin:1.3.72 1. ListViewの特徴であるviewの再利用を実装 実はListViewには、convertViewという画面外に出たviewを再利用できる仕組みがあります。 前回私が実装したListViewでは、リスト要素の描画をするgetViewメソッドの中で毎回inflateが呼ばれるようになっていました。 しかし、inflateは重い処理のため、リスト要素がたくさんある場合に毎回呼び出すと処理が遅く、動作がカクカクしてしまいます。 そのような状況を防ぐためにListViewにはconvertViewという仕組みがあり、convertViewを効果的に使うことでぬるぬると動く操作性が実現できるとのことでした。 以下のように実装することで、画面外に出たViewを再利用します。 ①データクラスを設置 ②convertViewがnullの場合のみinflateし、nullでない場合はviewを再利用する 今回はgetViewの中でPairクラスを用いて、holderとviewの2つの値を返すよう設定しています。 Pairクラスの実装方法について:参考-Qiita MyAdapter.kt data class MyViewHolder(val checkedTextView: CheckedTextView)//①データクラスを設置 override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val (holder, view) = if(convertView == null) { // ②初回時など、convertViewがnullの場合のみinflateする val inflater = LayoutInflater.from(context) val v = inflater.inflate(R.layout.item, parent, false) val checkedTextView = v.findViewById<CheckedTextView>(R.id.checked_text_view) val viewHolder = MyViewHolder(checkedTextView) v.tag = viewHolder viewHolder to v } else { // ②再利用時など、convertViewがnull出ない場合はviewを再利用する convertView.tag as MyViewHolder to convertView } holder.checkedTextView.text = itemList[position] // viewにクリックリスナーを設定 holder.checkedTextView.setOnClickListener { val view = it as CheckedTextView if (view.isChecked) { //押し直した時にAndroidのマークになるように設定 view.setCheckMarkDrawable(R.drawable.ic_android_black_24dp) view.isChecked = false } else { //1回押した時にチェックマークが出るように設定 view.setCheckMarkDrawable(R.drawable.ic_baseline_check_24) view.isChecked = true } } return view } 以上でぬるぬる動作を実現できました。 しかし、この状態ではチェックマークの位置が正確に取れていないようです。 そこで、次は画面外に出た時にもチェックマークの位置がずれないよう対策をしていきます。 2. 要素が画面外に出た時のチェックマークを対策 チェックマークはCheckedTextViewのisChecked()による条件分けで設置しています。 1のviewの再利用を実装する前、つまり前回の記事の実装では、一度画面外に出て戻ってきた要素は全てAndroidマークに戻っていました。 これはgetView()が呼ばれた際に毎回inflateをし、新しいviewを作成していたためです。 しかし、現在はチェックマークが別の要素に移動しているように見えます。 これはどうやら、再利用するviewに設定されていた真偽値の影響を、再利用後のviewが受けてしまうことが原因のようです。 ここで先ほどの写真のテキストに注目してみると、チェックマークの位置がずれている場合でもテキストの順番は正しく表示されていることが分かります。 テキストはチェックマークと違い、viewが生成された後に改めて設定しています。 よって、チェックの状態についてもviewの再利用をした後で改めて設定するようにします。 そして今回はmapを導入し、以下のように実装しました。 ①mapのインスタンスを生成 ②クリック時にpositionをキーとして真偽値を追加する ③ ②の値を使ってチェックマークやクリック時の処理の分岐を行う MyAdapter.kt private val map = mutableMapOf<Int, Boolean>()//①mapのインスタンスを生成 data class MyViewHolder(val checkedTextView: CheckedTextView) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val (holder, view) = if(convertView == null) { // 初回時など、convertViewがnullの場合のみinflateする val inflater = LayoutInflater.from(context) val v = inflater.inflate(R.layout.item, parent, false) val checkedTextView = v.findViewById<CheckedTextView>(R.id.checked_text_view) val viewHolder = MyViewHolder(checkedTextView) v.tag = viewHolder viewHolder to v } else { // 再利用時など、convertViewがnullでない場合はviewを再利用する convertView.tag as MyViewHolder to convertView } holder.checkedTextView.text = itemList[position] if (map[position] == true) {//③map[position]がtrueのときチェックマークを設置 holder.checkedTextView.setCheckMarkDrawable(R.drawable.ic_baseline_check_24) } else { //③map[position]がnullもしくはfalseのときAndroidマークを設置 holder.checkedTextView.setCheckMarkDrawable(R.drawable.ic_android_black_24dp) } // viewにクリックリスナーを設定 holder.checkedTextView.setOnClickListener { val view = it as CheckedTextView if (map[position] == true) { //③map[position]がtrueのときチェックマークを設置 view.setCheckMarkDrawable(R.drawable.ic_android_black_24dp) map[position] = false //②positionをキーとして真偽値を追加 } else { //③map[position]がnullもしくはfalseのときAndroidマークを設置 view.setCheckMarkDrawable(R.drawable.ic_baseline_check_24) map[position] = true //②positionをキーとして真偽値を追加 } } return view } 以上で、チェックマークのずれないCheckedTextViewを実装することができました。 ここまでのコードの全体像は以下です。 MainActivity.kt import android.os.Bundle import android.widget.BaseAdapter import android.widget.ListView import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity(){ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //方法② arrayからとってくる val itemList = resources.getStringArray(R.array.nutrients) //activity_main.xmlに定義したListViewを読み込む val listView = findViewById<ListView>(R.id.list_view) // リスト項目とListViewを対応付けるArrayAdapterを用意する // ArrayAdapterではcontext、1項目分のレイアウトファイル、項目を定義した配列を指定する val adapter: BaseAdapter = MyAdapter(this, itemList) listView.adapter = adapter } } MyAdapter.kt import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.CheckedTextView class MyAdapter( private val context: Context, private var itemList: Array<String> ) : BaseAdapter() { override fun getCount(): Int { return itemList.size } override fun getItem(position: Int): Any { return itemList.get(position) } override fun getItemId(position: Int): Long { return position.toLong() } private val map = mutableMapOf<Int, Boolean>()//①mapのインスタンスを生成 data class MyViewHolder(val checkedTextView: CheckedTextView) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val (holder, view) = if(convertView == null) { // 初回時など、convertViewがnullの場合のみinflateする val inflater = LayoutInflater.from(context) val v = inflater.inflate(R.layout.item, parent, false) val checkedTextView = v.findViewById<CheckedTextView>(R.id.checked_text_view) val viewHolder = MyViewHolder(checkedTextView) v.tag = viewHolder viewHolder to v } else { // 再利用時など、convertViewがnullでない場合はviewを再利用する convertView.tag as MyViewHolder to convertView } holder.checkedTextView.text = itemList[position] if (map[position] == true) {//③map[position]がtrueのときチェックマークを設置 holder.checkedTextView.setCheckMarkDrawable(R.drawable.ic_baseline_check_24) } else { //③map[position]がnullもしくはfalseのときAndroidマークを設置 holder.checkedTextView.setCheckMarkDrawable(R.drawable.ic_android_black_24dp) } // viewにクリックリスナーを設定 holder.checkedTextView.setOnClickListener { val view = it as CheckedTextView if (map[position] == true) { //③map[position]がtrueのときチェックマークを設置 view.setCheckMarkDrawable(R.drawable.ic_android_black_24dp) map[position] = false //②positionをキーとして真偽値を追加 } else { //③map[position]がnullもしくはfalseのときAndroidマークを設置 view.setCheckMarkDrawable(R.drawable.ic_baseline_check_24) map[position] = true //②positionをキーとして真偽値を追加 } } return view } } list_item.xml <resources> <string-array name="nutrients"> <item>タンパク質</item> <item>脂質</item> <item>飽和脂肪酸</item> … </string-array> </resources> activity_main.xml <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> <ListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> item.xml <?xml version="1.0" encoding="utf-8"?> <CheckedTextView android:id="@+id/checked_text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:textSize="20sp" android:checked="false" android:checkMarkTint="@color/teal_700" android:checkMark="@drawable/ic_android_black_24dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android" /> まだまだツッコミどころは残っていた... 望んだ動き自体は実装することができました。 しかしコードを見ていると、より良い方法があるのではないかと思う部分があります。 例えば、以下のような点です。 ①折角CheckedTextViewを使っているのにそのチェック状態を使っていない ②テキストを保持する配列、チェック状態を保持するmap、viewを保持するデータクラスなどコレクションが乱立している ①に関してはもはや本末転倒な気がしていますが、現状私の力では今回の実装が精一杯でした。ここを通過点として今後もより良い方法を探していきたい気持ちです。 改善点やお気づきの点がありましたらぜひコメントをお願いいたします。 参考 Qiita:AndroidのListViewやRecyclerViewの、ViewHolderやDataBindingを調べた記録 Qiita:AndroidのViewHolderパターンをKotlinでいい感じに書く stackoverflow:BaseAdapter causing ListView to go out of order when scrolled
- 投稿日:2021-09-01T01:01:33+09:00
AndroidのUI開発にJetpack Composeを使ってみる(1)
前提条件 Android Studio Arctic Fox以降 Kotlinの基本的文法 導入 Android Studioを開いてNew Project -> Empty Compose Activityを選択しましょう。 ただし古いバージョンのAndroid Studioを使っている場合は該当部分が存在しません。 アプリケーションの情報を決めると以下のようなテンプレートコードが生成されます。(2021/8/31現在) MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeApplicationTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { Greeting("Android") } } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") } @Preview(showBackground = true) @Composable fun DefaultPreview() { ComposeApplicationTheme { Greeting("Android") } } ここで重要なのは、 MainActivity -> onCreate -> setContent内にコンポーズ可能な関数から構成されるカタマリを記述する @Composable修飾子をつけた関数はコンポーズ可能であるという @Preview修飾子をつけた関数に配置したコンテンツがAndroid Studio上でプレビューとしてみられる ここで以下の説明のため一旦setContent内とMainActivity外部の2つの@Composable関数は消去した状態にします。 MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } } } 方針 setContent内が複雑になるのを防ぐため、新しくComposable関数MyAppを定義します。UI部品の組み合わせはMyApp内部で行ってsetContentにはMyAppのみを配置するのがオススメです。また、エディタでプレビューを見られるようにするためにPreview関数も作っておきます。 MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApp() // コレ以外書かない } } } @Composable fun MyApp() { // ここにUI部品を並べていく } @Preview(showBackground = true) @Composable fun Preview() { MyApp() } このようにすることで、MyApp関数を編集していくだけで自動的にプレビューとエミュレータないし実機でのデバッグ時に、同じUIテストができるようになります。 UI部品の配置 後述するUI部品を、並べて配置する際はRow()やColumn()が使えます。ここで使うUI部品は後述するので今は流し見してください。 Row Composable関数を横並びで表示したい場合は、並べたい部品をRow()の内部で与えることで重ならずに表示することができます。 @Composable fun MyApp() { // ここにUI部品を並べていく Row() { Text(text = "ちくわぶ") Text(text = "ちくわ") } } Column UI部品を縦に並べて表示したい時はRow()と同様にColumn()で括ることで実現できます。 @Composable fun MyApp() { // ここにUI部品を並べていく Column() { Text(text = "ちくわぶ") Text(text = "ちくわ") } } UI部品 Text 画面上にテキストを配置する際はText()を使います。引数に文字列を指定することで画面に表示することができます。これを使って引数wordsの文字を画面に表示する@Composable PrintText()を定義しましょう。 @Composable fun MyApp() { // ここにUI部品を並べていく PrintText(words = "おはようございます") } @Composable fun PrintText(words: String) { Text(text = words) } 今、PrintText()は構成されるComposable部品にText()1つしか持たないので、MyApp()にそのままText(words)を直書きしても構いませんが、後に機能拡張する場合を考慮して別関数にしています。 ここで、画面に表示するためのComposable関数はMyAppに並べるのを忘れないようにしてください。実行すると画面左上に下図のような文字が出力されるはずです。 よく使うText()のパラメータとして以下のものが存在します。 - color : テキストの色指定 - fontSize : テキストのサイズ変更 - maxLines : テキストの最大表示行数指定 以降でも共通の概念ですが、ここで2つの異なるText()を表示する場合は注意が必要です。 @Composable fun MyApp() { // ここにUI部品を並べていく PrintText(words = "おはようございます") PrintText(words = "今日はもう寝ます") } このようにPrintText()を並べた場合、位置が指定されていないので2つのテキストが被さってしまい次のように表示されてしまいます。 並べて配置する際は、先述したUI部品の配置にあるRow()やColumn()を使いましょう。 @Composable fun MyApp() { // ここにUI部品を並べていく Column { PrintText(words = "おはようございます") PrintText(words = "今日はもう寝ます") } } 実行結果は以下のようになります。 TextField, OutlinedTextField テキスト入力を行いたい場合はTextField()やOutlinedTextField()が使えます。 @Composable fun MyApp() { // ここにUI部品を並べていく Column() { TextField(value = "ほげほげ", onValueChange = {}) OutlinedTextField(value = "ほげほげ", onValueChange = {}) } } OutlinedTextField()を使う際には、 - value : 入力エリアの値 - onValueChange : 値が変化した時の処理 - label : 入力エリアの題名のようなもの を引数で与えましょう。(labelはなくても良い) また、テキストが変更された際に入力中であることを表すために、画面に表示されるvalueを変更し、その変更に伴ってUIが変更されるようにしたいです。これは再コンポーズと呼ばれ、今回のOutlinedTextField()の場合は以下のように記述するのが良いです。 @Composable fun PrintTextField() { // textが変更されるとPrintTextFieldが再コンポーズ(再描画みたいなもの)される var text by remember { mutableStateOf("") } OutlinedTextField( onValueChange = { text = it }, // 値が変わったらそれをtextにいれる value = text, // textの値をvalueに入れる->画面に表示される label = { Text(text = "ここに入力")} ) } remember()内のmutableStateOf()に渡す引数の値は、TextFieldに表示されてる初期値だと思ってもらえれば良いです。 Image 画像を表示する簡単な方法としてImage()があります。前準備としてapp->res->drawableに貼りたい画像ファイルを置いておきます。 @Composable fun MyApp() { // ここにUI部品を並べていく Image( painter = painterResource(id = R.drawable.ckwb), contentDescription = "プロフィール画像" ) } painterResource()に与えている引数のidはR.drawable.(ファイル名)の形です。 細かなレイアウト指示 Modifier Text()やImage()などComposableな関数には引数にmodifierを与えることができることが多いです。ここでは細かいレイアウトの指示を行うことができます。 @Composable fun MyApp() { // ここにUI部品を並べていく Row { DisplayMyImage() // 画像表示 ProfileTextArea() // 名前と本文の表示 } } @Composable fun DisplayMyImage() { Image( painter = painterResource(id = R.drawable.ckwb), contentDescription = "プロフィール画像", modifier = Modifier .padding(all = 8.dp) .size(50.dp) .clip(CircleShape) ) } @Composable fun ProfileTextArea() { Column() { PrintName(name = "ちくわぶ") PrintBody(text = "ほげほげとふがふがの間") } } @Composable fun PrintName(name: String) { Text( text = name, color = Color.Green, style = MaterialTheme.typography.subtitle1, modifier = Modifier.padding(all = 4.dp) ) } @Composable fun PrintBody(text: String) { Text( text = text, style = MaterialTheme.typography.body2, modifier = Modifier.padding(all = 4.dp) ) } 実行すると以下のようなプロフィールが出力できます。 参考 Jetpack Compose Tutorial