androidでwidgetを実装してみた

今年もAdventCalendarの時期が来ました。
全く書くことが浮かばず業務の中でやってみた事をつらつらと書いてみようかと思います。

と言うことでwidgetを実装してみたときの事を書いてみます。

#widgetとは

*Widgetとは、デスクトップ上で特定の機能を実行するための簡易的なアプリケーションの総称である。*

だそうです。
androidではおなじみで、大抵ホーム画面の何もない部分を長押しすると
壁紙/ウィジェット/設定
てな感じで出て来るウィジェットです。
よくあるのは天気予報やカレンダー、SNSの簡易表示などもありますね。
widgetと言っていますが正式にはAppWidgetと言って、APIレベル3から存在するやつです。


#って事でさっそく実装方法
もうAndroid開発者の皆さんはAndroid Studioの移行されているはずなのでその体で進めます。
ていうことで
http://hiko00.blog85.fc2.com/blog-entry-9.html
このページを参考にしていただけると特に考えることも無く実装できちゃいます。


ん？
そのページを見るだけで実装できちゃうの？

え？
はい、実装できちゃうんですｗｗ

てな感じでwidgetの表示まではかんたんに出来てしまいます。
まぁただそれだけでは何も出来ないので、クリックアクションの追加や僕がハマったことを書いてみます。

#widgetの大切なこと
widgetで肝になるものが4つあります。

+ AppWidgetProvider(java)
+ widgetの設定ファイル(xml)
+ widgetのレイアウトファイル(xml)
+ AndroidManifestへのreceiver登録

この4つ実はこれ全部さっきのAndroid Studioでの操作で自動生成されています。
だから絶対必要なものです。

###AppWidgetProvider
ここで実際にwidgetの動作を記述するクラス。
アプリ内からwidgetに何かしたい場合にはstaticで記述する必要があります。
なぜならAppWidgetProviderはBroadcastReceiverを継承しているので、インスタンスの生成が出来ないから。
当然インスタンスを生成しようとすると、怒られてクラッシュします。
でもstaticで実装すること自体は全く問題ないようになっているので大丈夫。
動作を記述する上で使うviewなり何なりはその場で取得できるようになっています。

###widgetの設定ファイル
res/xmlの中に生成されています。
名前はAppWidgetProviderを継承したクラス名の~~キャメルケース~~スネークケース＋_info.xmlとなっている。
widgetの情報がまるっと入っている感じで、縦横の最小サイズ、縦横リサイズの不可否、レビュー画像の指定、ホーム画面/キーボード/またはその両方のどれに表示させるかの指定等があります。

###widgetのレイアウトファイル
どこにでもあるxmlのレイアウトファイル。
ただしviewが使えない。
viewが使えないと言っても皆さんがパッと想像するviewではなくandroid.view.ViewのView。
いわゆるまっさらなview。
そのviewを入れると何もエラーが出ること無く、widgetがこんな感じになります。
![device-2016-12-04-223815.png](https://qiita-image-store.s3.amazonaws.com/0/102066/f24e0f17-b969-ec11-2302-87a9a920e2e7.png)

えっ？って感じ
と言うかどうしてviewが使えないの？
エラーぐらい出して欲しいです。。。
実はこれで結構ハマりましたorz
きれいなスペースを入れるためにLinearLayoutでweightを指定してviewを入れたらスクショの状態になり原因不明
自動生成から一気にレイアウトを作り変えたために何が問題か全く分からず、1個レイアウトを追加してはビルドし、確認しては追加してビルドしとしてを繰り返しました。。。

後から気が付きましたが、うん、リファレンスにちゃんと書いてありますね。
https://developer.android.com/guide/topics/appwidgets/index.html

リファレンに書いてある通り、他にも使えるものに制限があって
layoutだと
FrameLayout,LinearLayout,RelativeLayout,GridLayout
viewだと
AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,
ViewFlipper,ListView,GridView,StackView,AdapterViewFlipper
のみ使えるようになっています。
またこれらを継承した独自クラスも使えないのでご注意ください。

###AndroidManifestへのreceiver登録
まぁこれは当然で上で書いた通りAppWidgetProviderがBroadcastReceiverを継承しているのでそのために必要です。
Javaクラス内で書いても問題なと思いますが、必ず登録する必要があるのでAndroidManifestに書いた方が分かりやすいでしょう。

#クリックアクションを書く
クリック時のアクションはPendingIntentを使います。
実際に書いてみましょう
まずはAppWidgetProvider

```NewAppWidget.java
public class NewAppWidget extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);
        views.setOnClickPendingIntent(R.id.appwidget_text, clickAction(context));

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views); // ※1 ここ大事
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals("click_action")) {
            Toast.makeText(context, "click_action", Toast.LENGTH_SHORT).show();
        }
    }

   private static PendingIntent clickAction(Context context) {
        Intent intent = new Intent("click_action");
        return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }
}
```
ほぼ自動生成のままで、onReceive()とclickAction()を追加しています。
clickAction()はupdateAppWidget()内で呼んでいます。
1つ大事なのが※1としている部分。
viewを更新したり、アクションの書き換えを行ったら必ずAppWidgetManagerクラスのupdateAppWidget()を呼ぶこと。
英語のコメントに書いてある通りupdateAppWidget()を呼ぶことで実際に更新が発生します。
呼ばないと実際に更新処理を書いても書き換わりません。
これを忘れて何度か、なんで更新されなんだろ？ってなりました。

次にAndroidManifest

```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exsample.xxxx.sampleappwidget">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <receiver android:name=".NewAppWidget">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
                <action android:name="click_action"/>  /*ここ*/
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/new_app_widget_info"/>
        </receiver>
    </application>
</manifest>
```

これもほぼ自動生成のまま

```xml
<action android:name="click_action"/>
```

を足しただけ
これでOK！
widgetの真ん中の[EXAMPLE]をタップするとトーストが表示されます。

#アプリ内からwidgetをいじる
今度はアプリ内からwidgetをいじってみましょう。
というかこの為に各メソッドをstaticで実装しています。
widget内で完結するならstaticで書く必要はありません。
先程のNewAppWidget.javaを書き換えます。(書き換えた部分のみ記載)

```NewAppWidget.java
public class NewAppWidget extends AppWidgetProvider {

    private static String mText = "default text"; //　追加

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
        views.setTextViewText(R.id.appwidget_text, mText); // 変更
        views.setOnClickPendingIntent(R.id.appwidget_text, clickAction(context));

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    // 追加
    public static void updateText(Context context, String text) {
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        int[] ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, NewAppWidget.class)); // ※2 
        mText = text;
        for (int appWidgetId : ids) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals("click_action")) {
            updateText(context, "widget text"); //　追加
            Toast.makeText(context, "click_action", Toast.LENGTH_SHORT).show();
        }
    }
}
```

widget内のTextViewの書き換えを行っています。
大切なのが※2の部分

```java
int[] ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, NewAppWidget.class));
```

idsはNewAppWidget.javaが管理しているwidgetのidを配列で持っています。
onUpdate()で引数として渡ってきているappWidgetIdsと同じものが取得でき、
そのidを使ってappWidgetManager.updateAppWidget()を実行することで、表示中のすべてのwidgetの更新を行うことが出来ます。
当たり前ですが、別クラスで管理しているwidgetは取得出来ません。
その別クラス内で同じ様に記述して更新する必要があります。

updateText()をpublic staticで実装したので後は適当に呼び出すだけ。
一応書いておきます。

```MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.app_button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                NewAppWidget.updateText(MainActivity.this, "app text");
            }
        });
    }
}
```

動作としてはビルド直後のwidgetには[default text]と表示されていて、アプリ内のボタンを押すと[app text]に書き換わり、widget内のテキストとタップするとトーストと同時に[widget text]に書き換わります。
もしwidgetが複数あった場合はすべて同時に書き換わります。

因みに複数の別widgetがあった場合でも同じintentのアクションが使えるので、違うレイアウトでも同時にアクションを受け取り表示を切り替えること自体は可能です。

#まとめ
実装開始前はかなりビビっていましたが、やってみると意外とかんたん。
昔からある機能なので情報もそれなりにありますし、Android studioの自動生成が敷居を下げているように感じました。
実際は同じ機能をもったレイアウトが違うwidgetを複数作る要件だったので親クラスを作って継承させようとしたら、abstractでstaticなメソッドが作れない問題にぶち当たったりして、設計で少し悩みましたがいい勉強になりました。