どうもこんばんわ。
スタディ§ステディ2 攻略しました、えちえちでした
E-mote 搭載だから立ち絵がめっちゃ動く、
本編関係ないけど UI もアニメーション頑張っててすごいと思った(こなみかん、大変そう
やえちゃん!!
前作ヒロイン?とのやりとりがあったんですけどやえちゃんルートのが好み
なまりかわいい
由乃ちゃんかわいいのでぜひ!!!
この目すき
イベントCG貼るわけには行かないけどヒロインも背景もめっちゃきれいでした、すごい
過去の話とかちょろっと出てくるので由乃ちゃんルートは最後が良いかも?
本題
NewRadioSupporter
にウィジェットを追加しました!お待たせしちゃいました
ホーム画面からすぐ確認できます!!!
大きいサイズも作りました、
後述しますがJetpack Glance
が面倒なことを全部肩代りしてくれたので大きいサイズも難しくないです。
(小さいアプリみたいで結構気に入ってる)
どうでもいいけどドコモのn28
見つけた、わーい
高いキャリア版買ってよかった(まぁミリ波アンテナほしかったし...)
で、今回はこのNewRadioSupporter
でウィジェットを作るために使ったJetpack Glance
のお話です。
Android Jetpack Glance
https://developer.android.com/jetpack/compose/glance
Jetpack Compose
な文法で、Android
のホーム画面ウィジェット
が作れる。
うーんxml + RemoteViews
でウィジェットを作ったことがある身からすると現実味が無いんだけど、確かに動いているんですよね。
なんで動いてるか分からんくて本当に魔法みたい
不思議でたまらないけど、、、もしこれなら難しすぎて地獄のxml + RemoteViews
から開放されるってことでいい?
Android のウィジェットは難しい
レビューに来てたし私も欲しかったんですけど、、、
あ、この部分は読んでも読まなくてもどっちでもいいです。
Android 2 とかから存在するのでメソッドが古いまんま
しばらく放置されていたイメージ、、あと後方互換のために下手に手を出せなかった可能性。
古いから情報があるのかと思うとあんまり...。
実際公式ドキュメントの日本語版はいつのスクリーンショットだよって言いたくなる、、はぁ
(日本語版なんて見るなという話ではある
RemoteViews で使える View のみ
決まったView
しか使えない!
https://developer.android.com/reference/android/widget/RemoteViews
ConstraintLayout
も使えません。RelativeLayout
とか今更やるくらいならLinearLayout
とかFrameLayout
使ったほうが良さそう。
あと、xml
がベースなので、ちょっと凝った事するとめんどいです。角を丸くしたい場合はカスタムDrawable
を作らないと行けない、Jetpack Compose
だと一発なのにつらいね。
また、findViewById
は使えない。じゃあどうやってテキストや画像をセットするんだって話ですが、
RemoteViews
にテキスト設定メソッド、画像設定メソッドがあるので、xml
のandroid:id
と値をそれぞれセットしていく形になります。
https://developer.android.com/reference/android/widget/RemoteViews#setTextViewText(int,%20java.lang.CharSequence)
Visibility
変更や、Drawable
セットなんかの基本的なやつはありますが、あんまり凝ったメソッドまではないので出来ない事もある。
一応、リフレクションじみた事ができますが、、、これも万能ではないらしく、LinearLayout
のsetOrientation
しようとしたら落ちた
https://developer.android.com/develop/ui/views/appwidgets/enhance#use-runtime-mod-of-remoteviews
あとつらいのがリスト系。Gmail
とかGoogle Keep
とかのリストでアイテム増やしたりできるやつ。あれが難しい
最近はドキュメントが充実してて良い感じなのですが、リストの各アイテムを押せるようにするためには、あらかじめメソッドを呼び出す必要があるとか、
リストへデータを渡すためのクラス、なんかよくわからないやつを継承するんだけど、実装しないといけないメソッドが多く圧倒される。
すぐ壊れる
レイアウトを変更させると?、ウィジェットは利用できませんみたいな表示になって、置き直さないといけない
アプリアイコン長押し→ウィジェット
を押すとすぐ再設置できます(時短テク
https://support.google.com/android/answer/9450271?hl=ja#zippy=%2Cウィジェットを追加するサイズ変更する
Logcat
追ってったらクラッシュするようなコード書いてたうわーみたいな。
ボタンを押したときの処理が難しい
ワナが多すぎる
前述の通りfindViewById
が使えないので、setOnClickListener { }
なんてものは使えない。
なので、あらかじめPendinIntent
という押した時に発行するIntent
を設定しておきます。(すぐ使うわけじゃないのでPending
なIntent
)
アプリの画面を開くPendingIntent.getActivity
、サービスを起動するPendingIntent.getService
、任意のコードを呼ぶためのコールバックを受け取るPendinIntent.getBroadcast
など。
任意のコードを呼ぶためのコールバックを受け取るやつはAppWidgetProvider#onReceive
に書く。すぐブロードキャストレシーバーが使える状態にはなってる。
PendingIntent
を作る際は、このクラス(以下の例だとExampleWidget
)に向けたPendingIntent
を作ってセットすれば良い。
context
がもらえるけど、Activity
のcontext
ではないのでダイアログは出せない。
Kotlin Coroutines の suspend 関数
を呼びだければgoAsync()
メソッドを見てみてください。
ぱっと見何も難しいことはないように見えるが、、、なんかたまによく反応しない時がたまによくあるんですよね。
PendingIntent
の引数に渡したrequestCode
が重複していると押してもBroadcastReceiver
が反応しない・・・から重複しないようにすれば直るとか...
ウィジェットを再設置するとボタンが反応するようになる時がある...とか。
難しすぎる
書き出してみるとそんなに無いな...確かにしんどかった記憶はあるんだけど。
Android Jetpack Glance
これらの問題が結構解決します!!
Jetpack Compose の文法で書ける
xml
で書く必要がないのでとても見やすい。やっぱレイアウトと行き来するのつらいよな。
使えるコンポーネントはandroidx.glance
パッケージ傘下にあるComposable 関数
に限定される。最終的にはRemoteViews
に変換するため致し方ない。
(なので、よってJetpack Compose
とUIコンポーネント
を相互利用できるわけではない)
ただ、UI
関係ないComposable 関数
は使えちゃいます!
Flow#collectAsState
とかLaunchedEffect { }
とか。なんで動くのか本当に不思議
そして何より嬉しいのが、リスト系がめちゃめちゃ簡単に作れるようになった
LazyColumn
がGlance
でも動く。本当に感動。こんな楽していいのか?
ウィジェットを少しの間動的にできる
どういうことかというと、Jetpack Glance
はJetpack Compose
の文法で書かれたUI
をRemoteViews
に変換するために、Composable 関数
を監視しておく必要があるわけで、
その監視のための時間があるわけです。その間は動的なので、remember { mutableStateOf() }
なんかも使えちゃうわけです。
完全なコード→ https://github.com/takusan23/GlanceCountWidget
最新の Android Studio でビルドできるはずです。
Jetpack Glance
でFlow#collectAsState
やLaunchedEffect { }
が動くのは、この動的な間が存在するからなんですね。
ちなみに、数十秒間の間しか動かないので、もし状態を保持しておきたい(上の例だとカウントを引き継ぎたい)場合はSharedPreferences
やDataStore
等の保存するためのシステムを用意する必要があります。
詳しくは:https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
そこそこ新しい技術で動いている
Jetpack Compose
からRemoteViews
に変換する部分でWorkManager
?が使われていたり、非同期処理にKotlin Coroutines
が使われています。神!!!
Modifier
Jetpack Glance
専用のGlanceModifier
が用意されています。
角を丸くする処理がJetpack Compose
と同じく一行で書けます。
もうカスタムDrawable
作らずに済みます・・・!
押したときの処理
これもめっちゃ簡略化されていて、なんとGlanceModifier.clickable { }
を使うだけです。
内部ではBroadcastReceiver
が動いているため、Context#startActivity
が呼び出せない等の成約があるものの、BroadcastReceiver#onReceive
にクリックイベントを書くより何倍も分かりやすい!!!
もちろんActivity
を開く方法もちゃんとあります。
実際に作ってみる!!
今回は写真を一覧表示するウィジェットでも作ってみましょう。
ただ、それだと既にあると思うので、写真を押したらウィジェット内で表示するように(一覧表示から1つだけ)してみます。
本当にミニアプリを目指していきます。
いや~~~これRemoteViews
でやろうとするとめっちゃだるいやろなぁ
環境
こうしき https://developer.android.com/jetpack/compose/glance
なまえ | あたい |
---|
Android Studio | Android Studio Giraffe 2022.3.1 Patch 1 |
端末 | Xperia 1 V / Google Pixel 6 Pro |
targetSdk | 34 (Android 14) |
適当にプロジェクトを作る
Jetpack Compose
が入っているプロジェクトである必要があります。新規に作るならEmpty Compose Project
?
Jetpack Compose
ない場合はまず入れるところからですね
Empty Compose Project
で、後は適当に、
Android
最低バージョンは6
が良いらしい(Jetpack Glance
が5
をサポートしてないとかなんとか)
kts
を使うかはおまかせします、新規で作るならkts
で良いかも、今あるプロジェクトを書き換えてまではメリットなさそう。
Jetpack Glance を入れる
app
の中のbuild.gradle.kts
(もしくはbuild.gradle
)で、以下を足す
glance
と、画像読み込みライブラリのGlide
です。
あとは、targetSdk
を34
にします。
xmlを書く
ウィジェットのメタデータ(最小幅とか、ウィジェット設定用 Activity の指定)を書く。
res
の中にxml
があるはずなので、そこに適当にtouch_photo_widget_info.xml
みたいなのを置く
後は適当にコピペします。
これらのメタデータの詳細は以下。
https://developer.android.com/develop/ui/views/appwidgets#other-attributes
description
とかpreviewImage
はちゃんとしておいたほうが良い
Jetpack Glance のクラスを用意する
2つ用意します。
まずは GlanceAppWidget
を継承したクラスを作ります。
次に、GlanceAppWidgetReceiver
を継承したクラスも作ります。
glanceAppWidget
は、↑で作ったGlanceAppWidget
を継承したクラスのインスタンスを渡せば大丈夫
多分こうなってるはず。
AndroidManifest を書く
<receiver>
を書きます。
android:name=".TouchPhotoWidgetReceiver"
と、android:resource="@xml/touch_photo_widget_info"
は各自違う値になるはず!
ウィジェットのレイアウトを書く
TouchPhotoWidget
(GlanceAppWidget
を継承したクラス)を開き、provideGlance
の中で、provideContent { }
を呼び出します。
provideContent
のブロック内はComposable
なので、あとはここにレイアウトを書いていくだけです!
まじで魔法みたいに動く。
適当に、Hello World
と、あと押したらアプリを起動するようにするにはこんな感じで。
前述の通り、Glance
用のコンポーネントを呼び出す必要があるので、import
の際は注意してください
(GlanceModifier
が引数に入っているか、パッケージ名がandroidx.glance
で始まっているか)
あとは実行して、実際にウィジェットを置いてみてください、
どうでしょう、ちゃんとHello World
が出て、押して起動しますか?
RemoteViews
時代よりずっっっっっっっと簡単になりましたね、感動
いよいよ写真ウィジェットを作る
が、その前に写真の取得の話をしないとなんですよね。
Android 13
以上をターゲットにする場合(今回は14
なのでもちろんこちらの対象)、READ_EXTERNAL_STORAGE
ではなくREAD_MEDIA_IMAGES
の権限を宣言してリクエストする必要があるそう。
ただし、この権限は13
からなので、それ以前のAndroid
をサポートする場合は引き続きREAD_EXTERNAL_STORAGE
を宣言してリクエストする必要がある。
(ちなみにドキュメントに書いてあるかわかんないですが、自分が作った写真(自分のアプリからContentResolver#insert
)の場合は↑の権限無しで取得できたはず。。。)
写真を取り出すには
パスを渡す方法は取れません(Android 10
からのScoped Storage
のせい)。つまり、以下のような方法は取れません
val file = File("sdcard/DCIM/Example.jpg")
じゃあどうするんだって話ですが、Android
では画像
等のメディアはデータベースみたいなやつに問い合わせると取得できるようになります。
MediaStore
とかContentResolver
とか言われてるやつです。
例です、こんな感じだと思う。
SQL
っぽいやつで取得したいメディアを取り出して、cursor
で上から舐めていきます。
もちろん、これを実行する前に権限があるかのチェックが必要です。
(0 until 10).map { }
とかで指定した数の配列を作れます。書いてて楽しいKotlin
写真ウィジェットを作る
権限
AndroidManifest.xml
に書き足します。
権限を要求する
MainActivity
に書きます。
権限がなければ要求するボタンを出します。簡単になりましたね。
画像の取得処理
画像を読み込むユーティリティクラスを用意します!!!
ID
をとって、Uri
にして、Bitmap
を取得する感じです。
、、、と思ってたんですけど、メモリ使い過ぎで怒られたため、Glide
というライブラリでBitmap
を読み込むようにしました。
ウィジェットに入れる
あとはウィジェット上で画像を表示するだけ!
Jetpack Compose
みたいな感じで書いていけば良いはず。LocalContext
はなさそうなので、引数にあるContext
をバケツリレーすると良さそう
使ってみる
こんな感じに一覧表示されてて、
押すと一枚だけ表示されます!
戻る
ボタンを押すと戻れます。あと開く
を押すとアプリを選択する画面が出ます!
Intent(Intent.ACTION_VIEW, photoData.uri)
←この第2引数
にUri
を入れるやつ
ちなAndroid 14
で押したら落ちた
PendingIntent
の引数をFLAG_IMMUTABLE
にしないといけないんだけど、現状はGlance
が内部でPendingIntent
を作っているので直せない。
RemoteViews
を作って、PendingIntent
を渡して、そのRemoteViews
をGlance
で使う(相互利用ができる)しかなさそう・・・
見た目を整える
色
まずGlanceTheme
が使えるように、親をGlanceTheme
で囲います。
Android 12
以降はDynamic Color ( Material You )
を使います。
それ以下で使うためのcolors
は、Empty Compose Project
書いた時に付いてきたやつを使うことにします。
背景の色は、secondaryContainer
にすると良さそうです。
ボタンの色は、アイコンだけのやつだとprimary
っぽい。
塗りつぶしアイコンの場合は、塗りつぶしがprimary
で、アイコンの色がprimaryContainer
っぽい(Container
が逆転!?)
あと適当にアイコンを持ってきました。Icon
は多分なさそうなので、Image
を使うであってそう。
こんな感じにしてみた。他のウィジェットも同じ感じの色使いしてそう!
ちなみに、primaryContainer
は実際にはAndroid
のカラーリソースのID
を指している(android.R.color.xxxxx
みたいな)ので、以下のcolors.xml
を見ると実際のID
を見ることが出来ます。
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance/src/main/res/values-v31/colors.xml;l=1
primary
は@android:color/system_accent1_600
を指定するのと同じ働きをするみたいですね!
レスポンシブデザイン
これだと、横幅に関係なく4つ
表示しているので、幅がないときは2
とかにしたい!みたいなことが出来ます。
これもJetpack Glance
なら簡単にできるのでやります。(RemoteViews
だったらやりたくない
まずは大きさを定義して
次に、GlanceAppWidget
のsizeMode
をオーバーライドして、SizeMode.Responsive
を作って返してあげます。
あとは、LocalSize
が使えるようになるので、グリッド表示コンポーネントの横並びセル数の部分で使います。
これで、横幅が小さいときは横並びが2つになりました。
多分もっと刻むことができるはずです。いや~~楽ですねこれ
ちなみに、普通にこーゆーこともできるので、レイアウト全体を変えることも出来ます。
本当になんで動いてるか不思議だ...
ここまでコード全部
Android Jetpack Glance 知見
最後にこれ作ってたり、NewRadioSupporter
のウィジェット作ってたときの知見を残します。
provideContent { }
は45
秒間動き続ける?
- ↑ の間、
collectAsState
とかが動くのか
GlanceAppWidget#provideGlance
のドキュメントを見てください。
- ウィジェットを更新する関数があります。
TouchPhotoWidget().update()
とかTouchPhotoWidget().updateAll()
とか
- これらは、
provideContent { }
が動いているときは特に何もしなさそう
WorkManager
が動いている間?は既にウィジェット動いてるので特に何もしない?
GlanceModifier.clickable { }
の際、provideContent { }
が起動していなかったら起動するぽい
WorkManager
が裏で動く
- ボタンを追加で押すと、
45
秒のタイムアウトも延長するらしい
- 多分レイアウト変更時に再設置が必要なのは変わってないと思う、
RemoteViews
だから仕方ないのかも
おわりに
これもiOS
でウィジェットが入った影響みたいなのがあるんですかね?
もしも入らなければ、Android
もウィジェット周りの改修が入らず地獄のままだったのでしょうか・・・ありがとうiOS
ソースコード置いておきます。最新のAndroid Studio
で実行できるはずです。
https://github.com/takusan23/TouchPhotoWidget
おわりに2
BroadcastReceiver
で思い出した、レシーバーのスペル、たまにReciever
って書いちゃう
おわりに3
NewRadioSupporter
審査出ししました