Widgets de Escritorio

Vamos a tratar aquí de cómo desarrollar un widget de escritorio, que no es sino una app miniatura que puede incrustarse en otra app (como el Escritorio, por ejemplo) y que recibe actualizaciones periódicas.

Crearemos un widget que nos indique la hora.

Como viene siendo habitual, creamos nuestro proyecto para la API lv8 (o 2.2), con el nombre primerWidget. El paquete será com.alberovalley.primerwidget.
Empezaremos creando el layout del widget. Lo llamaremos widget1.xml y contendrá en un LinearLayout vertical dos elementos: un Textview (id widget1label, dimensiones a wrap_content, tamaño de texto 20dp) y un Button (dimensiones a wrap_content y texto “¡Púlsame!” en el fichero strings.xml).

A la hora de crear nuestros widgets no debemos dejarnos llevar por el impulso de crear layouts complejos con componentes propios. Hay bastantes restricciones a los componentes que se pueden usar en un Widget ya que éste corre en una aplicación diferente y no tiene acceso a tu propio código a la hora de renderizar.

Los únicos componentes que puedes usar en un widget son:

FrameLayout
LinearLayout
RelativeLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper

Cuando tengamos todo listo, iremos a File > New > Android XML File para crear un nuevo AppWidgetProvider,de nombre primer_widget_info.xml sin nada mas. El contenido de dicho xml será el siguiente

 

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dp"
    android:minHeight="72dp"
    android:updatePeriodMillis="1000"
    android:initialLayout="@layout/widget1">
</appwidget-provider>

Le estamos aquí indicando unas dimensiones mínimas, tanto de ancho como de alto; un periodo de actualización en milisegundos y un layout inicial. Iremos por partes.

Android dispone la pantalla de escritorio en matrices de 4×4 celdas cuadradas de 72dp de ancho y alto. Según si queremos que nuestro widget ocupe mas o menos (con el mínimo obvio de 1×1), tendremos que calcular las dimensiones en función a ello. Esta fórmula es muy útil:
Para cada dimensión (alto/ancho), el valor será tamaño = (número de celdas * 74) – 2

Para nuestro widget queremos que tenga 4×1 (4 de ancho, 1 de alto), por lo que los valores se calculan como
ancho = (4 * 74 ) -2 = 294dp.
alto = (1 * 74 ) -2 = 72dp.

El atributo updatePeriodMillis indica la frecuencia con la que el widget recibirá actualizaciones. Se expresa en milisegundos, por lo que en este caso nosotros lo hemos puesto para que actualice cada segundo.

Ahora vamos con el código java. Crearemos una clase que extienda de AppWidgetProvider llamada Primer WidgetProvider en la que usaremos el procedimiento onUpdate para definir cómo queremos que actualice la hora nuestro widget.
El código de la misma sería el siguiente

package com.alberovalley.primerWidget;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;

public class PrimerWidgetProvider extends AppWidgetProvider {

	DateFormat df = new SimpleDateFormat("hh:mm:ss");
	  public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
	    final int N = appWidgetIds.length;
	    // Este bucle se ejecuta para cada App Widget perteneciente a este provider
	    for (int i = 0; i < N; i++) {
	      int appWidgetId = appWidgetIds[i];
	      // Crea un Intent para lanzar PrimerWidgetActivity
	      Intent intent = new Intent(context, PrimerWidgetActivity.class);
	      PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
	      // Obtiene el layout para el App Widget y le asigna un on-click listener al botón
	      RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget1);
	      views.setOnClickPendingIntent(R.id.button, pendingIntent);
	      // Para actualizar el textview
	      views.setTextViewText(R.id.widget1label, df.format(new Date()));
	      // Le dice al AppWidgetManager que realice una actualización al widget actual
	      appWidgetManager.updateAppWidget(appWidgetId, views);
	    }
	  }
}

Los Widgets se ejecutan, como ya dijimos, en una aplicación diferente (la pantalla Home o Escritorio) de la nuestra, por lo que actualizar sus componentes no es tan sencillo como habíamos visto hasta ahora. En lugar de tener acceso directo a los componentes del layout, usamos componentes RemoteViews que nos permiten cambiar cadenas de texto, imágenes y acciones de botones de los componentes remotos.

En nuestro caso queremos dos cosas. Primero, añadir una acción listener a nuestro botón para que cuándo lo pulsemos abra nuestra aplicación principal. Para ello se usan el PendingIntent y el método setOnClickPendingIntent(). Segundo, queremos actualizar la fecha/hora actual en nuestro TextView, lo que logramos llamando a setTextViewText(). Una vez hecho todo eso, actualizamos el widget con el método updateAppWidget().

Por último, debemos añadir al Manifest las siguientes líneas, justo debajo de la activity:

		
<receiver android:name=".PrimerAppWidgetProvider" android:label="@string/widget_label">
  <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data android:name="android.appwidget.provider" android:resource="@xml/primer_widget_info" />
</receiver>

Así especificamos la clase PrimerWidgetProvider, responsable de recibir los intents APPWIDGET_UPDATE. Hay que darle un nombre al widget, lo que hacemos especificando un valor para el atributo android:label. El resto de la configuración del widget se especifica en el fichero de metadatos, sito en res/xml/primer_widget_info.xml.

Y ya hemos acabado. Ejecutamos el proyecto para que se instale en el emulador. Nos aparecerá la vista de aplicación, con su HelloWorld, etc (ya que no habíamos cambiado nada). Salimos a la pantalla de Escritorio, hacemos una pulsación larga en el fondo y elegimos Widgets. De entre la lista de widgets disponibles debe aparecer el nuestro “Mi Primer Widget”. Lo seleccionamos y en la pantalla de Escritorio nos aparecerán un botón y un texto con la hora actual.
Nuestro Widget
¡Un momento! ¡El reloj no se está actualizando! Pusimos el periodo de actualización a 1000 milisegundos, por lo que debería actualizar cada segundo ¿no?. Pues no. Hay una razón perfecta para ello: cada vez que un widget necesita actualizarse, Android “despertará” el dispositivo, cargará tu aplicación y ejecutará la clase WidgetProvider para actualizar el widget. Es así incluso si tu pantalla está apagada y el teléfono “dormido”. Si el periodo de actualización es de un par de veces por hora o inferior no implicará mucho desgaste de la batería. Pero si el periodo de actualización es tan corto como en nuestro ejemplo, la batería se agotaría muy rápido, con el consiguiente cabreo de los usuarios. Para evitarlo, Google limita la actualización a una cada media hora o superior.

Por suerte hay otro modo de manejar actualizaciones mas frecuentes: una que sólo opera cuándo la pantalla está activa para no agotar la batería. Para ello se usa el AlarmManager.

Si quieres actualizar tu widget con frecuencia mientras la pantalla está encendida y sólo cuándo lo está, puedes usar un ‘AlarmManager‘, que es un servicio de sistema dónde tu código registra eventos que se disparan en momentos concretos o a periodos concretos. Esto permite que algo ocurra en un momento concreto incluso si la aplicación no está ejecutándose en dicho momento. Mejor aún, cuándo especificas el tipo de temporizador adecuado, puedes decirle que no entregue el evento si el teléfono está “dormido” en el momento en que correspondería dispararlo.

Las alarmas, como las actualizaciones de Widget, se envían como Intents, al igual que todo en Android. Por lo tanto, lo primero que debemos hacer es configurar nuestra aplicación para que escuche a un intent personalizado que crearemos específicamente para nuestras actualizaciones de reloj.

Añadiremos para ello en el Manifest, bajo el intent-filter para APPWIDGET_UPDATE, nuestro intent personalizado. Nosotros le damos el nombre, pero éste debe ser cualificado (debe tener un dominio invertido delante) para que así podamos diferenciar nuestros intents de los de los demás que pudiera haber en el sistema. Añadiremos lo siguiente:

  <intent-filter>
    <action android:name="com.alberovalley.primerwidget.widget.PRIMERWIDGET_WIDGET_UPDATE" />
  </intent-filter>

A continuación debemos configurar el AlarmManager en nuestra clase que hereda de WidgetProvider, añadiéndole lo siguiente:

	  private PendingIntent createClockTickIntent(Context context) {
		    Intent intent = new Intent(CLOCK_WIDGET_UPDATE);
		    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		    return pendingIntent;
		}
		@Override
		public void onEnabled(Context context) {
		        super.onEnabled(context);
		        //El Widget Provider está habilitado, iniciamos el temporizador para 
		        // actualizar el widget cada segundo
		        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
		        Calendar calendar = Calendar.getInstance();
		        calendar.setTimeInMillis(System.currentTimeMillis());
		        calendar.add(Calendar.SECOND, 1);
		        alarmManager.setRepeating(AlarmManager.RTC, calendar.getTimeInMillis(), 1000
		, createClockTickIntent(context));
		}
		@Override
		public void onDisabled(Context context) {
		        super.onDisabled(context);
		        // El Widget Provider está deshabilitado, apagamos el temporizador
		        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
		        alarmManager.cancel(createClockTickIntent(context));
		}

Los métodos onEnabled() y onDisabled() son llamados respectivamente el primero cuándo se crea el widget de tu tipo, y el segundo cuándo se borra el widget. Queremos mandar “ticks” regulares cuándo el primer widget se crea, y queremos parar el ‘AlarmManager’ cuándo el último widget sea borrado. Simplemente llamamos al AlarmManager.setRepeating() para crear una alarma reiterativa, y llamamos al método cancel() cuándo hemos acabado con ella.

Hay que notar que cuándo creamos la alarma especificamos un Tipo de Alarma AlarmManager.RTC. Este tipo de alarma sólo se genera cuando el móvil está “despierto”. Esta es la característica en la que confiamos para mandar las actualizaciones sólo cuándo está activo, y no cuándo el dispositivo está “dormido”.

En este punto ya le hemos dicho a Android que nuestra app tiene interés en recibir estos intents, y hemos configurado el ‘AlarmManager’ para que mande los intents con una frecuencia regular. Todo lo que queda es escribir el código para manejar los intents conforme llegan. Para ello tenemos que alterar nuestra clase que hereda de WidgetProvider de dos formas:
1) refactorizamos el código que había dentro de onUpdate dentro de un método void que sirva para un único widget (en el onUpdate se trata todo un array de widgets), para evitar repetirnos innecesariamente. Esta sería una forma:

public void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetID){
      // Crea un Intent para lanzar PrimerWidgetActivity
      Intent intent = new Intent(context, PrimerWidgetActivity.class);
      PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
      // Obtiene el layout para el App Widget y le asigna un on-click listener al botón
      RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget1);
      views.setOnClickPendingIntent(R.id.button, pendingIntent);
      // Para actualizar el textview
      views.setTextViewText(R.id.widget1label, df.format(new Date()));
      // Le dice al AppWidgetManager que realice una actualización al widget actual
      appWidgetManager.updateAppWidget(appWidgetID, views);
  
}

Esto dejaría así nuestro onUpdate ahora:

  public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            final int N = appWidgetIds.length;
	    // Este bucle se ejecuta para cada App Widget perteneciente a este provider
	    for (int i = 0; i < N; i++) {
	    	int appWidgetId = appWidgetIds[i];
	    	updateAppWidget(context, appWidgetManager, appWidgetId);
	    }
  }

2) incluímos el método receptor de intents para que gestione el nuestro:

/**
 * El nombre del Intent Personalizado que usa el 'AlarmManager' para que se actualice el reloj 1 vez por segundo.
 */
public static String CLOCK_WIDGET_UPDATE = "com.alberovalley.primerwidget.widget.PRIMERWIDGET_WIDGET_UPDATE";
@Override    public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);
    if (CLOCK_WIDGET_UPDATE.equals(intent.getAction())) {
        // Obtén el widget manager y el id para este widget provider, 
        // tras ello llama al método de actualización del reloj compartido
        ComponentName thisAppWidget = new ComponentName(context.getPackageName(), getClass().getName());
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        int ids[] = appWidgetManager.getAppWidgetIds(thisAppWidget);
        for (int appWidgetID: ids) {
            updateAppWidget(context, appWidgetManager, appWidgetID);
        }
    }
}

Este último método queda escuchando a la espera del intent, y actualiza los contenidos del componente una vez recibido.

Aquí lo tenemos. Usando el ‘AlarmManager’ podemos hacer que el sistema operativo mande a nuestra aplicación un intent regularmente. En este ejemplo, hemos usado la técnica para actualizar un widget con mucha frecuencia, pero también puede usarse para hacer actualizaciones periódicas de otros componentes de tu app.

El código completo de esta app puede encontrarse en el repositorio correspondiente de github

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: