woshidan's blog

そんなことよりコードにダイブ。

AndroidのWidgetで時計を作ってみる

Androidを立ち上げたらホーム画面にいくつか時計や天気予報などが表示されていると思うのですが、AppWidgetクラスを利用してそういったホーム画面に表示されるウィジェットを作成することができます。

今回は時計もどきを作成してみたのでそのときのメモです。

ウィジェットの作成の仕方

まず、はじめに基本的なウィジェット自体の作成の仕方について簡単にまとめておきます。

  • AppWidgetProviderを継承したクラスを作成
  • 使える要素の制限に注意しながらAppWidget用のレイアウトを作成
  • AppWidgetProviderを継承したクラスをのandroid.appwidget.action.APPWIDGET_UPDATEアクションを受け取るインテントフィルターを持つreceiverとしてAndroidManifestに追加
    • meta-dataタグでAppWidgetの設定ファイルを指定
  • meta-dataタグで指定したAppWidgetの設定ファイルを作成
  • ServiceAppWidgetProviderなどから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分以上の値を入れます。

30分未満の値を入れた場合無視されてしまいます

時計など頻繁に更新が必要なウィジェットを作成したい場合、これは困るので、この周期を利用せず、AlarmManagerを用いて更新するための処理を起動させるようにする方法があります(今回は試しにこちらで書きました)。

ServiceAppWidgetProviderなどからAppWidgetを更新する処理を書く

ServiceAppWidgetProvideronUpdateメソッドから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)で一気にウィジェットの更新を行う感じですね。

つまったこと

  • ウィジェットはアプリをインストールした後に、ウィジェット一覧から起動させてホーム画面に配置する必要があった
  • 秒単位など細かい間隔で更新したい場合はAlarmManagerを通して実装する必要があった

ウィジェットはアプリをインストールした後に、ウィジェット一覧から起動させてホーム画面に配置する必要があった

ウィジェットのクラスを追加したアプリがあるならば、インストールしたら自動的にウィジェットが起動するのでは? と思ったのだけど、そういうことはなく、ウィジェット一覧からホーム画面に置きたいウィジェットを選択して配置する必要があった。

これに気づかなくて1時間以上経過して辛かった。

facebookメッセンジャーが自動的にウィジェットを出していたような気がするのですが、あれはどうするのでしょう...?

もしかしてCかな...?

秒単位など細かい間隔で更新したい場合はAlarmManagerを通して実装する必要があった

上の方の設定ファイルあたりにも書いているのですが、android:updatePeriodMillisの値を通した自動更新は30分より細かい単位では行われません。

http://stackoverflow.com/questions/3310264/frequently-updating-widgets-more-frequently-than-what-updateperiodmillis-allows

なので、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();
    }

実際仕事で使う前には、

などを読んでみるといいのでしょうね。

とりあえず今日はそんな感じです。