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>
<?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>
<?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>
<?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>
<?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);
}
}
}
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); } } }
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;
}
};
}
}
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; } }; } }
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" />
<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" />
<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. 完整代码

  完整代码可以参考这里