2012年3月14日水曜日

ウィジェットホストの作り方

「ウィジェットを扱うアプリ」と言っても2通りの捉え方ができる。
1つは「自作ウィジェットを他人のアプリで表示する」という意味。
もう1つは「他人のウィジェットを自作アプリで表示する」という意味。
本記事では後者、つまりウィジェットホストの機能について記載する。
前者については多くの書籍やサイトで説明されているので、ここでは記載しない。

ssFlickerにウィジェットホストの機能を追加したのはもう半年ほど前のことだが、当時ネットで検索した限り、この機能について書かれたサイトは見当たらなかった。
何とか標準ホームのソースを追って機能は実装できたのだが、同じ悩みを抱える人もいるだろう。

作り始めた当初、ウィジェットホストの機能が実現できる自信はまったくなかった。
ウィジェットホストで必要な処理を想像して、一覧を表示して設定画面を開いてアプリの情報などを色々と保存して…と色々考えていた。
が、終わってみればそこまで難しい物でもない。想像していた処理の半分位をシステム側(Android OS)が行っている為である。

毎度の事だがリファレンスを読み解いた訳ではなく、どちらかと言えば結果論(やってみたら動いた)になっているので、あくまで参考程度にして欲しい。



処理の流れ                                   

まず、処理の流れは以下のようになる。
順を追って説明する。
  1. ウィジェット一覧画面を表示する。
  2. 必要に応じてウィジェット設定画面を表示する。
  3. ウィジェット情報を保存する。
  4. ウィジェットを表示する。
この内、2と3はonActivityResult()の中で行う。


ウィジェット一覧画面の表示                           

ウィジェットを設置するには、まずウィジェット一覧画面を表示する必要がある。
これは簡単。Intentを発行するだけで済む。

AppWidgetHost appWidgetHost = new AppWidgetHost(this, XXXX)    //…(1)
int appWidgetId = appWidgetHost.allocateAppWidgetId();    //…(2)

ArrayList<appwidgetproviderinfo> appWidgetProviderInfoList = new ArrayList<appwidgetproviderinfo>();

ArrayList<bundle> bundleList = new ArrayList<bundle>();
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
  .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
  .putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_INFO, appWidgetProviderInfoList)
  .putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_EXTRAS, bundleList);
   
startActivityForResult(intent, REQUEST_CODE_ADD_APPWIDGET);    //…(3)

(1)AppWidgetHostをインスタンス化する。
これを利用してシステムとのウィジェットに関する情報のやり取りを行う。
コンストラクタの引数に"XXXX"と記載しているが、ここにはhostId(int値)を入れる。
このhostIdが何者かは分からなかったのだけど、おそらく、ウィジェットホスト毎に一意であるべきIDなのだと思われる。
つまり、他のウィジェットホストとは異なる値にする必要があるんだけど、他のウィジェットホストで利用しているIDは分からない。
ssFlickerでは適当な数値を使っている。

(2)でappWidgetIdを取得する。
これはシステムがウィジェット毎にユニークに付与しているID。
複数ホームアプリを利用している人は各ホームアプリでウィジェットを設置しているかも知れないが、実はそれらにはシステムで払い出されたユニークなappWidgetIdが付与されている。

(3)でstartActivityForResult()を実行するとウィジェット一覧が表示される。


ウィジェット設定画面の表示、ウィジェット情報の保存               

ユーザがウィジェット一覧から目的のウィジェットを選択すると、onActivityResult()が動く。
そこで必要に応じてウィジェット設定画面を表示する。
ウィジェット設定画面で設定が完了すると、もう一度onActivityResult()が動くので、そこでアプリ内に情報を保存すれば良い。
そのため、2度onActivityResult()を動かす必要がある。

@Override
public void onActivityResult (int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);

  switch (resultCode) {
  case RESULT_OK:

    switch (requestCode) {
    case REQUEST_CODE_ADD_APPWIDGET:    //…(1)

      int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
      AppWidgetProviderInfo appWidgetProviderInfo = AppWidgetManager.getInstance(this).getAppWidgetInfo(appWidgetId);    //…(2)

      //…(3)
      if (appWidgetProviderInfo.configure != null) {
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
          .setComponent(appWidgetProviderInfo.configure)
          .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        startActivityForResult(intent, REQUEST_CODE_ADD_APPWIDGET_2);

      //…(4)
      } else {
        onActivityResult(REQUEST_CODE_ADD_APPWIDGET_2, RESULT_OK, data);
      }

      break;

    case REQUEST_CODE_ADD_APPWIDGET_2:    //…(5)

      //…(6)
      int appWidgetId2 = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
      AppWidgetProviderInfo appWidgetProviderInfo2 = AppWidgetManager.getInstance(this).getAppWidgetInfo(appWidgetId2);
      String widgetLabel = appWidgetProviderInfo2.label;
      Drawable widgetIcon = getPackageManager().getDrawable(appWidgetProviderInfo2.provider.getPackageName(), appWidgetProviderInfo2.icon, null);
      widgetWidth = appWidgetProviderInfo2.minWidth;
      widgetHeight = appWidgetProviderInfo2.minHeight;

      break;
    }

    break;

  case RESULT_CANCELED:    //…(7)

    switch (requestCode) {
    case REQUEST_CODE_ADD_APPWIDGET:
    case REQUEST_CODE_ADD_APPWIDGET_2:

      if (data != null) {
        int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
        AppWidgetHost appWidgetHost = new AppWidgetHost(this, XXXX);
        if (appWidgetId != -1) appWidgetHost.deleteAppWidgetId(appWidgetId);    //…(8)
      }

      break;
    }

    break;
  }
}

(1)のcase文は1回目のstartActivityForResult()の結果。
ウィジェット一覧画面から目的のウィジェットを選択した後に動く。
(2)でAppWidgetProviderInfoをインスタンス化。
これを利用してウィジェットの様々な情報を取得できる。
(3)でウィジェット設定画面の有無を確認し、必要に応じてIntentを発行してウィジェット設定画面を表示する。
不要な場合は場合はそのまま(4)のように2回目の結果に飛ばす。

(5)のcase文は2回目のstartActivityForResult()の結果。
ウィジェット設定画面で設定が完了した後に動く。
(6)の場所でウィジェットの情報をアプリ内に保存する。
このサンプルコードでは、appWidgetId、widgetLabel、widgetIcon、widgetWidth、widgetHeightを取得しているが保存はしていない。
ここで保存が必須なのはappWidgetIdのみ、他の情報は後からでもシステムから取得できる。
appWidgetIdも後から取得できる気がするのだが、その方法は分からなかった。
また、設置場所(座標)もシステムは保存しないため、必要があれば自作アプリで保存する必要がある。

(7)はRESULT_CANCELEDされた場合の処理。
ウィジェット一覧画面を閉じたり、ウィジェット設定画面を設定せずに閉じた場合に動く。
(8)のようにシステムからappWidgetIdを削除する必要がある。
これを行わないとシステム内に余計な情報が残ってしまう。(残ってしまっても見た目上の問題はないが)


ウィジェットの表示                               

ここまではウィジェット設定の話。
ここからはウィジェットを表示するときの話。
ウィジェットはAppWidgetHostViewとして簡単に取得できるので、それを画面内の好きな位置に配置してあげれば良い。

AppWidgetHost appWidgetHost = new AppWidgetHost(this, XXXX);
appWidgetHost.startListening();    //…(1)

int appWidgetId = appWidget.getAppWidgetId();
AppWidgetProviderInfo appWidgetProviderInfo = AppWidgetManager.getInstance(this).getAppWidgetInfo(appWidgetId);

//…(2)
AppWidgetHostView appWidgetHostView = appWidgetHost.createView(this, appWidgetId, appWidgetProviderInfo);
appWidgetHostView.setLayoutParams(layoutParams);
appWidgetHostView.setAppWidget(appWidgetId, appWidgetProviderInfo);

(1)でリスニングを開始。
これを行わないと、時計などの定期的に更新されるウィジェットが更新されない。

(2)はウィジェットの表示。
ウィジェットの画像はAppWidgetHostViewの形で簡単に取得できるため、あとはアプリのレイアウトにaddView()すれば良い。
ウィジェットに付与されたリスナもこれで有効になる。


注意点                                     

その他注意点など。

アプリからウィジェットを削除する場合は、システムからappWidgetIdを削除する事。
RESULT_CANCELEDと同じ方法で削除できる。
ウィジェットホストがアンインストールされた場合は、そのウィジェットホストが抱えていたウィジェットは全て削除されるので、わざわざアプリ内で削除する必要はない。

標準ホームの場合、ウィジェットは画面を4×4に分割したセル上に配置される。(タブレットだとセル数はより多い)
しかし、自作アプリでどのように表示するかはアプリの仕様次第。
表示位置をセル単位ではなく、ピクセル単位で指定する事も可能。拡大/縮小表示も可能だと思う。
ただし、何にしろアプリ内で座標の計算などは必要になる。
また、Androidでは機種が異なると画面サイズが変わってくるため、ウィジェットのサイズを変更したりする必要がある。
この辺はアプリ内で実装が必要と思われる。

0 件のコメント:

コメントを投稿