Widgets

Se describen como una vista de aplicación que puede ser incrustada en otras aplicaciones (como la pantalla de inicio del terminal) y suelen recibir actualizaciones periódicas. Los pasos mas importantes para la creación de un widget son los siguientes:
  • Crear un archivo de recursos xml con un objeto "AppWidgetProviderInfo" que describa sus metadatos tales como su diseño y periodo de actualización.
  • Implementar la clase "AppWidgetProvider" con sus métodos básicos.
  • Y definir un layout con el diseño que utilizara el widget.
Toda la información de este articulo disponible en: App Widgets


1. Declarar widget en el AndroidManifest

Antes de ponernos a crear nuestro widget es necesario declararlo en el manifiesto de la siguiente manera:
<application .... >
    ....
    ....
        
    <receiver android:name="com.datohosting.widgets.MiWidget">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
        </intent-filter>
        <meta-data 
            android:name="android.appwidget.provider"
            android:resource="@xml/widget_normal"/>
    </receiver>
        
</application>
Lo primero es declarar un elemento <receiver> que requiere el atributo "android:name" en el que indicaremos la clase "AppWidgetProvider" que utilizara nuestro widget.

A continuación y dentro del elemento receiver necesitamos crear un elemento <intent-filter> para especificar que el widget es actualizable.

Y por ultimo unos meta-datos para enlazar el archivo de recursos xml "AppWidgetProviderInfo" con el widget que estamos creando.



2. Crear archivo de recursos xml

Este archivo va a describir las cualidades esenciales del widget como sus dimensiones, diseño o actualización. Debe ubicarse en la carpeta "res/xml" y debe definir un elemento <appwidget-provider> de la siguiente manera:
<appwidget-provider 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure" 
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

No necesariamente hay que declarar todos los atributos, todo dependerá de nuestras necesidades. Vamos a explicar para que sirve cada uno:

  • minWidth y minHeight - indican la cantidad mínima de espacio que utilizara el widget.
  • minResizeWidth y minResizeHeight - indican el tamaño mínimo que deberá utilizar el widget.
  • updatePeriodMillis - define la frecuencia de actualización, el tiempo que debe de pasar para volver a llamar al método "onUpdate()". No se garantiza con exactitud el valor introducido y es recomendable no actualizar continuamente, tal vez una vez por hora. Si el dispositivo esta dormido se llevara a cabo la actualización de toda maneras por lo que puede ser recomendable actualizarlo a través de una alarma del dispositivo.
  • initialLayout - recurso "layout" con el diseño del widget.
  • configure - define una activity por defecto para configurar el widget la primera vez que el usuario lo arrastra al escritorio.
  • previewImage - define un archivo de recursos "drawable" con una imagen de lo que seria el widget, la vista previa en la pestaña widgets del dispositivo. Si no se establece, el usuario vera por defecto el icono de la aplicación como vista previa.
  • autoAdvanceViewId - especifica la id del widget que debe auto-avanzar. 
  • resizeMode - especifica como se debe ajustar su tamaño.
  • widgetCategory - indica si el widget sera de pantalla de inicio, de bloqueo o ambos.
  • initialKeyguardLayout - recurso "layout" con el diseño del widget para la pantalla de bloqueo.

Para una correcta creación del tamaño y dimensiones del widget podemos visitar el siguiente enlace: Instrucciones de diseño para widgets



3. Crear diseño

El siguiente paso es crear el diseño de nuestro widget y para ello crearemos un nuevo recurso en la carpeta "res/layout". Este diseño se basa en "RemoteViews" y no soporta cualquier tipo de diseño o vista.

ViewGroup compatibles:
  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

Widgets compatibles:
  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

También soporta "ViewStub" (vista invisible que puede utilizarse para inflar un diseño en tiempo de ejecución).



4. Aplicar margenes

De manera opcional podemos aplicar un margen a nuestro widget, aunque en versiones superiores a Android 4.0 se gestionan automáticamente. Para ello deberemos seguir estos tres pasos:

  1. Ajustar el atributo "targetSdkVersion" a 14 o mayor

  2. Crear los recursos con el margen:
  3. res/values/dimens.xml
        <dimen name="widget_margin">8dp</dimen>
    
    res/values-v14/dimens.xml
        <dimen name="widget_margin">0dp</dimen>
    

  4. Aplicar el margen en el layout de nuestro widget:
  5. android:padding="@dimen/widget_margin"
    



5. Crear clase AppWidgetProvider

El ultimo paso para crear nuestro widget sera crear la clase que maneje todos los eventos y configuración del widget. En este caso tenemos que extender la clase "AppWidgetProvider" que a su vez hace de "BroadcastReceiver" permitiendo así manejar todos los eventos del widget.

Esta clase recibe los eventos mas relevantes del widget como puede ser una actualización, eliminación, habilitación y desabilitación.

Los métodos que podemos utilizar para esta clase son los siguientes:
  • onUpdate() - sera llamado cada vez que el widget se actualice a intervalos definidos en el atributo "updatePeriodMillis". También es llamado cada vez que el usuario añada el widget al escritorio por lo que es el lugar indicado para configurar el widget y añadir un servicio si es necesario. Deberemos tener en cuenta que si creamos una activity de configuración, al añadir el widget al escritorio no se llamara al método, aunque si lo hará en las siguientes actualizaciones. Por lo tanto deberemos configurar el widget en la activity de configuración.
  • onAppWidgetOptionsChanged() - sera llamado la primera vez que se coloque el widget en el escritorio y cuando el widget cambie su tamaño.
  • onDeleted() - es llamado cada vez que un widget es eliminado.
  • onEnabled() - sera llamado cuando el usuario añada una instancia del widget al escritorio. Si posteriormente añade mas instancias del mismo widget al escritorio no se llamara a este método.
  • onDisabled() -sera llamado cuando se elimine la ultima instancia de nuestro widget. Es un lugar indicado para limpiar los recursos generados en la clase del widget.
  • onReceive() - es llamado para manejar cualquier estado anterior. Por lo tanto podremos anular todos los métodos anteriores y manejar desde aquí todos los eventos del widget. O incluso podremos crear un BroadcastReceiver personalizado.

Para crear eventos onClick tendremos que utilizar "PendingIntents" y aplicárselos a las vistas de diseño a través del método "setOnClickPendingIntent()". Para crear un PendingIntent podremos utilizar los siguientes métodos: 
  • getActivity() - iniciara una nueva activity de igual manera que lo hace "startActivity()".
  • getBroadcast() - llevara a cabo una emisión de igual manera que"sendBroadcast()".
  • getService() - iniciara un nuevo servicio al igual que lo hace "startService()".

Vamos a ver un ejemplo sencillo para crear un widget:
public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

        for (int i=0; i < appWidgetIds.length; i++) {

            Intent intent = new Intent(context, MainActivity.class);
            PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent, 0);

            RemoteViews v = new RemoteViews(context.getPackageName(), R.layout.widget);
            v.setOnClickPendingIntent(R.id.button, pIntent);

            appWidgetManager.updateAppWidget(appWidgetIds[i], views);
        }
    }
}
Este widget tiene un layout llamado "widget" que contiene un único botón.

Dentro del método "onUpdate()" se crea un bucle for para configurar todas las instancias del widget que haya en el escritorio, es decir, si se añade dos veces el widget al escritorio el bucle for se encarga de configurar las dos instancias del widget. Por lo tanto cada widget tendrá su identificador "appWidgetIds".

Lo siguiente es configurar un PendingIntent que se encargara de comenzar una activity. Para ello creamos el Intent con la activity y se lo aplicamos al PendingIntent a través del método "getActivity()".

Ahora necesitaremos aplicar ese PendingIntent a nuestro botón. Para ello primero creamos una instancia "RemoteViews" especificando el layout de nuestro widget. Una vez creada le aplicamos el PendingIntent al botón a través del método "setOnClickPendingIntent()" indicando en los parámetros la id del botón y el PendingIntent.

Para concluir el método deberemos actualizar el widget con la nueva configuración. Esto lo podemos realizar con un objeto "AppWidgetManager" y su método "updateAppWidget()". Indicando en los parámetros el identificador del widget y su nueva vista RemoteView.



6. Sobreescribir el método onReceive

Como se comentaba en el punto anterior, tenemos la posibilidad de anular los métodos mas importantes de la clase AppWidgetProvider y poder gestionar cada evento del widget a través del método "onReceive()". También existe la posibilidad de crear un BroadcastReceiver personalizado.

Vamos a ver como se podría utilizar el método onReceive para gestionar el widget:
public void onReceive(Context context, Intent intent) {
  
    String accion = intent.getAction();
  
    if (accion.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
        // Maneja el metodo onUpdate
    } else if (accion.equals(AppWidgetManager.ACTION_APPWIDGET_ENABLED)) {
        // Maneja el metodo onEnabled
    } else if (accion.equals(AppWidgetManager.ACTION_APPWIDGET_DISABLED)) {
        // Maneja el metodo onDisabled
    } else if (accion.equals(AppWidgetManager.ACTION_APPWIDGET_DELETED)) {
        // Maneja el metodo onDelete
    } else if (accion.equals(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED)) {
        // Maneja el metodo onAppWidgetOptionsChanged
    }
}
Simplemente se recoge la acción del intent y se compara con las que ofrece la clase "AppWidgetManager".



7. Crear Activity de configuración

De manera opcional podemos crear una Activity para configurar el widget que sera lanzada en el momento que el usuario arrastre nuestro widget al escritorio. Deberemos configurar nuestro widget dentro de la activity porque la primera vez que se cree el widget en el escritorio el método "onUpdate()".

Lo primero es declarar la activity en el AndroidManifest aplicandole un filtro que indicara que es una activity de configuración de widget:
<application .... >
    ....
    ....
        
    <activity android:name=".MiActivityConfigure">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
        </intent-filter>
    </activity>
        
</application>

Partiendo del ejemplo anterior en el que teníamos un widget con un único botón, vamos a ver como se podría configurar todo desde la activity:
public class MiConfigure extends Activity implements OnClickListener {
 
    int idWidget = 0;
 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.configure_widget);
  
        setResult(RESULT_CANCELED);
        
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            idWidget = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 
                                      AppWidgetManager.INVALID_APPWIDGET_ID);
 }
  
        Button aceptar = (Button)findViewById(R.id.acptar);
        aceptar.setOnClickListener(this);
        Button cancelar = (Button)findViewById(R.id.cancelar);
        cancelar.setOnClickListener(this);
    }
 
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.acptar:
            AppWidgetManager appWM = AppWidgetManager.getInstance(MiConfigure.this);
    
            configuraWidget(getApplicationContext(), appWidgetManager, idWidget);
    
            Intent resultValue = new Intent();
            resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, idWidget);
            setResult(RESULT_OK, resultValue);
            finish();
            break;
   
        case R.id.cancelar:
            setResult(RESULT_CANCELED);
            finish();
            break;
        }
    }
 
    private void configuraWidget(Context c, AppWidgetManager appWM, int idWidget) {

        Intent intent = new Intent(c, MainActivity.class);
        PendingIntent pIntent = PendingIntent.getActivity(c, 0, intent, 0);

        RemoteViews v = new RemoteViews(c.getPackageName(), R.layout.widget);
        v.setOnClickPendingIntent(R.id.button, pIntent);

        appWM.updateAppWidget(idWidget, v);

    }
 
}
Esta activity tiene como layout "configure_widget" que contiene dos botones, uno para aceptar la configuración y otro para cancelar el proceso. Comentar que se establece el método "setResult(RESULT_CANCELED)" al principio del onCreate para que se cancele el proceso si el usuario pulsa la tecla back del dispositivo.

Los pasos mas importantes para crear esta Activity de configuración son los siguientes:
  1. Obtener la ID del widget - En el ejemplo estamos recuperando la id del widget y almacenándola en la variable "idWidget".
  2. Configurar diseño de la Activity - Simplemente aplicar el layout y configurar todos sus puntos de vista así como las acciones que tendrá cada uno.
  3. Crear una instancia de AppWidgetManager - Dicha instancia nos servirá para configurar el widget, en el ejemplo la tenemos situada justo al principio del primer caso del swtich.
  4. Actualizar widget - Este paso consiste en configurar todos los elementos del widget, en el ejemplo hemos creado el método "configurarWidget".
  5. Devolver un resultado - Para terminar la Activity deberá devolver un resultado, en este caso devolvemos un resultado por defecto al principio de la activity, devolvemos otro resultado al pretar el botón aceptar y por ultimo otro resultado al pretar el botón cancelar.



8. Crear imagen de previsualización

Este punto describe como se puede crear una imagen del widget para utilizarla como previsualización en la pestaña widgets del dipositivo.

Para conseguir una captura de nuestro widget podemos utilizar la aplicacion "Widget Preview" directamente en el emulador Android o descargandola desde GooglePlay: Widget Preview.



9. Habilitar widget en el LockScreen

A partir de Android 4.2 es posible mostrar un widget en la pantalla de bloqueo. Simplemente tendremos que indicarlo en el archivo de configuración xml del widget a través del atributo "widgetCategory" con una de las siguientes posibilidades:

  • "home_screen" - Widget de pantalla de inicio.
  • "keyguard" - Widget de pantalla de bloqueo.
  • "home_screen | keyguard" - Widget para los dos casos.

Si no se establece dicho atributo, Android por defecto configura el widget para que sea de pantalla de inicio.

Si declaramos el widget para los dos casos, podremos indicar mediante programación diferentes diseños para el widget (uno para la pantalla de bloqueo y otro para la pantalla de inicio). Este proceso requiere un nivel de API 16 o superior.

Para realizarlo simplemente tendremos que llamar al método "getAppWidgetOptions()" y preguntarle donde se esta añadiendo el widget. Con las siguientes lineas de código podremos aplicarle un diseño u otro:
Bundle myOptions = appWidgetManager.getAppWidgetOptions(idWidget);
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1);
boolean isKeyguard = (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD);
int layout = isKeyguard ? R.layout.widget_keyguard : R.layout.widget_home_screen;
El resultado final se almacena en la variable "layout" que sera el identificador del layout que habrá que aplicarle al objeto "RemoteViews".

Otro aspecto a tener en cuenta es que al añadir el widget en la pantalla de bloqueo, por defecto siempre llena el ancho del widget. Si necesitamos llenar el alto del widget deberemos establecer en el archivo de configuración xml del widget el atributo "resizeMode" a "vertical".



10. Mini aplicación widgets

Todo lo explicado en el articulo esta en forma de aplicación que podemos descargar en el siguiente enlace:

DESCARGAR: EJEMPLO WIDGETS


10.1 Ejemplo extra: Utilizar una colección

La aplicación de ejemplo contiene un ejemplo extra que consiste en mostrar una colección de objetos estilo al widget de YouTube. Este punto se centra en hacer un pequeño resumen de como lograrlo.

A partir de Android 3.0 es posible mostrar una colección de objetos dentro de un widget. Para ello es necesario utilizar la clase "RemoteViewsService" que admite los siguientes diseños de vista:
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

Se da por hecho que necesitaremos un Adaptador para mostrar la colección de objetos, en este caso se utiliza la clase "RemoteViewFactory" que sera la encargada de crear y devolver la colección de objetos.

  • RemoteViewsService - consiste en un servicio que permite a un adaptador remoto solicitar objetos RemoteViews.
  • RemoteViewsFactory - es una interface que nos permite crear un adaptador con una colección de datos y un diseño de vista.

La vista "StackView" consiste en mostrar un conjunto de vistas de manera que podemos desplazarnos entre ellas para mostrar una vista anterior o posterior.

El widget avanza por la colección de vistas mostradas en el StackView gracias a establecer el atributo "autoAdvanceViewId" en su archivo de configuración xml.

En el ejemplo al pulsar un ítem se nos mostrara un Toast con la id del ítem de la colección.

Los pasos mas importantes son los siguientes:

  • Declarar el servicio "RemoteViewsService" con el permiso "BIND_REMOTEVIEWS". De esta manera evitamos que el resto de aplicaciones acceda a los datos del widget.

  • Crear el archivo de configuración xml del widget.

  • Crear el diseño que mostrara un item de la colección. Este diseño sera el utilizado por el adaptador para crear la colección de objetos.

  • Crear la clase "AppWidgetProvider" y establecer el método "setRemoteAdapter()" que nos servirá para indicar el adapter con la colección de datos.

  • Crear la clase "RemoteViewsService" implementando la interface "RemoteViewsFactory". Este servicio es el que utilizara el adaptador para solicitar RemoteViews. Los métodos mas importantes de esta interface son: onCreate() (Lugar indicado para configurar las conexiones y/o cursores con la fuente de datos) y getViewAt() (Devuelve un objeto RemoteView con sus datos).

  • Crear evento para controlar el "onClick" de un ítem. En este caso se utiliza el método "setOnClickFillInIntent()".











2 comentarios:

  1. Hola, gracias por el tutorial, pero tengo una duda:

    appWidgetManager.updateAppWidget(appWidgetIds[i], views);

    De donde sale la variable views?

    ResponderEliminar