AndroidのWidgetで時計を作ってみる
Androidを立ち上げたらホーム画面にいくつか時計や天気予報などが表示されていると思うのですが、AppWidget
クラスを利用してそういったホーム画面に表示されるウィジェットを作成することができます。
今回は時計もどきを作成してみたのでそのときのメモです。
ウィジェットの作成の仕方
まず、はじめに基本的なウィジェット自体の作成の仕方について簡単にまとめておきます。
AppWidgetProvider
を継承したクラスを作成- 使える要素の制限に注意しながら
AppWidget
用のレイアウトを作成 AppWidgetProvider
を継承したクラスをのandroid.appwidget.action.APPWIDGET_UPDATE
アクションを受け取るインテントフィルターを持つreceiverとしてAndroidManifestに追加meta-data
タグでAppWidget
の設定ファイルを指定
meta-data
タグで指定したAppWidget
の設定ファイルを作成Service
やAppWidgetProvider
などからAppWidget
を更新する処理を書く
それぞれについてもう少し詳しく書いていきます。
AppWidgetProvider
を継承したクラスを作成
ウィジェットはAppWidgetProvider
を通して作成、更新されます。
AppWidget
の設定ファイルでウィジェットの更新処理を行う周期などが設定できるのですが、その更新周期であったり、作成時に呼び出されるメソッドをオーバーライドしておきます。
package com.example.woshidan.mywidget; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.util.Log; /** * Created by woshidan on 2016/07/02. */ public class MyAppWidget extends AppWidgetProvider { /* コメント引用: * Androidプログラミングレシピ増補改訂版 * メディア/データ/システム/ライブラリ/NDK編 * * 下記のメソッドは、このプロバイダによって作成されたウィジェットを更新するために、 * 通常は次の2つのケースで呼び出される: * 1. 最初にウィジェットが作成されたとき * 2. AppWidgetProviderInfoで定義されたupdatePeriodMillisの周期で */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // 動作確認用のログ Log.d("MyAppWidget", "onUpdate"); // バックグラウンドサービスを始動してウィジェットを更新する context.startService(new Intent(context, CurrentTimeService.class)); } ... }
オーバーライドしたメソッドの内容としては、更新周期が来たらウィジェットを更新するための処理を行うServiceを起動する、というものです。
今回は深く考えずサービスを起動していますが、正直今回程度の内容であれば、更新処理の内容をonUpdateメソッドの中に書いても良いかと思います。
使える要素の制限に注意しながらAppWidget
用のレイアウトを作成
AppWidget
のレイアウトには、通常のレイアウトと比べて利用出来るクラスに制限があります(詳しくはこちら)。
また、ホーム画面にはセルという配置の単位があって、ウィジェットの大きさによってホーム画面で占めるセルの数が決まるので、一応これらに気をつけながらレイアウトを作成します。
要するに単純で小さいレイアウトにしませうという感じです。
<!-- 参考: Androidプログラミングレシピ増補改訂版 メディア/データ/システム/ライブラリ/NDK編 --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/widget_background" android:padding="10dp"> <LinearLayout android:id="@+id/container" android:layout_height="wrap_content" android:layout_width="0dp" android:layout_weight="1" android:layout_gravity="center_vertical" android:orientation="vertical" > <TextView android:id="@+id/text_title" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center_horizontal" android:textAppearance="?android:attr/textAppearanceMedium" android:text="Random Number" /> <TextView android:id="@+id/text_current_time" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center_horizontal" android:textAppearance="?android:attr/textAppearanceMedium" android:textStyle="bold" /> </LinearLayout> <ImageButton android:id="@+id/button_refresh" android:layout_weight="0" android:layout_width="55dp" android:layout_height="55dp" android:layout_gravity="center_vertical" android:background="@null" android:src="@android:drawable/ic_menu_rotate" /> </LinearLayout>
AppWidgetProvider
を継承したクラスをのandroid.appwidget.action.APPWIDGET_UPDATE
アクションを受け取るインテントフィルターを持つreceiverとしてAndroidManifestに追加
下記のようなレシーバーをAndroidManifestに追加します。
<application ...> <receiver android:name=".MyAppWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <!-- 次のデータは、このAppWidgetの設定に必要 --> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_appwidget" /> </receiver> </application>
このとき、meta-data
タグでウィジェットの更新周期やリサイズ設定などを記述するAppWidget
の設定ファイルを指定します。
meta-data
タグで指定したAppWidget
の設定ファイルを作成
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="180dp" android:minHeight="40dp" android:updatePeriodMillis="1800000" android:initialLayout="@layout/my_widget_layout" android:previewImage="?android:drawable" android:widgetCategory="home_screen" > <!-- android:updatePeriodMillisの値は30分以上でないと無視される--> </appwidget-provider>
minWidth
, minHeight
でホーム画面上で配置される際の最低サイズを決めます。
天気やニュースなどの場合、ウィジェットを定期的に更新したい場合は、android:updatePeriodMillis
に30分以上の値を入れます。
時計など頻繁に更新が必要なウィジェットを作成したい場合、これは困るので、この周期を利用せず、AlarmManager
を用いて更新するための処理を起動させるようにする方法があります(今回は試しにこちらで書きました)。
Service
やAppWidgetProvider
などからAppWidget
を更新する処理を書く
Service
やAppWidgetProvider
のonUpdate
メソッドからAppWidget
を更新する処理を書きます。
この更新処理は
- パッケージ名とレイアウトIDから
RemoteViews
を取得 RemoteViews.setTextViewText(int resourceId, String value)
やRemoteViews.setOnClickPendingIntent((int resourceId, PendingIntent intent)
などを通して更新したい予定のウィジェットと更新したい値を設定していくAppWidgetManager
を取得AppWidgetProvider
のクラス名から更新したいウィジェットのComponentName
を取得AppWidgetManager.updateAppWidget(ComponentName widget, RemoteViews views)
でウィジェットの更新を行う
という流れになります。
// AppWidgetのビューを作成する RemoteViews views = new RemoteViews(getPackageName(), R.layout.my_widget_layout); // TextViewの更新をセットする views.setTextViewText(R.id.text_current_time, "current time"); // PendingIntentを利用してクリックイベントを設定する // Serviceの起動やActivityの起動など. PendingIntent appIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(this, MainActivity.class), 0); views.setOnClickPendingIntent(R.id.container, appIntent); // AppWidgetManagerでウィジェットを更新する AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); ComponentName widget = new ComponentName(getApplicationContext(), MyAppWidget.class); manager.updateAppWidget(widget, views);
通常のTextViewなどはほとんどその場で更新処理を行いますが、RemoteViews
上のTextViewなどについては更新を予約するメソッドを呼んでおいて、AppWidgetManager.updateAppWidget(ComponentName widget, RemoteViews views)
で一気にウィジェットの更新を行う感じですね。
つまったこと
ウィジェットはアプリをインストールした後に、ウィジェット一覧から起動させてホーム画面に配置する必要があった
ウィジェットのクラスを追加したアプリがあるならば、インストールしたら自動的にウィジェットが起動するのでは? と思ったのだけど、そういうことはなく、ウィジェット一覧からホーム画面に置きたいウィジェットを選択して配置する必要があった。
これに気づかなくて1時間以上経過して辛かった。
facebookのメッセンジャーが自動的にウィジェットを出していたような気がするのですが、あれはどうするのでしょう...?
もしかしてCかな...?
秒単位など細かい間隔で更新したい場合はAlarmManagerを通して実装する必要があった
上の方の設定ファイルあたりにも書いているのですが、android:updatePeriodMillis
の値を通した自動更新は30分より細かい単位では行われません。
なので、AlarmManager
を通して下記のように更新用のService
を通して定期的に更新するようにしてみたのですが、PendingIntent
の取り扱いやAlarmManager
の利用による電力消費的な配慮などが怪しくてもう少し勉強が必要なつらみがありました。
// Activity private PendingIntent mAlarmIntent; private static final String PREF_WORKING_ALARM = "MainActivity.WORKING_ALARM"; private boolean workingAlarm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 現在時刻用の値を更新するサービスを起動するレシーバのインテントを作成 Intent launchIntent = new Intent(this, AlarmReceiver.class); // 上記を使ってアラームを起動するインテントを作成 mAlarmIntent = PendingIntent.getBroadcast(this, 0, launchIntent, 0); } // 設定用切り替えボタンのクリックイベント public void onToggleButtonClick(View v) { AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); long interval = 1 * 1000; // 1秒 workingAlarm = getPreferences(0).getBoolean(PREF_WORKING_ALARM, false); if (workingAlarm) { manager.cancel(mAlarmIntent); } else { manager.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + interval, interval, mAlarmIntent); } workingAlarm = !workingAlarm; SharedPreferences.Editor editor = getPreferences(0).edit(); editor.putBoolean(PREF_WORKING_ALARM, workingAlarm); editor.apply(); }
実際仕事で使う前には、
- https://developer.android.com/training/monitoring-device-state/index.html
- http://brightechno.com/blog/archives/361
- http://d.hatena.ne.jp/TAKAOMAYA/20090803/1249303524
などを読んでみるといいのでしょうね。
とりあえず今日はそんな感じです。