Android 101: Como criar um widget StackView

Widgets são uma boa ferramenta para os utilizadores personalizarem o ecrã principal do seu dispositivo. É uma boa forma de aumentar o interesse numa aplicação. Um widget pode fornecer bastante informação de forma sucinta e mostrar atalhos para acções comuns.

O Honeycomb introduziu widgets para colecções. Como o nome implica, são capazes de mostrar vários itens. Existem vários tipos de widgets para colecções:

  • ListView: uma lista vertical de itens (e.g. o widget do Gmail).
  • GridView: uma lista vertical com duas colunas (e.g. o widget dos bookmarks).
  • StackView: uma vista de cartas empilhadas, onde o item da frente pode ser passado à frente para dar lugar ao item a seguir (e.g. o widget do Market).
  • AdapterViewFlipper: anima entre vistas, apenas mostrando uma de cada vez.

Vou explicar como fizemos o widget para a aplicação Honeybuzz. É um widget StackView e mostra os Buzzes com uma foto do autor e uma porção do texto.

Adicionar o widget ao manifesto

É necessário que o widget esteja declarado no manifesto da aplicação. Como estamos a usar um widget StackView, temos que especificar tanto um widget provider como um 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>

Como criar um widget provider

Para o widget provider, temos de especificar a informação necessária e criar a implementação da classe.

A informação do provider é apenas um ficheiro XML que se cria na pasta res\xml. Este é o ficheiro da aplicação Honeybuzz:

<?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>

A maioria dos atributos são auto-explicativos. O atributo updatePeriodMillis define o intervalo de actualização do provider. O mínimo possível são 15 minutos, mas deve-se actualizar o menos frequentemente possível, de forma a conservar bateria.

Para determinar o tamanho do widget podem usar a seguinte fórmula (encontrada no site Android Developers):

  • (número de células * 74) - 2

Portanto, se quiserem um widget 3 por 3 como na aplicação Honeybuzz:

  • (3 * 74) - 2 = 220

O layout de um widget é parecido com um layout de uma Activity, mas é muito mais restritivo. Baseiam-se em RemoteViews e as classes de layout e das vistas que se podem usar são limitadas (principalmente antes do Honeycomb, onde vistas com scrolling não eram possíveis para widgets).

O layout para um widget StackView pode ser tão simples quanto o seguinte:

<?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>

E a implementação da classe do widget provider:

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);
    }
}

No método onUpdate iteramos por todas as instâncias de widgets e adicionamos os Intents para o serviço que vai criar cada vista e também para cada click num item.

Criar o widget service e a vista para cada item

Num widget StackView preciamos de um layout separado para cada item da colecção. Já devem saber como criar um, mas aqui está o que foi utilizado no widget do Honeybuzz:

<?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>

O widget service tem de especificar a fábrica das views, que é responsável por criar a vista para cada item da colecção.

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);
    }
}

As partes mais importantes:

  • StackWidgetService: escrever o método onGetViewFactory para devolver a fábrica das views.
  • StackRemoteViewsFactory: gere as views para cada item da colecção.
    • onDataSetChanged: carrega a informação necessária para mostrar no widget.
    • onDestroy: destruir todos os objectos que já não são necessários.
    • getViewAt: tem de devolver uma vista para o item na posição específica. Criamos uma nova vista com o ficheiro de layout especificado anteriormente, depois vamos buscar o objecto Buzz apropriado que usamos para preencher a vista com informação. No final adicionamos o Intent para o click, de forma a que quando o item seja seleccionado, a aplicação Honeybuzz seja aberta com o mesmo.

Como criar uma imagem de amostra para o widget

Uma imagem de amostra é apresentada quando o utilizador está a navegar na galeria dos widgets e ajuda o mesmo a perceber como o widget funciona. Se não for fornecida uma imagem de amostra, será usado o ícone da aplicação no seu lugar.

Felizmente, o emulador e o SDK do Android vêm com uma aplicação para gerar imagens de amostra a partir de widgets. A aplicação chama-se "Widget Preview" e pode ser usada a partir do emulador ou ser copiada para um dispositivo e ser corrida a partir daí. O código da aplicação está na pasta do SDK no caminho android-sdk\samples\android-11\WidgetPreview. É possível abrir o projecto, fazer o build e copiá-lo para um dispositivo. A aplicação é simples de usar e muito útil. Depois de gerar uma imagem, é necessário adicioná-la aos drawables do projecto e especificá-la na informação do widget provider (ver mais a cima para um exemplo).

Mais recursos

Nuno Freitas
Publicado por Nuno Freitas em 12 dezembro, 2011

Artigos relacionados