Android 101: How to create a StackView widget

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:

  • ListView: a vertical scrolling list of items (e.g. Gmail widget).
  • GridView: a scrolling list with two columns of items (e.g. bookmarks widget).
  • StackView: a stacked card view, where the front item can be flipped to give room for the item after it (e.g. Market widget).
  • AdapterViewFlipper: animates between views, only showing one at a time.

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.

Add the widget to the manifest

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>

How to create the widget provider

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):

  • (number of cells * 74) - 2

So if you want a 3 by 3 widget like in the Honeybuzz application you get for each side:

  • (3 * 74) - 2 = 220

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.

Create the widget service and the view for each item

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:

  • StackWidgetService: override the onGetViewFactory method so we can return our own views factory.
  • StackRemoteViewsFactory: will handle the views for each item in the collection.
    • onDataSetChanged: loads the necessary data that will be displayed in the widget.
    • onDestroy: make sure to destroy any objects that are no longer needed.
    • getViewAt: must return a view for the item at the specified position. We create a new view from the layout file specified previously, then we get the appropriate Buzz object which we use to fill the view with information. At the end we make sure the view has the on-click Intent, so that when it gets clicked, it will open the Honeybuzz application and load the correct Buzz object.

How to create a preview image for your widget

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).

More resources

Nuno Freitas
Posted by Nuno Freitas on December 12, 2011

Related articles