AndroidのAppWidgetに挑戦してみた
最近、個人プロジェクトでやってるAndroidアプリにWidgetを実装してみたので、その手順をメモ。
ここで言うWidgetは、アプリを起動せずともホームアプリ内でよろしくできるインターフェースの事で、
要するに他アプリに埋め込めるミニアプリみたいなものです。
詳しくは、App Widgets | Android Developersを参照してください。
- 今回やりたいこと
- Layoutを定義
- AppWidgetProviderのサブクラスを実装
- onUpdateでWidgetを定義
- onReceiveでWidget or アプリ内からのアクションを受け取る
- AppWidgetProviderInfoを定義
- RemoteViewsServiceのサブクラスからWidgetを描画
- AndroidManifestにAppWidget関連を宣言
- アプリ内での変更をWidgetに反映させる為に
- 最後に
今回やりたいこと
「一週間メモリーズ。」というメモ帳アプリで作成したメモのリストをWidgetに表示してやろうと思います。
イメージとしてはこんな感じ。
Toolbar的なものがあったり、メニューがあったり、リストがあったりな感じです。
では、順を追って実装してみましょう。
↓興味があったら是非DLを(笑)↓ play.google.com
Layoutを定義
まずはWidgetのLayoutを定義していきます。
Widgetでは使用できるLayoutやViewに制限があります。
利用できるLayout Class |
---|
FrameLayout |
LinearLayout |
RelativeLayout |
GridLayout |
利用できるView Class |
---|
AnalogClock |
Button |
Chronometer |
ImageButton |
ImageView |
ProgressBar |
TextView| |
ViewFlipper |
ListView |
GridView |
StackView |
AdapterViewFlipper |
今回のLayoutはこんな感じ↓
<!-- widget_layout.xml --> <?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:background="@color/background" android:orientation="vertical"> <FrameLayout android:id="@+id/header" android:layout_width="match_parent" android:layout_height="48dp" android:background="@color/main_color" android:padding="@dimen/activity_padding"> <TextView android:id="@+id/header_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:gravity="center|start" android:text="@string/app_name" android:textColor="@color/white" android:textSize="@dimen/text_widget_title" /> <FrameLayout android:id="@+id/icon_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:layout_marginEnd="4dp" android:layout_marginStart="4dp" android:clickable="true"> <ImageView android:id="@+id/create_memo_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/create_view" android:src="@drawable/ic_action_editor_mode_edit" /> </FrameLayout> </FrameLayout> <ListView android:id="@+id/memo_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="@color/grey400" android:dividerHeight="0.5dp" /> </LinearLayout>
AppWidgetProviderのサブクラスを実装
Widgetを実装するにあたってAppWidgetProviderを継承したサブクラスを実装していきましょう。
AppWIdgetProviderはBroadcastReceiverを継承しているため、ほぼReceiverと同様の扱いでいけます。
以下のメソッドはAppWidgetProviderで定義されたものです。
onEnabled(Context context)
Widgetが追加されるときに呼ばれる
onUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetIds)
Widget が追加されるとき、またはupdatePeriodMillisで指定されたインターバルに達したときに呼ばれる
onDeleted(Context context, int appWidgetIds)
onDisabled(Context context)
今回はonUpdateでレイアウト定義したものをRemoteViewsとして取り出して扱っていきます。
WidgetのView押下時のイベント等の設定もここでします。
基本的にIntentを飛ばしてやりたいことする感じ。
onUpdateでWidgetを定義
public class OneMemoWidgetProvider extends AppWidgetProvider { private static final String ACTION_ITEM_CLICK = "com.kobaken0029.android_appwidget.ACTION_ITEM_CLICK"; private static final String ACTION_CLICK = "com.kobaken0029.android_appwidget.ACTION_CLICK"; @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { if (appWidgetIds != null && appWidgetIds.length > 0) { for (int appWidgetId : appWidgetIds) { // ウィジェットレイアウトの初期化 RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout); // ヘッダー Intent activityIntent = new Intent(context, NavigationActivity.class); PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0); remoteViews.setOnClickPendingIntent(R.id.header, activityPendingIntent); // 新規作成アイコン Intent clickIntent = new Intent(context, OneMemoWidgetProvider.class); clickIntent.setAction(ACTION_CLICK); PendingIntent createMemoPendingIntent = PendingIntent.getBroadcast( context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT ); remoteViews.setOnClickPendingIntent(R.id.icon_container, createMemoPendingIntent); // リスト Intent widgetServiceIntent = new Intent(context, OneMemoWidgetService.class); remoteViews.setRemoteAdapter(R.id.memo_list, widgetServiceIntent); Intent itemClickIntent = new Intent(context, OneMemoWidgetProvider.class); itemClickIntent.setAction(ACTION_ITEM_CLICK); PendingIntent itemClickPendingIntent = PendingIntent.getBroadcast( context, 0, itemClickIntent, PendingIntent.FLAG_UPDATE_CURRENT ); remoteViews.setPendingIntentTemplate(R.id.memo_list, itemClickPendingIntent); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } } } }
onReceiveでWidget or アプリ内からのアクションを受け取る
onUpdateで怒りの矛先(?)を自身に向けた場合(投げたIntentが自身だったら)、onReceiveで受け取ることができます。
受け取ったIntentからIntent#getActionで処理を分けていきます。
投げるIntentにIntent.FLAG_ACTIVITY_NEW_TASKというフラグをセットする必要があります。
public class OneMemoAppWidgetProvider extends AppWidgetProvider { private static final String ACTION_ITEM_CLICK = "com.kobaken0029.android_appwidget.ACTION_ITEM_CLICK"; private static final String ACTION_CLICK = "com.kobaken0029.android_appwidget.ACTION_CLICK"; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); Intent navigationActivityIntent = new Intent(context, NavigationActivity.class); switch (intent.getAction()) { case ACTION_CLICK: navigationActivityIntent.putExtra(MemoFragment.TAG, true); navigationActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(navigationActivityIntent); break; case ACTION_ITEM_CLICK: navigationActivityIntent.putExtra(Memo.ID, intent.getLongExtra(Memo.ID, 0L)); navigationActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(navigationActivityIntent); break; default: break; } } }
AppWidgetProviderInfoを定義
次にres/xml/
にAppWidgetProviderInfoを定義していきます。
xml形式での記述になります。
<!-- widget_provider.xml --> <?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/widget_layout" android:updatePeriodMillis="86400000" android:minHeight="110dp" android:minWidth="180dp" android:previewImage="@drawable/memomiya" android:resizeMode="horizontal|vertical" />
それぞれの属性の意味は下記の通りです。
android:initialLayout
Widgetの初期状態のレイアウトを指定します。
android:updatePeriodMillis
Widgetの更新タイミングをミリ秒で指定します。
指定時間は1800000(30分)以上でなければなりません。
android:minHeight, android:minWidth
Widgetの最小幅、高さを指定します。
この値はDetermining a size for your widgetで提示されているように以下の値を使用するべきです。
セルの数 | サイズ(dp) |
---|---|
1 | 40dp |
2 | 110dp |
3 | 180dp |
4 | 250dp |
… | … |
n | 70 × n − 30 |
android:previewImage
ユーザがWidgetを選ぶ際に確認できるように画像等を指定します。
未指定の場合はアプリのアイコンが表示されます。
android:resizeMode
Widgetがリサイズ出来るかどうかを指定します。
RemoteViewsServiceのサブクラスからWidgetを描画
Widgetにリストを使用するので、その要素たちを描画するためにServiceを使用したいと思います。
リストのItemごとのLayoutを定義
まず、リストの各Item(row)のLayoutを定義していきます。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="4dp" android:paddingEnd="8dp" android:paddingStart="8dp" android:paddingTop="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/subject_text" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:ellipsize="end" android:maxLines="1" android:paddingEnd="8dp" android:paddingStart="8dp" android:paddingTop="8dp" android:textColor="@color/text" android:textSize="@dimen/text_widget_subject" tools:text="Subject" /> <TextView android:id="@+id/last_update_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="end" android:paddingEnd="12dp" android:paddingStart="12dp" android:paddingTop="8dp" android:textColor="@color/sub_text" android:textSize="@dimen/text_widget_update" tools:text="Subject" /> </LinearLayout> <TextView android:id="@+id/memo_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" android:paddingBottom="8dp" android:paddingEnd="8dp" android:paddingStart="8dp" android:textColor="@color/sub_text" android:textSize="@dimen/text_widget_memo" tools:text="Main Contents" /> </LinearLayout>
RemoteViewsFactoryを実装
次に、RemoteViewsFactoryを実装します。
private class OneMemoWidgetFactory implements RemoteViewsFactory { private MemoHelper memoHelper; private List<Memo> memos; @Override public void onCreate() { memoHelper = new MemoHelperImpl(); // DBからメモを全て取得する memos = memoHelper.findAll(); } @Override public void onDataSetChanged() { // DBからメモを全て取得する memos = memoHelper.findAll(); } @Override public void onDestroy() { } @Override public RemoteViews getViewAt(int position) { if (memos.size() <= 0) { return null; } Memo memo = memos.get(position); RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.row_widget_memo_list); remoteViews.setTextViewText(R.id.subject_text, memo.getSubject()); remoteViews.setTextViewText( R.id.last_update_text, DateUtil.convertToString( DateUtil.MONTH_DAY, DateUtil.convertStringToDate(DateUtil.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, memo.getUpdateAt()) ) ); remoteViews.setTextViewText(R.id.memo_text, memo.getMemo()); Intent intent = new Intent(); intent.putExtra(Memo.ID, memo.getId()); remoteViews.setOnClickFillInIntent(R.id.container, intent); return remoteViews; } @Override public int getCount() { return memos.size(); } @Override public RemoteViews getLoadingView() { return null; } @Override public long getItemId(int position) { return position; } @Override public int getViewTypeCount() { return 1; } @Override public boolean hasStableIds() { return true; } }
onGetViewFactoryでRemoteViewsFactory実装クラスを返却
先ほど作成したRemoteViewsFactoryの実装クラスをonGetViewFactoryでReturnします。
public class OneMemoWidgetService extends RemoteViewsService { @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { return new OneMemoWidgetFactory(); } }
AndroidManifestにAppWidget関連を宣言
AppWidgetに関する記述をAndroidManifestへ。
<receiver android:name=".views.widget.OneMemoWidgetProvider" android:label="@string/app_name"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="com.kobaken0029.android_appwidget.ACTION_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_provider" /> </receiver> <service android:name=".services.OneMemoWidgetService" android:permission="android.permission.BIND_REMOTEVIEWS" />
アプリ内での変更をWidgetに反映させる為に
最後に、アプリ内でメモを新規作成・編集した内容をWidgetへ伝えるために
冒頭部分で定義していたAppWidgetProviderのサブクラスのonReceiveでAppWidgetManager#notifyAppWidgetViewDataChangedを呼びます。
public class OneMemoWidgetProvider extends AppWidgetProvider { public static final String ACTION_UPDATE = "com.kobaken0029.android_appwidget.ACTION_UPDATE"; // 中略 @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); switch (intent.getAction()) { // 中略 case ACTION_UPDATE: // AppWidgetManagerのインスタンスを取得 AppWidgetManager manager = AppWidgetManager.getInstance(context); // 対象Widgetのコンポーネント名を取得 ComponentName myWidget = new ComponentName(context, OneMemoWidgetProvider.class); int[] appWidgetIds = manager.getAppWidgetIds(myWidget); // Managerにデータ変更の通知をする manager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.memo_list); break; default: break; } } }
そしてActivity or Fragment等からIntent経由でProviderを呼び出せばOK。
Intent intent = new Intent(getApplicationContext(), OneMemoWidgetProvider.class); intent.setAction(OneMemoWidgetProvider.ACTION_UPDATE); try { PendingIntent.getBroadcast(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT).} catch (PendingIntent.CanceledException e) { e.printStackTrace(); }
最後に
以上、AndroidでWidgetを作る方法でした。
詳しいことは公式リファレンスをご覧になることをおすすめします(投げやり)
それでは、楽しい楽しいAndroid Lifeをノシ