AndroidアプリでWidgetをつくるまでの備忘録
備忘録も兼ねておいておきます
目標
- Widgetを持つAndroidAppを作る(公開はしない)
環境
- Windows11 Home
筆者の環境
- Visual Studio Codeユーザー
- 普段はWebのフロントエンドを触っている
- .NetFramework WinFormを中学生の時少しだけ触ったが、UIの作り方についてはreactやvueなどの仮想DOMを使ったやり方に慣れている
本題
環境構築
環境構築:インストール
環境構築:Android Studio
参照元: https://developer.android.com/studio/install?hl=ja
- https://developer.android.com/studioからダウンロード
- Installer実行
Android Studioの日本語化(任意)
参照元: https://neet-rookie.hatenablog.com/entry/AndroidStudio日本語化
- Android Studioを立ち上げて、Help > Aboutからバージョンを確認
- そのバージョンのプラグインを(JetBrainsマーケットプレース)[https://plugins.jetbrains.com/plugin/13964-japanese-language-pack------/versions]からダウンロード(Not compatible with the version of your running IDE (Android Studio Giraffe | 2022.3.1 Patch 2)といわれるのは、Getを押してダウンロードしてもそのバージョンは違うから別のバージョンを自分で選んでねという意味)
- File > Settingsをクリック
- そのなかからPluginsをクリック
- 上の歯車アイコンをクリック
- Install Plugin from Diskをクリック
- ダウンロードしたzipファイルを選択 -> Okをクリック
- Restart IDEをクリックして、IDEを再起動する
Android Studioのキーマップ設定(任意)
VSCodeのキーマップを追加
既存のIDEやエディタのキーマップを読み込めます。すごい。
Visual Studio Codeは選択肢にはないですが、別途追加することができます。
ファイル > 設定 > キーマップ で以下のウィンドウを開きます。
以上の画像を参考にプラグインマネージャを開いてください。
開いたプラグインマネージャから「VSCode Keymap」を追加してください
キーマップの変更
ファイル > 設定 > キーマップ でまた設定項目を開いてこんどは今現在「Windows」と表示されている場所からVSCodeを選択してください
個別でキーマップ変更
ファイル > 設定 > キーマップから変更します。
おすすめは「選択範囲の拡大」を「Ctrl+l」あたりに割り当てると便利です。
またここで変更したキーマップは、別途保存されるため困った場合はもう一度「Keyマップの設定」からVSCodeを選択してください
学習
Kotlin
とりあえずとほほのKotlin入門を見る。とほほさん森羅万象への入門があるので助かる。
Android App & Kotlin Compose
公式のチュートリアルを少しだけする
Widget
compose glanceでトライ
文章内でimplementation "androidx.glance:glance-material3:1.0.0"
を追加しろと言われているが、追加先はGradle Scripts/build.gradle.kts(Module :app)
に入れる
ただしimplementation("androidx.glance:glance-material3:1.0.0")
とする必要がある。
bingさんいわくこういうことらしい
implementation "androidx.glance:glance-material3:1.0.0"
とimplementation("androidx.glance:glance-material3:1.0.0")
の両方は、Gradleの依存関係を追加するための異なる記法ですが、どちらも同じ結果をもたらします。
implementation "androidx.glance:glance-material3:1.0.0"
はGroovy記法で、Gradleの初期バージョンで使用されていました。
implementation("androidx.glance:glance-material3:1.0.0")
はKotlin DSL記法で、Kotlinを使用したGradleスクリプトでよく使われます。
どちらの記法を使用しても、同じライブラリがプロジェクトに追加されます。ただし、プロジェクトがKotlinで書かれている場合や、Kotlin DSLを使用している場合は、Kotlin DSL記法(つまり括弧を使用する方法)を使用することをお勧めします。これにより、一貫性が保たれ、混乱が避けられます。
Create an app widget with Glanceまでやったとこで、glanceがないといわれてしまう
Unresolved package 'glance'
Unresolved class 'MyReceiver'
...
ちょくちょく「GlanceSnippets.ktに書けって言われるけどそのファイルどこに追加すればいいんだよ」っていう基礎知識不足が見えるのと、どうやらglaceを使わないwidget作成方法があることを知ったので、そっちに手を出してみる。
try AppWidgetProvider
Glaceを使わないタイプ(古いタイプ?)のやり方でやってみる
まずAppWidgetHost
を作れと言われているので、これはまた別の公式チュートリアルを参考に作る。
多分Host
側とWidget
が、それぞれwebでいうサーバーとクライアント見たいな感じになるのかな?
ただ、ちゃんと読んだら(最初からちゃんと読め)、どうやらアプリウィジェットをホストってのはbingさんに聞いた感じ必ずはしなくていいみたい。
というわけで、もういちどGlaceを使わないタイプ(古いタイプ?)のやり方に戻ってやってみる。
基本情報
アプリウィジェットの構成要素
- AppWidgetProvider: ハンドラーをここに書くイメージ
- AppWidgetProviderInfo: xmlで定義するウィジェットのメタデータ
- ビューのレイアウト: 初期状態
設定画面(Activity)も作れるよ。
AppWidgetProvider
AndroidManifest.xml
に以下のコードを追加
android12以降はreceiver
のandroid:exported
が必須みたいなのでつけてあげる。
<receiver android:name="ExampleAppWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>
<action>
とかはproviderに提供するイベントを追加できるみたい。ここで追加しなくてもデフォルトで受け取れるイベントはいくつかある。
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
この部分でmeta-data、つまりAppWidgetProviderInfoを指定している。
AppWidgetProviderInfo メタデータを追加する
さっき作ったAndroidManifest.xml
でandroid:resource=...
と指定している部分で参照しているように、以下の内容をres/xml/example_app_widget_info.xml
に追加する。
android:previewImage="@drawable/preview"
ってのはwidget選択画面で表示される画像。任意なので、今回はdrawableに追加するのがめんどくさいのでここでは追加しない。
@layout/example_appwidget
とandroid.ExampleAppWidgetConfigure
でエラーが出るけど気にしない。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.プロジェクト名.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
widget用のレイアウトを追加する
レイアウトを追加する際はres/layout
に追加する
とりあえず今回はこんな感じの単純なレイアウトを追加してみる
res/layout/example_appwidget.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="136dp"
android:text="はじめてのwidget" />
</LinearLayout>
AppWidgetProvider クラスを使用する
いろいろ使い方が書いてあるけど、どこにファイルを追加すればいいかがわからない。とりえあずbingさんに聞いてみる。
AppWidgetProviderを継承したクラスは、通常はプロジェクトのソースコードディレクトリに保存します。具体的な場所はプロジェクトの構造によりますが、一般的にはsrc/main/java/ディレクトリ以下の適切なパッケージ内に配置します。
例えば、com.example.myapp.widgetsというパッケージを作成し、その中にAppWidgetProviderを継承したクラスを作成することができます。その場合、ファイルの場所はsrc/main/java/com/example/myapp/widgets/MyAppWidgetProvider.javaとなります。
ということなので、javaディレクトリを右クリック、新規>パッケージからcom.example.trywidget2.widgetsというパッケージを作り、その中にExampleAppWidgetProvider.ktを作成すればいいのかと思ったが、どうやら違いそう。
AndroidManifest.xml
でExampleAppWidgetProvider
を指定しているので、正しくは**MainActivity.kt
と同じディレクトリに新しくExampleAppWidgetProvider.kt
を作る**
チュートリアルで紹介されているプロバイダをそのまま置いてみる
package com.example.trywidget2.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
class ExampleAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
// Create an Intent to launch ExampleActivity
val pendingIntent: PendingIntent = Intent(context, ExampleActivity::class.java)
.let { intent ->
PendingIntent.getActivity(context, 0, intent, 0)
}
// Intentから通じてonClickイベントをActivityに設定する
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.appwidget_provider_layout
).apply {
setOnClickPendingIntent(R.id.button, pendingIntent)
}
// AppWidgetManagerに、現在のアプリ・ウィジェットのアップデートを実行するよう指示する。
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
ここで言われているCreate an Intent to launch ExampleActivity
のIntent
がわからないのでbing先生に聞く
Androidアプリ開発における「インテント(Intent)」は、「何かを行う意図」という意味で、特定のアクションをリクエストする時に使用できるメッセージングオブジェクトのことを指します。
例えば、別の画面を開きたい時に、AndroidではActivity単位で画面が作られますが、「開いている画面に対して別の画面を開いてください」とアプリにメッセージとして伝えることで、別の画面を開くことが可能になります
- Activityを起動
- Service(バックグラウンドで動く奴)を起動
- ブロードキャストの配信(全アプリに対して送るメッセージ)
みたいなことができるものみたい。
で、Intentには以下の情報を渡すみたい。
Context:現在コンテキスト(通常は現在のアクティビティ)
Class:遷移先アクティビティ
Action:実行するアクション ex.「電話をかける」、「ウェブページを開く」
Data:アクションに関連するデータ
などなど・・・
ただActivityを用意するのがめんどくさいなぁと思った別の方法も調べてみた。
web開発とかだとalert()とかよく使われるけど、android開発だとLog
というものが使われるみたい。
Log.d("MyActivity", "MyClass.getView() — get item number $position")
こうするとLogcatウィンドウでログ出力が確認できるみたい。
ちゃんと読んでみるとちゃんと後でActivityの作成をしているので、今回は言われたとおりにやってみる。
とりあえずインポートできそうなやつだけインポートすると、ExampleActivity
,R.layout.appwidget_provider_layout
,R.id.button
だけがエラーの状態になる。
アプリウィジェットを固定する
固定しなくていいので今回はパス
アプリ ウィジェットの設定アクティビティを作成する
新しくアクティビティを宣言する際は、AndroidManifest.xmlに追加する必要がある
.ExampleAppWidgetConfigure
がエラーを吐くけど気にしない
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
sdkのバージョン
sdkのバージョンが34以上が必要といわれたので、build.gradle.kts
でandroid.compileSdk
とandroid.defaultConfig.targetSdk
を33
から34
に修正
Widgetないんだけど!?
ただここまで来て、アプリアイコンを長押ししてもwidgetが出てこない
これは仕様ではなくしっかりミス。
自分のやりたいことをと、今回書いたコードを全部bingさんに投げてみたところ、example_appwidget_info.xmlの設定の仕方がミスってたみたい。
正しくはこう
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.trywidget2.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
なんかアプリ開かれるんだけど!?
それはexample_appwidget_info.xml
でandroid:configureにExampleAppWidgetConfigureを設定したから。
今回は何も設定がいらない前提なので、その行を消してみるとこんな感じにwidgetが設置できる。
ここまで3日かかった、長かった…
くそださウィジェット
いまのままだとさすがにくそださウィジェットすぎるので、少しだけデザインをちゃんとしてみる。
私「androidのwidgetのuiを作ってほしいんだ。まず必要な要素としてはテキストとボタンが欲しい、でウィジェットのバックグランドは黒で塗りつぶして、透明度を50%ぐらいにして。ボタンとテキストは横並びにして、テキストの位置としては左寄り、ボタンは右寄り。ボタンのテキストは「更新」、ボタンの横幅はそれに合わせてくれ、でテキストの横幅は余ったスペースを敷き詰めてほしい」
bing「
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textColor="#FFFFFF"
android:textSize="16sp" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="更新" />
</RelativeLayout>
」
bingの名誉のために行っておくとButtoin.android:textの部分はめんどくさいので私がハードコードするように修正した。
動的な表示
とりえあず静的な画面を表示することに成功したので、次は動的な情報として、時間を表示してみる。
と思って、bingさんに聞いてみたが、どうやらandroidのウィジェットは更新頻度が最短で30分らしい。それ以下の速度で更新したい場合は特殊な方法が必要っぽい。
30分の方法でやりたいところだが、毎回更新されるかどうか試すのに30分松の目現実的ではないので、タップしたら更新されるようにしてみる。
タップした際に更新されるかどうかを確認しやすいように、今回は時間を表示してみる(一般的に時間を表示する場合は後者の方法が望ましいが、今回はあくまで前者の一般的なウィジェットの更新方法の練習のため前者の方法を採用する)
ボタンのクリック
package com.example.trywidget2
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.RemoteViews
import java.util.Calendar
import java.util.Date
class ExampleAppWidgetProvider : AppWidgetProvider() {
companion object {
private const val BUTTON_CLICKED = "buttonClicked"
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("onUpdate","update")
Log.d("onUpdate",appWidgetIds.toString())
// ウィジェットのボタンが押されたときの処理を設定します。
val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
action = BUTTON_CLICKED;
}
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE )
// ウィジェットのレイアウトを取得し、ボタンのクリックイベントを設定します。
val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
setOnClickPendingIntent(R.id.button, pendingIntent)
}
// ウィジェットを更新します。
appWidgetManager.updateAppWidget(appWidgetIds, views)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (BUTTON_CLICKED == intent?.action) {
// ボタンが押されたときの処理を記述します。
Log.d("Receive", "Button clicked!")
}
}
}
これでwidgetにあるボタンが押されたさいに、LogcatでButton clicked!
が表示されるようになった。
ただこれだと後後うまくいかないことが頻出する
ボタンクリックしたwidgetの検知
widgetの更新処理はwidgetProviderのonUpdateに書いているが、外部から明示的にこれを実行するにはwidgetのIdが必要となる。
同じクラスから作れられたwidgetインスタンスのid一覧はonUpdateの第三引数(appWidgetIds
)で取得できるが、onReceiveの段階ではどのidのwidgetから贈られてきたものなのかわからないので、Intentに自分がどういうidをもつwidgetなのかを含める必要がある。
その処理を追加したコードが以下のコードである
class ExampleAppWidgetProvider : AppWidgetProvider() {
companion object {
private const val BUTTON_CLICKED = "buttonClicked"
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("onUpdate","update")
Log.d("onUpdate",appWidgetIds.toString())
// ウィジェットのボタンが押されたときの処理を設定します。
for (appWidgetId in appWidgetIds) {
val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
action = BUTTON_CLICKED;
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
val pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, FLAG_IMMUTABLE )
// ウィジェットのレイアウトを取得し、ボタンのクリックイベントを設定します。
val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
setOnClickPendingIntent(R.id.button, pendingIntent)
}
// ウィジェットを更新します。
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (BUTTON_CLICKED == intent?.action) {
// ボタンが押されたときの処理を記述します。
Log.d("Receive", "Button clicked!")
val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
Log.d("Receive",widgetId.toString())
}
}
}
以上のようにintentをちゃんと設定すると、clickした際にidが出力できると思う。
テキストの更新
今度はwidget上にあるテキストを更新(今の時間を表示する)してみる
以下のようにonReceive
を変え、``
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (BUTTON_CLICKED == intent.action) {
val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
val appWidgetManager = AppWidgetManager.getInstance(context)
val views = RemoteViews(context.packageName, R.layout.example_appwidget)
views.setTextViewText(R.id.text_view, LocalDateTime.now().toString())
appWidgetManager.updateAppWidget(widgetId, views)
}
}
Widgetの設定画面を作る
設定を管理するやつを作る
どうやらWidgetPreferenceというのを使うのが一般的みたい
なのでそれを実装
package com.example.trywidget2
import android.content.Context
import android.content.SharedPreferences
class WidgetPreferences(context: Context) {
private val PREFS_NAME = "com.example.app.widgetprefs"
private val PREF_PREFIX_KEY = "appwidget_"
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0)
// ウィジェットの設定を保存する
fun saveStringPref(appWidgetId: Int, text: String) {
val editor = prefs.edit()
editor.putString(PREF_PREFIX_KEY + "str", text)
editor.apply()
}
// ウィジェットの設定を読み込む
fun loadStringPref(appWidgetId: Int): String? {
return prefs.getString(PREF_PREFIX_KEY +"str", null)
}
}
設定画面の設定
次はwidgetの設定画面を作ってみる
私「widgetの設定画面のつくりかた」
bing「↓こんなかんじやで」
package com.example.trywidget2
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
class InputActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_setting)
val editText = findViewById<EditText>(R.id.editText)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
val input = editText.text.toString()
// ここでinputを保存または使用します。
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="ここにテキストを入力してください" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="送信" />
</LinearLayout>
ただここでエラーが発生。
アクティビティを設置する瞬間一瞬画面が開いた後にクラッシュするようになってしまった。
今までのコードとエラーメッセージをbingに投げると以下の回答がかえってきた
エラーメッセージを見ると、
SettingActivity
がTheme.AppCompat
テーマ(またはその子孫)を使用する必要があるというエラーが発生しているようです¹³。
このエラーは、
AppCompatActivity
を拡張するアクティビティでTheme.AppCompat
テーマ(またはその子孫)を使用しないと発生します¹³。AppCompatActivity
は、Androidのバージョンに関係なく、アクションバーとマテリアルデザインのスタイルを使用するためのクラスです¹³。したがって、AppCompatActivity
を拡張するアクティビティでは、Theme.AppCompat
テーマ(またはその子孫)を使用する必要があります¹³。
あなたの
SettingActivity
はAppCompatActivity
を拡張しているため、Theme.AppCompat
テーマ(またはその子孫)を使用する必要があります¹³。しかし、AndroidManifest.xml
でSettingActivity
に対してandroid:theme="@android:style/Theme.Holo.Light.Dialog"
を設定しているようです¹³。これはTheme.AppCompat
テーマではないため、エラーが発生しています¹³。
この問題を解決するには、
SettingActivity
のテーマをTheme.AppCompat
テーマ(またはその子孫)に変更する必要があります¹³。例えば、AndroidManifest.xml
でSettingActivity
のテーマをTheme.AppCompat.Light.Dialog
に変更すると、エラーが解消されるかもしれません¹³。具体的には、以下のように変更します¹³:
<activity android:name=".SettingActivity" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
<activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog">
</activity>
これで問題が解決するか試してみてください¹³。それでも問題が解決しない場合は、お知らせください¹³。
ソース: Bing との会話 2023/11/8
(1) このアクティビティでは、Theme.AppCompatテーマ(または子孫 .... https://qastack.jp/programming/21814825/you-need-to-use-a-theme-appcompat-theme-or-descendant-with-this-activity.
(2) Android 12でCaused by java.lang.IllegalStateException You .... https://qiita.com/atsuya046/items/b10ab779978e7cafeb93.
(3) スタイルとテーマ | Android デベロッパー | Android Developers. https://developer.android.google.cn/guide/topics/ui/look-and-feel/themes?hl=ja.
bing先生さすが
以下のように修正
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TryWidget2"
tools:targetApi="31">
<activity
android:name=".ExampleAppWidgetConfigure"
android:exported="false"
android:label="@string/title_activity_example_app_widget_configure"
android:theme="@style/Theme.TryWidget2">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.TryWidget2">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
<receiver
android:name=".ExampleAppWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>
</application>
</manifest>
全画面
ただこれだと、activityが小さすぎてみきれているので、全画面表示にする。
基本的にはmanifestファイルで、全画面であることを指定すればいい感じ。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
...
- <activity android:name=".SettingActivity" android:theme="@style/Theme.AppCompat.Light.Dialog" android:exported="true">
+ <activity android:name=".SettingActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
...
</manifest>
設定を保存する
どうやらWidgetPreferenceというのを使うのが一般的みたい
なのでそれを実装
package com.example.trywidget2
import android.content.Context
import android.content.SharedPreferences
class WidgetPreferences(context: Context) {
private val PREFS_NAME = "com.example.app.widgetprefs"
private val PREF_PREFIX_KEY = "appwidget_"
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0)
// ウィジェットの設定を保存する
fun saveStringPref(appWidgetId: Int, text: String) {
val editor = prefs.edit()
editor.putString(PREF_PREFIX_KEY + "str", text)
editor.apply()
}
// ウィジェットの設定を読み込む
fun loadStringPref(appWidgetId: Int): String? {
return prefs.getString(PREF_PREFIX_KEY +"str", null)
}
}
ウィジェットが設定を読むようにする
最後にウィジェットが設定を読むようにする
class ExampleAppWidgetProvider : AppWidgetProvider() {
companion object {
private const val BUTTON_CLICKED = "buttonClicked"
}
+ private fun setText(myId:Int,context: Context, views: RemoteViews) {
+ val pref = WidgetPreferences(context)
+
+ views.setTextViewText(R.id.text_view, LocalDateTime.now().toString()+"_"+pref.loadStringPref(myId))
+ }
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
for (appWidgetId in appWidgetIds) {
val intent = Intent(context, ExampleAppWidgetProvider::class.java).apply {
action = BUTTON_CLICKED
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
val pendingIntent =
PendingIntent.getBroadcast(context, appWidgetId, intent, FLAG_IMMUTABLE)
val views = RemoteViews(context.packageName, R.layout.example_appwidget).apply {
setOnClickPendingIntent(R.id.button, pendingIntent)
}
+ setText(appWidgetId,context,views)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (BUTTON_CLICKED == intent.action) {
val widgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
val appWidgetManager = AppWidgetManager.getInstance(context)
val views = RemoteViews(context.packageName, R.layout.example_appwidget)
+ setText(widgetId,context,views)
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
ちょっとだけ整理もしている。
初期設定設定項目を反映するようにする
このままでは更新ボタンを押すまで初期設定時の項目がウィジェットに反映されないので、設定用Activityで送信ボタンを押したときに更新処理を走らせるようにする。
AppWidgetManager
を使用すると、特定のウィジェットをIDで指定して更新したり削除したり、情報を取得したりすることができるようになる。
これは、全体を管理するオブジェクトに対するアクセス権を持つことを意味していて、AppWidgetManager
のインスタンスを作成すると、グローバルなウィジェットにアクセスできるようになる。
package com.example.trywidget
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.RemoteViews
import androidx.appcompat.app.AppCompatActivity
import java.time.LocalDateTime
class SettingActivity : Activity() {
private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val extras = intent.extras
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
}
setContentView(R.layout.setting_activity)
if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
val editText = findViewById<EditText>(R.id.editText)
val button = findViewById<Button>(R.id.send_button)
button.setOnClickListener {
val prefs = WidgetPreferences(this)
prefs.saveStringPref(mAppWidgetId, editText.text.toString())
+ val appWidgetManager = AppWidgetManager.getInstance(this)
+ updateAppWidget(this, appWidgetManager, mAppWidgetId)
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
Log.d("SettingActivity","set button onclick")
}
+ internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
+ val views = RemoteViews(context.packageName, R.layout.widget)
+ val pref = WidgetPreferences(context)
+ views.setTextViewText(R.id.text_view, LocalDateTime.now().toString()+"_"+pref.loadStringPref(appWidgetId))
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
}
成果物
結局↓のようなものが出来上がりました
以下のようなwidgetで、widgetを設置するとその下の画像のような設定画面が表示されます
感想
自分が思ったより記事をちゃんと読まない(知らない単語を間違った推論で読み進める)というのがよくわかる記録になりました。
後日試行錯誤の箇所を除いた記事にする予定です
Discussion