Collection Widget实现举例
Author: nex3z
2016-02-21
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" >
android:id="@+id/widget_stack"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/widget_stack_item" >
android:id="@+id/widget_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="sans-serif-condensed"
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/empty_list"/>
<?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" >
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" />
<?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();
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new RemoteViewsFactory() {
private List<Movie> mMovies;
public void onCreate() { }
public void onDataSetChanged() {
MovieService movieService = App.getRestClient().getMovieService();
Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC, 1);
MovieResponse response = call.execute().body();
List<Movie> movies = response.getMovies();
} catch (IOException e) {
Log.e(LOG_TAG, "ERROR: ", e);
public void onDestroy() { }
return mMovies == null ? 0 : mMovies.size();
public RemoteViews getViewAt(int position) {
RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_stack_item);
String posterUrl = ImageUtility
.getImageUrl(mMovies.get(position).getPosterPath());
Bitmap poster = Picasso.with(StackWidgetRemoteViewsService.this)
views.setImageViewBitmap(R.id.widget_movie_poster, poster);
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);
public RemoteViews getLoadingView() {
return new RemoteViews(getPackageName(), R.layout.widget_stack_item);
public int getViewTypeCount() {
public long getItemId(int position) {
if (position < mMovies.size()) {
return mMovies.get(position).getId();
public boolean hasStableIds() {
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:
android:name=".widget.StackWidgetProvider"
android:label="@string/stack_widget_name">
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
android:name="android.appwidget.provider"
android:resource="@xml/stack_widget_info" />
android:name=".widget.StackWidgetRemoteViewsService"
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. 完整代码
完整代码可以参考这里。