今回の投稿では、ListViewを利用したコレクションのウィジェットの実装方法について書いていきます。
ウィジェットに対する投稿は比較的少なかったため、この投稿で一通りの説明をしてみたいと思います。
この投稿での完成イメージ
以下のスクリーンショットで示すように、ウィジェットの設定をすると設定アクティビティで設定を行なってからウィジェットを出すような実装となります。
完成したものの挙動確認
これから説明する内容は、既にGooglePlayにて公開されているアプリで実装されているものについての解説となります。
実際の動作を確認したい場合には、以下のアプリで実装されていますので確認してみてください。
記録が残るToDoリスト 〜ウィジェット機能付きで無料でシンプルな操作のやること管理ツール〜
では解説を始めていきたいと思います。
1.AppWidgetProviderInfoメタデータを設定する
res/xmlフォルダを作成して配下にtodo_appwidget_info.xmlというファイルを作成します。
このメタデータの設定によって、設定アクティビティを利用するかどうか、ウィジェットのレイアウトや設定が決められます。
今回は以下のように設定します。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="180dp"
android:minResizeWidth="110dp"
android:minHeight="110dp"
android:minResizeHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/ic_widget_icon"
android:initialLayout="@layout/todo_appwidget"
android:configure="com.highcom.todolog.widget.ToDoAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
各要素について解説します。
- minWidth、minResizeWidth、minHeight、minResizeHeight
これは、ウィジェットを配置時にminWidth、minHeightのサイズで配置されます。minResizeWidhth、minResizeHeightはサイズ変更できる最小サイズの指定となるので、配置時の初期サイズよりも小さくできるように設定する場合には、この要素を設定します。
サイズの指定については以下のようになっています。
セルの数 | 使用可能なサイズ(dp) |
---|---|
1 | 40 dp |
2 | 110 dp |
3 | 180 dp |
4 | 250 dp |
... | ... |
n | 70 x n - 30 dp |
- updatePeriodMillis
これは、画面に配置された際の自動更新頻度をミリ秒で設定します。
電力の消費の関係からできるだけ低い頻度の更新となるような設定が推奨されています。
- previewImage
これはウィジェットを配置する際のイメージ画像を設定します。
以下のようにウィジェットを選択する際のイメージ画像になります。
- initial_layout
配置されるウィジェットのレイアウトを定義したxmlファイルを指定します。
このレイアウトファイルについては次の項目で説明します。
- configure
設定アクティビティを利用する場合には、この要素を設定します。
冒頭のイメージ画像のようにタスクリスト選択のアクティビティを挟むように今回は設定しています。
この設定アクティビティについては次の項目で説明します。
- resizeMode
配置したウィジェットのサイズ変更ができる方向を指定します。縦と横の両方を変更したい場合にはこのように設定します。
- widgetCategory
ウィジェットが配置できる場所を指定します。ロック画面とホーム画面が選択できるようですが、最新のバージョンではホーム画面の設定のみが有効になっているようです。
2.設定アクティビティの実装をする
メタデータの要素でandroid:configureで定義した設定アクティビティについて実装します。
2.1.AndroidManifest.xmlに設定アクティビティを定義
以下のように設定アクティビティであることを宣言します。
<activity android:name=".widget.ToDoAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
2.2.設定アクティビティのレイアウトを定義
設定アクティビティは、AndroidManifest.xmlの設定意外は普通のアクティビティと同じように設定できます。
なので、res/layoutの配下にレイアウトファイルを定義します。
タスクリストを選択する画面なので、以下のようにListViewを配置したレイアウトとします。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".widget.ToDoAppWidgetConfigure">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/app_widget_list_view"
tools:ignore="MissingConstraints">
</ListView>
</androidx.constraintlayout.widget.ConstraintLayout>
2.3.設定アクティビティの実装
以下のように通常のアクティビティのようにAppCompatActivityを継承した実装をします。
ViewModelProviderを利用してデータベースからデータを取得して、カスタムしたアダプタに設定する実装となっていますが、以降で要点について説明します。
public class ToDoAppWidgetConfigure extends AppCompatActivity {
public static final String SELECT_WIDGET_GROUP_ID = "selectWidgetGroupId";
public static final String SELECT_WIDGET_GROUP_NAME = "selectWidgetGroupName";
private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_to_do_app_widget_configure);
setTitle(getString(R.string.group_select));
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
ListView listView = findViewById(R.id.app_widget_list_view);
GroupViewModel groupViewModel = new ViewModelProvider(this).get(GroupViewModel.class);
groupViewModel.getGroupList().observe(this, groupList -> {
ArrayList<DrawerListItem> drawerListItem = new ArrayList<>();
for (Group group : groupList) {
drawerListItem.add(new DrawerListItem(group.getGroupName()));
}
DrawerListAdapter adapter = new DrawerListAdapter(this, R.layout.row_drawerlist, drawerListItem);
listView.setAdapter(adapter);
listView.setOnItemClickListener((adapterView, view, i, l) -> {
long selectGroupId = groupList.get(i).getGroupId();
String selectGroupName = groupList.get(i).getGroupName();
saveSelectWidgetGroupPref(getApplicationContext(), mAppWidgetId, selectGroupId, selectGroupName);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.todo_appwidget);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
appWidgetManager.updateAppWidget(mAppWidgetId, views);
ToDoAppWidgetProvider.updateAppWidget(this.getApplicationContext(), appWidgetManager, mAppWidgetId);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
});
});
}
static void saveSelectWidgetGroupPref(Context context, int appWidgetId, long selectGroupId, String selectGroupName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putLong(SELECT_WIDGET_GROUP_ID + appWidgetId, selectGroupId).apply();
prefs.edit().putString(SELECT_WIDGET_GROUP_NAME + appWidgetId, selectGroupName).apply();
}
static long loadSelectWidgetGroupIdPref(Context context, int appWidgetId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long groupId = prefs.getLong(SELECT_WIDGET_GROUP_ID + appWidgetId, -1);
return groupId;
}
static String loadSelectWidgetGroupNamePref(Context context, int appWidgetId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String groupName = prefs.getString(SELECT_WIDGET_GROUP_NAME + appWidgetId, "");
return groupName;
}
static void deleteSelectWidgetGroupPref(Context context, int appWidgetId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().remove(SELECT_WIDGET_GROUP_ID + appWidgetId).apply();
prefs.edit().remove(SELECT_WIDGET_GROUP_NAME + appWidgetId).apply();
}
}
onCreateメソッドについて
このメソッドで注目しておきたいのが以下の部分。
saveSelectWidgetGroupPref(getApplicationContext(), mAppWidgetId, selectGroupId, selectGroupName);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.todo_appwidget);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
appWidgetManager.updateAppWidget(mAppWidgetId, views);
ToDoAppWidgetProvider.updateAppWidget(this.getApplicationContext(), appWidgetManager, mAppWidgetId);
saveSelectWidgetGroupPrefを呼び出して、設定アクティビティで選択したデータを保存した後に、ウィジェットであるToDoAppWidgetProviderクラスのupdateAppWidgetメソッドを呼び出しています。
これは、画面にウィジェットを配置すると、ウィジェットであるToDoAppWidgetProviderのonUpdateが先に呼び出されてから、設定アクティビティのタスクリスト選択となるため、選択した情報を改めて渡す必要があるため、ここでウィジェットをアップデートしています。
saveSelectWidgetGroupPrefメソッドについて
選択したデータをSharedPreferenceに保存しています。
ウィジェットには、保存せずともIntentでデータを渡す事はできますが、再起動した時などは保存したデータから読み出す必要があるためです。
loadSelectWidgetGroupIdPref, loadSelectWidgetGroupNamePrefメソッドについて
ウィジェットが配置された時にウィジェットから利用されることを想定したメソッドです。ウィジェット側ではSharedPreferenceで保存されたデータを利用して設定をおこないます。
deleteSelectWidgetGroupPrefメソッドについて
ウィジェットが削除される時にウィジェットから利用されることを想定したメソッドです。Key値にウィジェットIDを入れているため、ウィジェットが削除された場合、そのウィジェットに対応するIDのデータも削除しておく必要があるためです。
3.コレクションのウィジェットを実装する
コレクションのウィジェットを実装する場合、以下の3つのクラスを継承して実装を行う必要があります。
- AppWidgetProvider
ウィジェットに対するブロードキャストを処理するクラスであり、RemoteViewsServiceを呼び出してコレクションのウィジェットを生成します。
- RemoteViewsService
コレクションウィジェットを生成するためのRemoteViewsFactoryを呼び出すためのサービスクラスです。
- RemoteViewsFactory
コレクションのウィジェットを生成する場合の本体となるクラス。ListViewを利用する場合のAdapterクラスと同じ位置付けのもの。
これらについて順番に説明していきます。
3.1.AndroidManifest.xmlにウィジェットの定義を追加
以下のように、AppWidgetProviderとRemoteViewsServiceを継承したクラスの定義を追加します。
なお、RemoteViewsFactoryを継承したクラスについては、RemoteViewsServiceのインナークラスになるため定義は不要です。
<receiver android:name=".widget.ToDoAppWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/todo_appwidget_info" />
</receiver>
<service android:name=".widget.ToDoWidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"></service>
ToDoAppWidgetProviderのメタデータの要素で、1.で定義したtodo_appwidget_info.xmlを利用することを宣言しています。
3.2.ウィジェットのレイアウトファイルを定義する
ウィジェットのレイアウトとそのアイテムの定義を以下のように定義します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/through_white"
android:padding="@dimen/widget_margin"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/widget_title_min_height"
android:id="@+id/todo_widget_title_view"
android:text="@string/widget_title"
android:textColor="@color/white"
android:background="@color/deepgreen"
android:textSize="14dp"
android:gravity="center"
android:textAllCaps="true"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ListView
android:id="@+id/todo_widget_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/offwhite"
android:dividerHeight="1dp"
android:divider="@android:color/darker_gray"
tools:listitem="@layout/todo_widget_list_item" />
</LinearLayout>
</LinearLayout>
<?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:paddingLeft="@dimen/widget_listview_padding_x"
android:paddingRight="@dimen/widget_listview_padding_x"
android:paddingStart="@dimen/widget_listview_padding_x"
android:paddingEnd="@dimen/widget_listview_padding_x"
android:minHeight="@dimen/widget_listview_item_height"
android:weightSum="2"
android:id="@+id/widget_item_container"
android:layout_height="wrap_content">
<TextView
android:id="@+id/widget_todo_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/black"
android:textSize="14dp"
android:layout_gravity="center_vertical"/>
</LinearLayout>
これらの定義は、通常のListViewの定義と同様です。
3.3.ToDoAppWigetProviderを実装する
以下のような実装になり、各メソッドの要点について説明します。
public class ToDoAppWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;
// Perform this loop procedure for each App Widget that belongs to this provider
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
String selectGroupName = ToDoAppWidgetConfigure.loadSelectWidgetGroupNamePref(context, appWidgetId);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.todo_appwidget);
Intent titleIntent = new Intent(context, ToDoMainActivity.class);
titleIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
views.setTextViewText(R.id.todo_widget_title_view, selectGroupName);
PendingIntent titlePendingIntent = PendingIntent.getActivity(context, 0, titleIntent, 0);
// タイトルを押下した時のアクションを定義する
views.setOnClickPendingIntent(R.id.todo_widget_title_view, titlePendingIntent);
Intent listIntent = new Intent(context, ToDoWidgetRemoteViewsService.class);
listIntent.setData(Uri.fromParts("content", Integer.toString(appWidgetId), null));
views.setRemoteAdapter(R.id.todo_widget_list_view, listIntent);
// リストを選択した時のアクションを定義する
Intent clickIntentTemplate = new Intent(context, ToDoMainActivity.class);
clickIntentTemplate.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.todo_widget_list_view, clickPendingIntentTemplate);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
public static void sendRefreshBroadcast(Context context) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.setComponent(new ComponentName(context, ToDoAppWidgetProvider.class));
context.sendBroadcast(intent);
}
@Override
public void onReceive(final Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
// refresh all your widgets
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
ComponentName cn = new ComponentName(context, ToDoAppWidgetProvider.class);
mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.todo_widget_list_view);
}
super.onReceive(context, intent);
}
}
onUpdateメソッドについて
ウィジェットを配置時には、appWidgetIdsには、配置しようとしているウィジェットのIDが1つだけ入ります。複数のウィジェットを配置した状態で、再起動した場合などにappWidgetIdsは複数入ってきます。ウィジェットを画面に配置した時点でこのメソッドが呼び出されるため、設定アクティビティで選択したデータはこのメソッドが呼び出されるタイミングでは決まっていない状態となります。
updateAppWidgetメソッドについて
このメソッドは、onUpdateからと設定アクティビティで選択した際に明示的に呼び出されています。
設定アクティビティから明示的に呼び出しているのは、選択したデータをSharedPreferenceに保存してから改めてウィジェットを更新するためです。
onUpdateの呼び出しは、再起動時に保存されたSharedPreferenceからデータを読み出して呼び出されます。
また、注目すべき実装ポイントは以下。
Intent listIntent = new Intent(context, ToDoWidgetRemoteViewsService.class);
listIntent.setData(Uri.fromParts("content", Integer.toString(appWidgetId), null));
views.setRemoteAdapter(R.id.todo_widget_list_view, listIntent);
// リストを選択した時のアクションを定義する
Intent clickIntentTemplate = new Intent(context, ToDoMainActivity.class);
clickIntentTemplate.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.todo_widget_list_view, clickPendingIntentTemplate);
ToDoWidgetRemoteViewsServiceクラスを利用してコレクションのウィジェットを生成しています。
ウィジェットID毎に処理を行いたいためToDoWidgetRemoteViewsFactoryにウィジェットIDをUriを使って渡しています。
なお、putExtraを利用すると、うまくFactoryが生成されないので注意して下さい。
また、コレクションが選択された際にアプリのメイン画面を起動するように実装をしています。addFlagsをしているのは、アプリのメイン画面が多重で起動されることを防ぐために設定しています。
sendRefreshBroadcastメソッドについて
外側のアプリからウィジェットに対してイベントを発行するためのメソッドです。
onReceiveメソッドについて
ウィジェット自身がイベントを受信する事ができるようにするためのメソッドです。
3.4.ToDoAppWidgetServiceを実装する
このクラスは、RemoteViewsFactoryを生成するためのonGetViewsFactoryメソッドをオーバーライドするだけです。
public class ToDoWidgetRemoteViewsService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ToDoWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
3.5.ToDoWidgetRemoteViewsFactoryを実装する
コレクションのウィジェットを実装する上でのメインとなるクラスです。
実装は以下のようになっており、要点を説明していきます。
public class ToDoWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context mContext;
private int mAppWidgetId;
private List<ToDoAndLog> mTodoAndLogList;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExtractor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public ToDoWidgetRemoteViewsFactory(Context applicationContext, Intent intent) {
mContext = applicationContext;
if (intent.getData() != null) {
mAppWidgetId = Integer.parseInt(intent.getData().getSchemeSpecificPart());
}
}
@Override
public void onCreate() {
}
@Override
public void onDataSetChanged() {
final long identityToken = Binder.clearCallingIdentity();
long selectGroupId = ToDoAppWidgetConfigure.loadSelectWidgetGroupIdPref(mContext, mAppWidgetId);
List<Future<?>> futureList = new ArrayList<>();
// ワーカースレッドで実行する。
Future<?> future = databaseWriteExtractor.submit(() -> {
mTodoAndLogList = ToDoLogRepository.getInstance(mContext).getTodoListOnlyToDoByTaskGroupSync(selectGroupId);
});
futureList .add(future);
// ワーカースレッドその処理完了を待つ
for (Future<?> f : futureList) {
try {
f.get();
} catch (Exception e) {
e.printStackTrace();
}
}
futureList.clear();
Binder.restoreCallingIdentity(identityToken);
}
@Override
public void onDestroy() {
ToDoAppWidgetConfigure.deleteSelectWidgetGroupPref(mContext, mAppWidgetId);
}
@Override
public int getCount() {
return mTodoAndLogList.size();
}
@Override
public RemoteViews getViewAt(int i) {
if (i == AdapterView.INVALID_POSITION) {
return null;
}
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.todo_widget_list_item);
rv.setTextViewText(R.id.widget_todo_contents, mTodoAndLogList.get(i).toDo.getContents());
Intent fillInIntent = new Intent();
fillInIntent.putExtra("TASK_TEXT", mTodoAndLogList.get(i).toDo.getContents());
rv.setOnClickFillInIntent(R.id.widget_item_container, fillInIntent);
return rv;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public boolean hasStableIds() {
return false;
}
}
ToDoWidgetRemoteViewsFactoryコンストラクタについて
ToDoAppWidgetProviderからUriで渡されたデータであるウィジェットIDをここで取得しています。
onDataSetChangedメソッドについて
データに変更がある毎に呼び出されるメソッドです。なので、ここで基本的にはデータを読み出す処理を実装します。
onDestroyメソッドについて
ウィジェットが削除される場合に呼びださます。なので、ここでの実装は対応するウィジェットIDのSharedPreferenceのデータを削除するための処理を実装しています。
getCountメソッドについて
コレクションであるListViewに表示する数を返却するように実装しています。
getViewAtメソッドについて
各セル単位で呼び出されるので、引数で渡されている要素の順番を利用してデータを設定していきます。
まとめ
以上の説明で、ListViewを利用したコレクションのウィジェットの実装ができました。
ウィジェットが関連するコードについては一通り掲載しましたが、説明についてはウィジェットに関連する部分について中心に説明していきました。
そのため、説明が不足している部分などあるかと思いますが、分かりづらい点がありましたら是非ご意見下さい。