Este artigo faz parte de um Guia de Desenvolvimento para Android. O guia fala de vários temas necessários para construir uma aplicação simples na sua totalidade, abordando várias funcionalidades introduzidas no Honeycomb e que ainda são válidas para o Ice Cream Sandwich. Usamos um cliente do Google Buzz chamado Honeybuzz como exemplo para cada tópico. Na introdução encontra-se uma listagem completa de todos os artigos.
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).