Collection Widget实现举例

  App Widget可以使用RemoteViewsService来展示集合数据,下面在Android App Widget实现举例的基础上,创建一个电影海报列表的Widget,效果如图1所示。

图1

1. 创建布局

  App Widget可用的Collection View有一下几种:

  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

  这里使用StackView,创建布局widget_stack.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/widget_stack_container"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/widget_margin" >

    <StackView
        android:id="@+id/widget_stack"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:listitem="@layout/widget_stack_item" >
    </StackView>

    <TextView
        android:id="@+id/widget_empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:fontFamily="sans-serif-condensed"
        android:textAppearance="?android:textAppearanceLarge"
        android:text="@string/empty_list"/>

</FrameLayout>

【完整代码】

  使用StackView展示一组海报,这里的widget_empty用于在没有数据时显示提示信息。海报布局widget_stack_item.xml如下,只有一个ImageView用于显示海报:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/widget_stack_item"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/widget_margin" >
        
    <ImageView
        android:id="@+id/widget_movie_poster"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:adjustViewBounds="true"
        android:src="@drawable/placeholder_poster_white"
        tools:src="@drawable/placeholder_poster" />

</FrameLayout>

【完整代码】

2. 创建AppWidgetProviderInfo

  和Android App Widget实现举例的流程类似,在/res/xml/下创建stack_widget_info.xml如这里所示。

3. 创建AppWidgetProvider

  创建StackWidgetProvider继承AppWidgetProvider,在onUpdate() 中,为RemotesViews设置RemoteAdapter:

public class StackWidgetProvider extends AppWidgetProvider {

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

            Intent intent = new Intent(context, StackWidgetRemoteViewsService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            views.setRemoteAdapter(R.id.widget_stack, intent);

            Intent clickIntentTemplate =  new Intent(context, MovieActivity.class);
            PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context)
                    .addNextIntentWithParentStack(clickIntentTemplate)
                    .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
            views.setPendingIntentTemplate(R.id.widget_stack, clickPendingIntentTemplate);

            views.setEmptyView(R.id.widget_stack, R.id.widget_empty);

            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

【完整代码】

这里的intent 包含了下面要介绍的RemoteViewsService,即StackWidgetRemoteViewsService ,通过setRemoteAdapter() 配置到RemoteViews。setEmptyView() 数据为空时显示的元素,即R.id.widget_empty 。clickIntentTemplate 用于配置Collection Widget中每一个元素的点击事件,点击将会打开显示对应电影海报的MovieActivity,但在这里还不知道是那个电影被点击了,这里只是配置了一个Intent Template,详细信息在创建RemoteViews时追加。

4. 创建RemoteViewsService

  Remote Adapter通过RemoteViewsService来请求RemoteViews。RemoteViewsService通过onGetViewFactory() 返回一个RemoteViewsFactory,它类似一个ListView的Adapter:

public class StackWidgetRemoteViewsService extends RemoteViewsService {
    private static final String LOG_TAG = StackWidgetRemoteViewsService.class.getSimpleName();

    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new RemoteViewsFactory() {
            private List<Movie> mMovies;

            @Override
            public void onCreate() { }

            @Override
            public void onDataSetChanged() {
                MovieService movieService = App.getRestClient().getMovieService();
                Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC, 1);

                try {
                    MovieResponse response = call.execute().body();
                    List<Movie> movies = response.getMovies();
                    mMovies = movies;
                } catch (IOException e) {
                    Log.e(LOG_TAG, "ERROR: ", e);
                }
            }

            @Override
            public void onDestroy() { }

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

            @Override
            public RemoteViews getViewAt(int position) {
                RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_stack_item);

                String posterUrl = ImageUtility
                        .getImageUrl(mMovies.get(position).getPosterPath());
                        
                try {
                    Bitmap poster = Picasso.with(StackWidgetRemoteViewsService.this)
                            .load(posterUrl)
                            .get();
                    views.setImageViewBitmap(R.id.widget_movie_poster, poster);
                }catch (IOException e) {
                    Log.e(LOG_TAG, "ERROR: ", e);
                }

                final Intent fillInIntent = new Intent();
                fillInIntent.putExtra(MovieActivity.EXTRA_URL, posterUrl);
                views.setOnClickFillInIntent(R.id.widget_stack_item, fillInIntent);

                return views;
            }

            @Override
            public RemoteViews getLoadingView() {
                return new RemoteViews(getPackageName(), R.layout.widget_stack_item);
            }

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

            @Override
            public long getItemId(int position) {
                if (position < mMovies.size()) {
                    return mMovies.get(position).getId();
                }
                return position;
            }

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

【完整代码】

大多数方法都类似用于如ListView之类的的Adapter。注意这里getViewAt() 返回的是RemoteViews。在getViewAt() 里面,我们已经知道了当前RemoteViews所对应的是哪一部电影,通过fillInIntent.putExtra(MovieActivity.EXTRA_URL, posterUrl) 把海报图片链接放入Intent,结合之前在StackWidgetProvider加入的Intent Template,当点击App Widget时,会打开MovieActivity,同时MovieActivity会收到带有海报图片链接的Intent。MovieActivity的代码见这里

5. 在Manifest中声明App Widget

  最后不要忘了在Manifest中声明App Widget:

<receiver
    android:name=".widget.StackWidgetProvider"
    android:label="@string/stack_widget_name">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/stack_widget_info" />
</receiver>


<service
    android:name=".widget.StackWidgetRemoteViewsService"
    android:exported="false"
    android:permission="android.permission.BIND_REMOTEVIEWS" />

除了AppWidgetProvider的声明,还要加入StackWidgetRemoteViewsService的声明,带有BIND_REMOTEVIEWS权限。

6. 完整代码

  完整代码可以参考这里