This article is part of an Android Development Guide. The guide focuses on several subjects necessary to build a fully functional basic app, talking about several features that were introduced in Honeycomb and are still valid for Ice Cream Sandwich. We use a Google Buzz client called Honeybuzz as an example for each topic. Refer to the introduction for a complete list of all articles.
Widgets are a great tool for a user to customize his home screen. It is a great way to enhance an app's engagement. A widget can provide a lot of information at a glance and offer shortcuts for common actions.
Honeycomb introduced widgets with collections. As the name implies, they are able to display several items. There are several types of widgets with collections:
I will explain how we built the widget for the Honeybuzz application. It is a StackView and displays Buzz-like cards with a user picture and a portion of the Buzz text.
The widget needs to be declared in the application's manifest. Because we are using a StackView widget, we specify both the widget provider and the widget service.
<application> <!-- StackWidget Provider --> <receiver android:name="StackWidgetProvider"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/stackwidgetinfo" /> </receiver> <!-- StackWidget Service --> <service android:name="StackWidgetService" android:permission="android.permission.BIND_REMOTEVIEWS" android:exported="false" /> </application>
For the widget provider, you need to specify the widget provider info and create the widget provider class implementation.
The widget provider info is just an XML file that you create in the res\xml folder of your application. Here is the file from the Honeybuzz application:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="220dp" android:minHeight="220dp" android:updatePeriodMillis="1800000" android:initialLayout="@layout/stackwidget" android:autoAdvanceViewId="@id/stackWidgetView" android:previewImage="@drawable/stackwidget_preview"> </appwidget-provider>
Most of the attributes are self explanatory. The updatePeriodMillis attribute defines how often it requests an update from the provider class. The minimum possible is 15 minutes, but you should update as little as possible in order to save battery life.
To determine the widget size you can use the following formula (given in the Android Developers site):
So if you want a 3 by 3 widget like in the Honeybuzz application you get for each side:
A layout widget is comparable to a normal layout for an Activity, but it is much more restrictive. They are based on RemoteViews and both the layout classes and the views that you can use are limited (especially before Honeycomb, where scrolling views were not available for widgets).
A layout for a StackView widget can be as simple as the following:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<StackView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stackWidgetView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:loopViews="true" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stackWidgetEmptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/no_activities"
android:background="@drawable/stackwidget_background"
android:gravity="center"
android:textColor="#ffffff"
android:textStyle="bold"
android:textSize="16sp" />
</FrameLayout>Now for the implementation of the widget provider class:
public class StackWidgetProvider extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int i = 0; i < appWidgetIds.length; ++i) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.stackwidget);
// set intent for widget service that will create the views
Intent serviceIntent = new Intent(context, StackWidgetService.class);
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); // embed extras so they don't get ignored
remoteViews.setRemoteAdapter(appWidgetIds[i], R.id.stackWidgetView, serviceIntent);
remoteViews.setEmptyView(R.id.stackWidgetView, R.id.stackWidgetEmptyView);
// set intent for item click (opens main activity)
Intent viewIntent = new Intent(context, HoneybuzzListActivity.class);
viewIntent.setAction(HoneybuzzListActivity.ACTION_VIEW);
viewIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
viewIntent.setData(Uri.parse(viewIntent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent viewPendingIntent = PendingIntent.getActivity(context, 0, viewIntent, 0);
remoteViews.setPendingIntentTemplate(R.id.stackWidgetView, viewPendingIntent);
// update widget
appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}In the onUpdate method we iterate through all the widget instances and we add Intents for the service that will create each widget view and also for an item's click.
In a StackView widget you need a separate layout that will be used to display a single item in the collection. You should know how to create one by now, but here is the one used in the Honeybuzz widget as an example:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stackWidgetItem"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@drawable/stackwidget_border"
android:padding="4dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@drawable/stackwidget_background">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stackWidgetItemUser"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:padding="10dp">
<ImageView android:id="@+id/stackWidgetItemPicture"
android:layout_height="100dip"
android:layout_width="100dip">
</ImageView>
<TextView android:id="@+id/stackWidgetItemUsername"
android:textSize="10sp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:paddingTop="6dp">
</TextView>
</LinearLayout>
<TextView android:id="@+id/stackWidgetItemContent"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:maxLines="7"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingRight="6dp"
android:paddingLeft="0dp">
</TextView>
</LinearLayout>
</LinearLayout>The widget service has to specify the views factory, which is responsible for creating a view for each item in the collection.
public class StackWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private final ImageDownloader imageDownloader = new ImageDownloader();
private List<Buzz> mBuzzes = new ArrayList<Buzz>();
private Context mContext;
private int mAppWidgetId;
public StackRemoteViewsFactory(Context context, Intent intent) {
mContext = context;
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
}
public void onCreate() {
}
public void onDestroy() {
mBuzzes.clear();
}
public int getCount() {
return mBuzzes.size();
}
public RemoteViews getViewAt(int position) {
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.stackwidget_item);
if (position <= getCount()) {
Buzz buzz = mBuzzes.get(position);
if (buzz.picture != null) {
try {
Bitmap picture = imageDownloader.downloadBitmap(buzz.picture, 100, 100, 70);
rv.setImageViewBitmap(R.id.stackWidgetItemPicture, picture);
}
catch(Exception e) {
Logging.e("Error reading picture file", e);
}
}
if (!buzz.username.isEmpty()) {
rv.setTextViewText(R.id.stackWidgetItemUsername, buzz.username);
}
rv.setTextViewText(R.id.stackWidgetItemContent, Html.fromHtml(buzz.content));
// store the buzz ID in the extras so the main activity can use it
Bundle extras = new Bundle();
extras.putString(HoneybuzzListActivity.EXTRA_ID, buzz.id);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
rv.setOnClickFillInIntent(R.id.stackWidgetItem, fillInIntent);
}
return rv;
}
public RemoteViews getLoadingView() {
return null;
}
public int getViewTypeCount() {
return 1;
}
public long getItemId(int position) {
return position;
}
public boolean hasStableIds() {
return true;
}
public void onDataSetChanged() {
mBuzzes = Buzz.getBuzzes(HoneybuzzApplication.buzz, mContext);
}
}The important tidbits:
A preview image will be shown when the user is selecting the widget to add from the gallery and will help a user understand how the widget looks like and what it does. If you don't provide a preview image for your widget, your application's icon will be used instead.
Thankfully, the Android emulator and the Android SDK come with an application to generate a preview image from a widget. The application is called "Widget Preview" and you can either use it from the emulator or copy it to your own device and run it from there. You will find the code for the application in your SDK folder at android-sdk\samples\android-11\WidgetPreview. You can open the project, build it and copy it to your device. The application is straightforward to use and very useful. After generating the image, you need to include it in the drawables of your project and specify it in the widget provider info resource (see the provider info above for an example).