Widgets en Android: creando una linterna


Todos los usuarios de Android conocemos los widgets, pequeñas aplicaciones que por lo general, corren y/o se sitúan en la pantalla principal (Homescreen).

A diferencia de una aplicación tradicional, un widget no necesita una actividad para vivir, sino que se hace uso de la clase RemoteViews para su ejecución. Esta clase implementa un layout (fichero xml con elementos de interfaz) que contiene una o más vistas (View) y tiene la peculiaridad de poder ser ejecutado por otro proceso con los mismos permisos que la aplicación original.

Como introducción a los widgets crearemos una aplicación completa que consistirá en un widget consistente en un ImageButton con el dibujo de una lámpara que cambiará su imagen cuando el usuario lo pulse, haciendo que se encienda y apague la linterna en un dispositivo que cuente con cámara con flash.

Los elementos que necesitaremos para crear este widget serán los siguientes, describiéndolos descendentemente, desde la capa más externa (interfaz) hasta la más interna (receptor de las acciones generadas al pulsar el widget).

  • res/layout/flashlight_layout.xml: Fichero XML que contendrá el layout del widget, es decir, la interfaz del widget en sí, compuesta en este caso por un ImageButton.
  • res/xml/flashlight_widget_provider.xml: Fichero XML que contendrá la descripción del widget provider. Contendrá el layout inicial (que será el fichero anterior) y el tamaño mínimo del widget.
  • FlashLightWidgetProvider.java: Clase que heredará de AppWidgetProvider y se encargará de generar los Intents al producirse algún evento sobre el widget y actualizar las instancias del widget.
  • FlashLightBroadcastReceiver.java: Clase que heredará de BroadcastReceiver y se encargará de detectar las acciones generadas por el elemento anterior. Al detectar la acción, ejecutará la lógica de la aplicación y regenerará la interfaz del widget.

Creando la aplicación

Comenzaremos creando un nuevo proyecto de Android. Dado que no necesitaremos actividades, omitiremos el tema.

Creando un nuevo proyecto

Desmarcamos la opción “Create activity” y le pulsamos en continuar.

Creando un proyecto (2)

Seleccionamos una imagen para nuestra aplicación. Utilizaremos dos imágenes para el widget: una bombilla apagada y una bombilla encendida. Utilizaremos la bombilla apagada como icono de la aplicación.

Icono

Añadiendo los recursos

Comenzaremos añadiendo las imágenes que servirán como estados del widget. Estas imágenes serán las siguientes, que tendrán como identificadores bombilla_on y bombilla_off:

Bombilla apagadaBombilla encendida

Para añadirlas, haremos click derecho sobre las carpetas res/drawable-XXXX y seleccionaremos la opción Import…

Importar imagen

En el diálogo, seleccionaremos la opción General > File System.

Importar imagen (2)

Buscaremos la carpeta en la que se encuentren las imágenes y las seleccionaremos. Con esto, tendremos las imágenes almacenadas en nuestro proyecto.

Configurando los ficheros XML

Es hora de definir el primer elemento: el layout del widget. En realidad estamos comenzando la casa por el tejado, pero es la mejor forma de entender el proceso. Pulsamos en New > Android > Android XML File y seleccionamos Layout en el desplegable Resource Type. Le asignamos un nombre y elegimos ImageButton como elemento raiz.

Creando un layout

A continuación abrimos el fichero y le asignamos los siguientes valores:


<?xml version="1.0" encoding="utf-8"?>
 <ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/ibFlashlight"
	android:layout_width="60dp"
	android:layout_height="60dp"
	android:scaleType="fitCenter"
	android:background="@drawable/bombilla_off" >
 </ImageButton>

Como podemos ver, asignamos un identificador al ImageButton (ibFlashlight), un ancho y alto fijo (60dp x 60dp) y hacemos que el fondo sea la bombilla apagada (drawable/bombilla_off).

Lo siguiente será crear el XML que define el proveedor de widgets. Este elemento será consultado por Android para añadir nuestro widget a la lista de widgets disponible al pulsar el menú de aplicaciones y seleccionar la sección “Widgets”. Seleccionamos New > Android > Android XML File, marcando AppWidget Provider en el desplegable y asignándole el nombre que deseemos.

Widgets provider

A continuación editamos el fichero y le indicamos cuál será el layout inicial, en este caso el que acabamos de crear en el paso anterior (layout/flashlight_layout). De este modo, Android sabrá que el widget estará compuesto por los elementos contenidos en ese layout, en este caso, por un ImageButton.

También indicaremos el tamaño mínimo del widget, que coincidirá con el del ImageButton.


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

Generando eventos

Crearemos ahora la clase que se encargará de actualizar el widget y lanzar en broadcast los Intent que serán capturados posteriormente por la clase que construiremos posteriormente. Haremos esto en dos pasos: en primer lugar construiremos dos métodos públicos estáticos genéricos: uno para generar el Intent y otro para solicitar la actualización de los widgets aplicación. El hecho de que se planteen como públicos y estáticos se justifica porque la clase encargada de capturar las acciones de los Intents generados por este elemento hará uso de ellos.

Comencemos creando una clase que herede de AppWidgetProvider al que le asignaremos una cadena de texto pública estática y constante que simbolizará la clave del Intent que lanzaremos y que deberá identificar posteriormente la clase de capturar las acciones.


	public class FlashlightWidgetProvider extends AppWidgetProvider
	 {
		public static final String PUSH_ACTION = "org.danigarcia.android.flashlight.FlashlightWidgetProvider.PUSH_ACTION";
	 }

A continuación crearemos un método que generará un PendingIntent que será enviado en modo de broadcast, de modo que cualquier objeto de la clase BroadcastReceiver suscrito a la acción PUSH_ACTION (definida en el paso anterior) pueda detectarlo.


	// Generamos un PendingIntent que sera enviado en broadcast, de modo que cualquier objeto
	// de la clase BroadcastReceiver suscrito al action PUSH_ACTION pueda detectarlo.
	 public static PendingIntent generarIntentBroadcast(Context context)
	 {
		// Creamos un nuevo Intent y le asociamos la accion PUSH_ACTION para que el BroadcastReceiver
		// pueda distinguir su tipo
		Intent intent = new Intent();
		intent.setAction(PUSH_ACTION);

		// Generamos el intent y lo devolvemos como resultado del metodo
		return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
	 }

Un segundo método se encargará de solicitar a la aplicación que actualice sus widgets.


	public static void solicitarUpdate(Context context, RemoteViews views)
	 {
		// Creamos un ComponentName a partir del paquete y la clase
		ComponentName widget = new ComponentName(context, FlashlightWidgetProvider.class);

		// Obtenemos una referencia al AppWidgetManager
		AppWidgetManager awManager = AppWidgetManager.getInstance(context);

		// Solicitamos una actualizacion al AppWidget
		awManager.updateAppWidget(widget, views);
	 }

Finalmente, sobrecargamos el handler onUpdate que realizará lo siguiente:

  1. Creamos una nueva interfaz del widget (RemoteViews) pasándole el layout con el que la queremos construir, que será el primer fichero XML que definimos. Este proceso sería el equivalente a utilizar un LayoutInflater al hacer uso de elementos de interfaz locales (View) en lugar de remotos (RemoteViews).
  2. Añadimos el evento onClickPendingIntent al ImageButton que se encuentra dentro del layout y cuyo ID es ibFlashlight (primer parámetro). El segundo parámetro será el PendingIntent que el botón enviará cuando sea pulsado, y que generaremos mediante el método estático generarIntentBroadcast() que hemos codificado más arriba en esta misma clase.
  3. Finalmente, solicitamos la actualización de los widgets de la aplicación.

@Override
 public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
 {
	// Instanciamos el widget a partir del layout
	RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.flashlight_layout);

	// Añadimos el listener para el evento onClick al ImageButton
	remoteViews.setOnClickPendingIntent(R.id.ibFlashlight, FlashlightWidgetProvider.generarIntentBroadcast(context));

	// Solicitamos una actualizacion de la aplicacion del widget
	solicitarUpdate(context, remoteViews);
 }

El código diseñado hasta el momento ya es capaz de generar un widget con un botón cuyo fondo se corresponde con una imagen y que genera un PendingIntent en broadcast. Sólo nos falta crear un elemento que sea capaz de detectar esos PendingIntents y ejecute la lógica de la aplicación cuando ello suceda.

Detectando eventos

Ya estamos familiarizados con la clase BroadcastReceiver, utilizada en ejemplos anteriores. En esta ocasión, crearemos una nueva clase que herede de ella y añadiremos un par de métodos personalizados que sirva para nuestros propósitos: uno para realizar la lógica asociada a la detección del evento (cambiarEstadoLinterna()) y otro para crear desde cero la interfaz remota del widget.

Comencemos declarando la clase y añadiendo un par de atributos: un booleano que indicará si la linterna está encendida o apagada y una cámara que referenciará la cámara del dispositivo.


	public class FlashlightBroadcastReceiver extends BroadcastReceiver
	 {
		private static boolean encendida = false;
		private static Camera camera = null;
	 }

El primer método realizará el mismo proceso de creación que hacíamos en el método onUpdate() de la clase anterior, con el añadido de que detectará el estado de la cámara (variable encendida) y cambiará su imagen dependiendo de su estado.

La imagen de un RemoteViews se puede modificar invocando el método setImageViewResource(), indicándole el identificador del elemento cuya imagen queremos cambiar y el identificador de la imagen en sí.


	// Dado que estamos instanciando RemoteViews desde cero mediante un constructor new(),
	 // este elemento no tendra asociado ni layout ni listeners para el evento onClickPendingIntent, por lo
	 // que sera necesario realizar todo el proceso de creacion en este metodo.
	 public static RemoteViews crearWidget(Context context)
	 {
		RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.flashlight_layout);

		// Seleccionamos la nueva imagen.
		// Si pasa a estar encendida, se mostrara bombilla_on
		// Si pasa a estar apagada, se mostrara bombilla_off
		int idImagen = (encendida ? R.drawable.bombilla_on : R.drawable.bombilla_off);

		// Actualizamos la imagen del widget
		remoteViews.setImageViewResource(R.id.ibFlashlight, idImagen);

		// Añadimos el listener para el evento onClick al ImageButton
		remoteViews.setOnClickPendingIntent(R.id.ibFlashlight, FlashlightWidgetProvider.generarIntentBroadcast(context));

		return remoteViews;
	 }

El segundo método se encargará de lo siguiente:

  1. Cambiar el estado de la cámara (encendida/apagada)
  2. Comprobar si el dispositivo posee cámara (y flash)
  3. Encender la cámara si está apagada
  4. Apagar la cámara si está encendida
  5. Realizar el mismo proceso que el método onUpdate() de la clase anterior (crear un widget y solicitar su actualización).

Vayamos paso a paso. En primer lugar actualizaremos el estado de la cámara y detectaremos si el dispositivo posee flash. En caso negativo, el método finalizará.


	private void cambiarEstadoLinterna(Context context)
	 {
		Parameters parameters = null;

		// Comprobamos que la linterna existe en el dispositivo
		PackageManager pm = context.getApplicationContext().getPackageManager();
		if(!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH))
		return;

		// Cambiamos el estado de la linterna. Si estaba encendida, se desactiva y viceversa
		encendida = !encendida;

	 }

El siguiente código se encargará de activar la linterna si el nuevo estado de encendida (recordemos que lo hemos cambiado en el paso anterior) es true. Para ello abriremos la cámara del dispositivo, extraeremos sus parámetros de configuración y cambiaremos el modo en el que funciona el flash a FLASH_MODE_TORCH.

A continuación volveremos a guardar los parámetros de configuración e invocamos el método startPreview() para activar el flash.


	if(encendida)
	{
		// Si la linterna va a encenderse, abrimos la camara y extraemos sus
		// parametros.
		camera = Camera.open();
		parameters = camera.getParameters();

		// Modificamos el parametro que controla el flash y le asignamos el
		// modo FLASH_MODE_TORCH para utilizarlo como linterna. A continuacion,
		// le volvemos a insertar los parametros e invocamos startPreview()
		// para activarla.
		parameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
		camera.setParameters(parameters);
		camera.startPreview();
	 }

En caso de que queramos apagarla, dado que camera es una referencia de carácter estático, estará ya instanciada por el método anterior, por lo que realizamos el proceso inverso: extraemos sus parámetros de configuración, cambiamos el modo de funcionamiento del flash a FLASH_MODE_OFF para apagarlo, volvemos a insertar la configuración en la cámara y paramos la previsualización invocando el método stopPreview().

A continuación liberaremos los recursos de la cámara, ya que no la necesitaremos más. Sobra decir que, aunque no esté mostrado explícitamente, conviene englobar esta casuística en un bloque try…catch que permita capturar las excepciones y poder liberar los recursos de la cámara adecuadamente.


	else
	 {
		if(camera == null)
			return;

		// Si la linterna va a apagarse, se extraen los parametros de la camara
		// y se asigna el modo de flash FLASH_MODE_OFF para apagarlo.
		parameters = camera.getParameters();
		parameters.setFlashMode(Parameters.FLASH_MODE_OFF);

		// Se asignan los parametros nuevamente a la camara y se llama al metodo
		// stopPreview() para apagar el flash.
		camera.setParameters(parameters);
		camera.stopPreview();

		// Se liberan los recursos de la camara
		camera.release();
		camera = null;
	 }

Finalmente, realizamos el mismo proceso que el método onUpdate() de la clase anterior, creando la interfaz remota del widget y solicitando su actualización.


	// Actualizamos el widget.
	 RemoteViews remoteViews = crearWidget(context);

	// Solicitamos una actualizacion al AppWidget
	 FlashlightWidgetProvider.solicitarUpdate(context, remoteViews);

El último método será el handler que se ejecutará al recibir un Intent lanzado en broadcast. Simplemente filtraremos por la acción (definida como una cadena estática en la clase anterior) y ejecutar el método cambiarEstadoLinterna() cuando se detecte.


	@Override
	 public void onReceive(Context context, Intent intent) {

	// Filtramos la acción PUSH_ACTION de la linterna
	 String action = intent.getAction();

	 // Cambiamos el estado de la linterna
	 if(FlashlightWidgetProvider.PUSH_ACTION.equals(action))
	 cambiarEstadoLinterna(context);
	 }

Como broche final, el siguiente diagrama muestra un pequeño resumen de la aplicación:

Resumen del widget

Configurando el manifiesto

Sólo nos queda un último paso antes de finalizar nuestro widget-linterna: configurar el manifiesto. En él deberemos realizar las acciones básicas tales como asignar permiso a la aplicación para usar la linterna:


	<uses-permission android:name="android.permission.CAMERA" />
	<uses-permission android:name="android.permission.FLASHLIGHT" />
	<uses-feature android:name="android.hardware.camera" />

Después registraremos dos ItentReceiver dentro del tag application : por un lado, la clase FlashLightWidgetProvider se suscribirá a los Intent cuya acción sea APPWIDGET_UPDATE y provocará el lanzamiento del método onUpdate().


	<receiver android:name="FlashlightWidgetProvider">
		<intent-filter>
			<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
		</intent-filter>

		<meta-data
			android:name="android.appwidget.provider"
			android:resource="@xml/flashlight_widget_provider"
		/>
	 </receiver>

Por otro lado, registraremos la clase FlashlightBroadcastReceiver para que se suscriba a los Intent cuya acción sea la declarada en la constante FlashlightWidgetProvider.PUSH_ACTION. Dado que estamos indicando esta información en el fichero XML, debemos incluir el valor de la cadena, que a fin de cuentas será lo que se utilizará para realizar el filtrado.


	<receiver android:name="FlashlightBroadcastReceiver" android:label="@string/app_name">
		<intent-filter>
			<action android:name="org.danigarcia.android.flashlight.FlashlightWidgetProvider.PUSH_ACTION" />
		</intent-filter>

		<meta-data
			android:name="android.appwidget.provider"
			android:resource="@xml/flashlight_widget_provider"
		/>
	 </receiver>

Esta última entrada en AndroidManifest.xml sería equivalente a realizar lo siguiente en código:


	IntentFilter filtro = new IntentFilter(FlashlightWidgetProvider.PUSH_ACTION);
	this.registerReceiver(flashlightBroadcastReceiver, filtro);

Hecho esto, nuestra aplicación estará terminada, y una vez instalada, la instalaríamos como un widget más. Puedes descargarte el código fuente desde aquí y el fichero .apk desde aquí.

Anuncios

One comment

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