Androidのウィジェットでアイテムリストを表示するかんたんな方法

2017/06/09

Gagandeep Singh

10

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Androidアプリのウィジェットでリストを表示する「コレクションウィジェット」の設定方法を解説します。

以前のバージョンのAndroidは、アプリのウィジェットでの表示はTextViewImageViewに限られていました。でも、翌週の予想気温をリストで表示するように、ウィジェットでアイテムのリストを表示できませんでした。 Android 3.0でListView、GridView、StackViewが表示できる「コレクションウィジェット」が導入されました。

Todoアプリ用のウィジェットを作成しましょう。保留中のタスクをリスト表示するためにコレクションウィジェットを使います。この記事がコレクションウィジェットの動作を理解する助けとなれば幸いです。

ウィジェットの基本的な作成方法を知っているものとして進めます。基本がわからない人はオリジナルのコレクションウィジェット作成する準備として、こちらの記事を参考にしてください。

始めに

作成に使うプロジェクトのスターターコードをこちらからダウンロードしてください。

コードには基本のウィジェットが実装されています。このプロジェクトにコレクションウィジェットを作成します。基本のウィジェットでは「保留中のタスクの数」が表示されますが、コレクションウィジェットでは「完全なリスト」を表示します。コレクションウィジェットの作成には、次の2つのコンポーネントを追加します。

  • RemoteViewsFactory
  • RemoteViewsService

上記コンポーネントの動作を説明します。

RemoteViewsFactory

RemoteViewsFactoryはウィジェットのコンテキストでアダプターの役割を果たします。アダプターはListViewやGridViewなどコレクションのアイテムとデータセットを接続するものです。

クラスをプロジェクトに追加します。Javaで新しいクラスを作成してクラス名を「MyWidgetRemoteViewsFactory」とし、RemoteViewsService.RemoteViewsFactoryを実装します。

public class MyWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

    private Context mContext;
    private Cursor mCursor;

    public MyWidgetRemoteViewsFactory(Context applicationContext, Intent intent) {
        mContext = applicationContext;
    }

    @Override
    public void onCreate() {

    }

    @Override
    public void onDataSetChanged() {

        if (mCursor != null) {
            mCursor.close();
        }

        final long identityToken = Binder.clearCallingIdentity();
        Uri uri = Contract.PATH_TODOS_URI;
        mCursor = mContext.getContentResolver().query(uri,
                null,
                null,
                null,
                Contract._ID + " DESC");

        Binder.restoreCallingIdentity(identityToken);

    }

    @Override
    public void onDestroy() {
        if (mCursor != null) {
            mCursor.close();
        }
    }

    @Override
    public int getCount() {
        return mCursor == null ? 0 : mCursor.getCount();
    }

    @Override
    public RemoteViews getViewAt(int position) {
        if (position == AdapterView.INVALID_POSITION ||
                mCursor == null || !mCursor.moveToPosition(position)) {
            return null;
        }

        RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.collection_widget_list_item);
        rv.setTextViewText(R.id.widgetItemTaskNameLabel, mCursor.getString(1));

        return rv;
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public long getItemId(int position) {
        return mCursor.moveToPosition(position) ? mCursor.getLong(0) : position;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

}

上のコードでMyWidgetRemoteViewsFactoryRemoteViewsFactoryクラスのメソッドを次のようにオーバーライドしています。

  • onCreate:ウィジェットを最初に作成したとき呼び出す
  • onDataSetChanged:ウィジェットの更新時に呼び出す
  • getCount:カーソル中のレコード数(ここではウィジェットの表示が必要なタスクアイテム数)を返す
  • getViewAt:処理中の全作業を扱う。RemoteViewsオブジェクト(ここでは1個のリストアイテム)を返す
  • getViewTypeCount:ListView中に含まれるビュータイプの数を返す。ここではListViewのビュータイプはすべて同じなのでreturn 1となる

RemoteViewsService

RemoteViewsServiceの目的は、ウィジェットに適切なデータを格納したRemoteViewsFactoryオブジェクトを返すことです。このクラスはさほど込み入ったものではありません。

RemoteViewsServiceクラスを継承した新しいクラス「MyWidgetRemoteViewsService」を作成します。

public class MyWidgetRemoteViewsService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new MyWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

ほかのAndroidのサービスと同様、サービスをマニフェストファイルに登録します。

<service android:name=".AppWidget.MyWidgetRemoteViewsService"
	android:permission="android.permission.BIND_REMOTEVIEWS"></service>

専用のパーミッション「android.permission.BIND_REMOTEVIEWS」によってシステムと作成中のサービスをひも付けします。各行にウィジェットのビューが作成され、別のアプリはこのウィジェットのデータにアクセスできなくなります。

RemoteViewsServiceを開始

追加のコンポーネントを設定したところで、RemoteViewsServiceを呼び出すWidgetProviderを作成します。

AppWidgetのパッケージに新しいクラス「CollectionAppWidgetProvider」を作成し、次のように記述します。

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        RemoteViews views = new RemoteViews(
                context.getPackageName(),
                R.layout.collection_widget
        );
        Intent intent = new Intent(context, MyWidgetRemoteViewsService.class);
        views.setRemoteAdapter(R.id.widgetListView, intent);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

ウィジェットのレイアウトを作成

res/xmlに新しいリソースファイルを作成し、ファイル名を「collection_widget.xml」とします。

このファイルは、ユーザーエクスペリエンス向上のためのウィジェットに使用したり、プレビュー画像を追加したりするレイアウトファイルを指定するなど、ウィジェットの設定を定義します。

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="864000"
    android:previewImage="@drawable/simple_widget_preview"
    android:initialLayout="@layout/collection_widget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

リソースファイルをもう1つ作成して今度はres/layoutに置き、ファイル名を「collection_widget.xml」とします。

このファイルはコレクションウィジェットのレイアウトを定義します。上位にタイトルを、その下位にタスクのリストを表示するListViewを記述します。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorWhite"
            xmlns:tools="http://schemas.android.com/tools"
            android:orientation="vertical">
    <FrameLayout android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView android:layout_width="match_parent"
            android:id="@+id/widgetTitleLabel"
            android:text="@string/title_collection_widget"
            android:textColor="@color/colorWhite"
            android:background="@color/colorPrimary"
            android:textSize="18dp"
            android:gravity="center"
            android:textAllCaps="true"
            android:layout_height="@dimen/widget_title_min_height"></TextView>
    </FrameLayout>
    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ListView android:id="@+id/widgetListView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorWhite"
            android:dividerHeight="1dp"
            android:divider="#eeeeee"
            tools:listitem="@layout/collection_widget_list_item"></ListView>
    </LinearLayout>
</LinearLayout>

各リストアイテムのレイアウトを定義するためのファイルをres/layoutにもう1つ作成します。ファイル名を「collection_widget_list_item.xml」とします。

<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/widgetItemContainer"
    android:layout_height="wrap_content">

    <TextView android:id="@+id/widgetItemTaskNameLabel"
        android:layout_width="wrap_content"
        android:gravity="start"
        android:layout_weight="1"
        android:textColor="@color/text"
        android:layout_gravity="center_vertical"
        android:layout_height="wrap_content"></TextView>

</LinearLayout>

アプリを起動してください。ウィジェットにTodoアイテムが表示されているのを確認できるはずです(必ずアプリを再インストールして変更を確認してください。Android StudioでInstant Runオプションを無効にできます)。

ウィジェットを手動で更新する

ロジックでは、Todoアイテムを新しく作成するたびにWidgetProviderにブロードキャストを送信しなければなりません。

「CollectionAppWidgetProvider」クラスに新しくメソッドを定義します。

public static void sendRefreshBroadcast(Context context) {
    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    intent.setComponent(new ComponentName(context, CollectionAppWidgetProvider.class));
    context.sendBroadcast(intent);
}

CollectionAppWidgetProviderクラスのonReceiveメソッドをオーバーライドします。

@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, CollectionAppWidgetProvider.class);
        mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.widgetListView);
    }
    super.onReceive(context, intent);
}

新しいTodoタスクの作成時に、CollectionAppWidgetProviderで定義されたsendRefreshBroadcastメソッドを呼び出します。

MainActivityで対応したaddTodoItemメソッドを変更します。

a.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        Toast.makeText(mContext, "New task created", Toast.LENGTH_LONG).show();
        getTodoList();
        // this will send the broadcast to update the appwidget
        CollectionAppWidgetProvider.sendRefreshBroadcast(mContext);
    }
});

ウィジェットでのイベント処理

作成中のウィジェットでは上部にタイトルを、下部にリストビューを記述しています。ユーザーがタイトルをクリックするとアプリを起動して、リストビューのアイテムをクリックするとタスクの詳細を表示します。このTodoアプリでタスクの詳細はさほど役に立たないかもしれませんが、コンセプトを理解するために設定します。

単一のビューでのクリックイベント

TextViewやImageViewといったビューにクリックイベントを追加するのはとても簡単です。onUpdateメソッドのコードを次のように変更します。

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        RemoteViews views = new RemoteViews(
                context.getPackageName(),
                R.layout.collection_widget
        );

        // click event handler for the title, launches the app when the user clicks on title
        Intent titleIntent = new Intent(context, MainActivity.class);
        PendingIntent titlePendingIntent = PendingIntent.getActivity(context, 0, titleIntent, 0);
        views.setOnClickPendingIntent(R.id.widgetTitleLabel, titlePendingIntent);

        Intent intent = new Intent(context, MyWidgetRemoteViewsService.class);
        views.setRemoteAdapter(R.id.widgetListView, intent);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

同様の方法でクリックイベントを追加します。ウィジェットによって動作するコンテキストが異なるので、PendingIntentを使ってクリックイベントを登録します。

ListViewアイテムでクリックイベントを設定する

ListViewアイテムにクリックイベントを追加するのは、ListViewオブジェクトにsetOnItemClickListenerを設定するだけではありません。いくつかのステップが必要です。

まずPendingIntent用にテンプレートを設定します。次のコードをCollectionAppWidgetProviderクラスのonUpdateメソッドのviews.setRemoteAdapter(R.id.widgetListView, intent);のあとに追加します。

// template to handle the click listener for each item
Intent clickIntentTemplate = new Intent(context, DetailsActivity.class);
PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
        .addNextIntentWithParentStack(clickIntentTemplate)
        .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.widgetListView, clickPendingIntentTemplate);

ListViewアイテムでDetailsActivityを起動します。DetailsActivityは「タスクに関する説明」を表示するものです。

RemoteViewsFactoryで新しいRemoteViewsオブジェクトが作成されるたびにテンプレートに書き込みます。

MyWidgetRemoteViewsFactorygetViewAtメソッドに次のコードを追加します。

Intent fillInIntent = new Intent();
fillInIntent.putExtra(CollectionAppWidgetProvider.EXTRA_LABEL, mCursor.getString(1));
rv.setOnClickFillInIntent(R.id.widgetItemContainer, fillInIntent);

これでCollectionAppWidgetProviderで定義されたpending intentsテンプレートはOKです。ちなみにすべての行でのクリックを有効にするために、collection_widget_list_item.xmlのルート要素にクリックリスナーを設定します。

最後に

初心者が直面する課題の解決に役立てば幸いです。

コードはこちらからダウンロードできます。

(原文:Killer Way to Show a List of Items in Android Collection Widget

[翻訳:新岡祐佳子/編集:Livit

Copyright © 2017, Gagandeep Singh All Rights Reserved.

Gagandeep Singh

Gagandeep Singh

5年以上の経験をもつフリーランス開発者です。Androidアプリ開発に熱心で、余暇の大半を新しいアイデアやAndroid技術について考えて過ごしています。現在インドのジャランダルにあるGuru Nanak Dev Universityでコンピューターアプリケーションを学んでいます。

Loading...