Las preferencias de una aplicación Android permiten que los usuarios configuren opciones del funcionamiento básico, como la frecuencia de sincronización, los estilos, datos de cuentas, uso de notificaciones, etc.
Para explicar la inclusión de preferencias en una aplicación android he decidido crear un tutorial que te sirva de guía para generar una buena actividad que se dedique a esta tarea a través de un ejemplo ilustrativo…
Descargar Proyecto Android Studio De «Revista Tempor»
…el siguiente video muestra el resultado final de lo que aprenderás si sigues hasta el final este tutorial:
Para desbloquear el link de descarga del código completo, sigue estas instrucciones:
[sociallocker id=»7121″][/sociallocker]
La Actividad De Preferencias En Android
La actividad de ajustes es una parte fundamental para las aplicaciones Android. Uno de los principios de diseño más importante del diseño Android establece que el usuario debe sentir que tiene el control de la aplicación y que puede darle toques personalizados.
Cuando un usuario modifica un ajuste, este cambio se conserva como una Preferencia (un par clave -valor) hasta que decida cambiarlo de nuevo. Por lo que no debe preocuparse por configurar la aplicación en cada movimiento dentro de ella.
¿Dónde Se ubica La Sección De Ajustes?
Por convención siempre han estado ubicados en el action overflow de la Action Bar. Si has notado, Android Studio cuando genera una nueva actividad, crea un ítem de menú llamado Settings para ubicar los ajustes.
Este jamás debe aparecer visible en la action bar, por eso tiene la característica never en su visualización. Además debe ser la última prioridad en el action overflow y es por eso que tiene su atributo ordenInCategory en 100.
No obstante, si deseas incluir una sección de ayuda para tu app, por convención Ajustes será la antepenúltima opción y Ayuda la última.
Incluso es viable incluir los ajustes en el patrón Navigation Drawer.
Tipos De Preferencias En Android
Cada uno de los ajustes se representa lógicamente con un objeto de la clase Preference, la cual representa un ítem de la lista de preferencias.
Obviamente esta clase no se implementa directamente para generar preferencias de distintos estilos visuales y con diferentes valores. Existen subclases escritas para que podamos acceder a patrones de diseño en forma de checkbox, listas, edit text, etc. También podemos crear nuestro propio diseño de preferencias.
Veamos tres tipos de preferencias vitales:
- CheckBoxPreference: Muestra un checkbox para proporcionarle al usuario dos opciones disponibles de acción.
- ListPreference: Despliega un dialogo con varias opciones para elegir como preferencia.
- EditTextPreference: A través de un diálogo toma el valor que el usuario digite en un campo de texto.
Cada preferencia maneja un esquema con pares clave-valor para definir las decisiones del usuario. La persistencia de esta información se da a través de un archivo llamado Shared Preferences.
Si deseamos acceder a este archivo debemos usar la clase SharedPreferences para obtener una vía de comunicación hacia cada par clave-valor almacenado.
SharedPreferences puede guardar cualquier de los siguientes tipos de datos para los valores de las preferencias:
- Booleanos
- Enteros
- Flotantes
- Longs
- Strings
- Conjuntos Set de la clase String (Set<String>)
Compatibilidad En Versiones De Android
Antes de implementar las preferencias para tus ajustes debes pensar hasta donde deseas mantener la compatibilidad de esta característica.
Para ello tienes dos opciones: o utilizar la clase PreferenceActivity para APIs de la 10 hacia abajo o emplear un enfoque de fragmentos con la clase PreferenceFragment para mayores o iguales a la 11. Por lo que esta cuestión ya depende de ti.
Existe un caso específico donde PreferenceActivity puede usarse en versiones superiores y es cuando deseamos implementar un patrón maestro-detalle. Lo cual veremos más adelante del artículo
1.Requerimientos De La Aplicación Android
¿Recuerdas la aplicación RevistaTempor del artículo sobre el SwipeRefreshLayout?
Esta aplicación cargaba una lista ficticia de artículos de un sitio web y cada vez que usábamos un gesto Swipe vertical actualizaba la información.
Lo que haremos ahora es extender su comportamiento para que el usuario pueda cambiar las siguientes configuraciones:
- Como usuario, deseo que sea opcional la visualización de las miniaturas en cada artículo de la lista.
- Como usuario, deseo limitar la cantidad de artículos que aparecerán en la lista.
- Como usuario, deseo sincronizar mi cuenta de Twitter con la aplicación.
Antes de continuar validaremos si cada una de estas posibles configuraciones puede ser una preferencia. Con este fin en mente acudiremos a una interesante plantilla que ha creado Google en lo documentación. Podemos verla en la siguiente ilustración:
Básicamente lo que sugiere este diagrama es que algún aspecto se convierte un ajuste si:
- El usuario no lo modifica con frecuencia
- No se trata de información estática que podría ubicarse en la sección de ayuda.
- Satisface a la mayoría de usuarios pero puede afectar a la minoría. Esto indica que existen distintas necesidades para otros usuarios, por lo que debes dejar que cada quien elija.
Hagamos la prueba con el segundo requerimiento:
—¿El usuario lo modifica con frecuencia?
R/ No, es eventual.
—¿Es información estática?
R/ No, es una decisión.
—¿Afecta a la minoría?
R/ Si, habrá usuarios que no deseen ver miniaturas.
—Si reflexionas aplicando esta misma lógica a los demás requerimientos, verás que también son potenciales ajustes.
2. Wireframing De La Aplicación Android
Una vez teniendo claro lo que vamos a implementar, entonces podemos bocetar como luciría el comportamiento de la aplicación incluyendo los ajustes:
Para el primer ajuste usaremos una preferencia estilo checkbox para decidir entre la aparición de las miniaturas o no al frente de cada ítem de la lista.
La cantidad máxima de ítems podemos representarla a través de una preferencia estilo lista para determinar 4 valores fijos: seis, ocho, doce y dieciséis respectivamente.
Ten en cuenta que es mejor predeterminar el límite de los valores para evitar que el usuario reduzca el rendimiento de inflado y búsqueda en la lista con valores muy altos.
El ajuste de la cuenta de twitter debemos incluirlo en otro grupo ya que necesitamos el usuario y la contraseña como dos preferencias separadas. Ambas deben ser con estilo EditText para permitir al usuario digitar su información personalizada.
No crearemos un servicio para la conexión a la interfaz de Twitter, estas preferencias son educativas.
3. Diseño De UI Para La Aplicación Android
El diseño de las preferencias se realiza a través del enfoque de inflado de una definición XML al igual que con los layouts, ya que estamos poblando implícitamente un ListView.
Las tareas a realizar para diseñar nuestra sección de Ajustes son las siguientes:
- Crear un archivo XML para las preferencias.
- Añadir cada preferencia identificada en los requerimientos.
- Crear un archivo de strings únicamente para los textos de las preferencias y sus valores.
- Agrupar ajustes si se requiere (8 o más requieren agrupación).
Crear Un Archivo XML Para Las Preferencias
Crea una nueva carpeta de recursos xml en Android Studio haciendo click derecho en la carpeta res y luego seleccionando Android resource directory.
Esto invocará el wizard de creación de directorios de recursos. Solo debes seleccionar el valor xml en la opción Resource Type y confirmar con el botón OK.
Ahora ve a la nueva carpeta xml, presiona click derecho, selecciona la opción New y ve a XML resource file.
En el primer campo File Name escribe el nombre de tu archivo de preferencias. Puedes usar el nombre que quieras. Yo usaré el nombre settings.xml.
Si todo salió bien, verás que tu archivo tiene un nodo raíz del tipo <PreferenceScreen>
.
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> </PreferenceScreen>
La etiqueta <PreferenceScreen> representa la lista de preferencias que será inflada en la actividad de ajustes.
Añadir Preferencias Al Archivo settings.xml
Ahora agregaremos la etiqueta correspondiente a cada uno de las preferencias identificadas en un orden establecido.
Cada preferencia es representada por una etiqueta equivalente a su clase java original. Por ejemplo <CheckBoxPreference>, <ListPreference> y <EditTextPreference>.
La primera preferencia es la activación/desactivación de las miniaturas. Para ello usamos la etiqueta <CheckBoxPreference> de la siguiente manera:
<!-- Visibilidad de Miniaturas de las Entradas --> <CheckBoxPreference android:key="miniaturas" android:title="@string/pref_miniaturas" android:defaultValue="true" />
Ahora mira el significado de los atributos más comunes para las preferencias:
key
: Representa la clave de la preferencia que se usará en el almacenamiento dentro del archivo Shared Preferences.title
: Es la cadena que verá el usuario en la aplicación como título principal.summary
: Aquí puedes escribir un resumen sobre el estado de la preferencia.defaultValue
: Es el valor por defecto que tendrá la preferencia cuando se inicie por primera vez la aplicación o en caso de que se requiera un reinicio. Como ves, asigné true como valor predeterminado para que se muestren las miniaturas al inicio.
Estos atributos son comunes para todas las preferencias. Sin embargo existen atributos que son propios de cada estilo dependiendo de su estilo.
Veamos cómo declarar la preferencia de la cantidad máxima de elementos de la lista a través de la etiqueta <ListPreference>:
<!-- Cantidad máxima de entradas --> <ListPreference android:key="numArticulos" android:title="@string/pref_numArticulos_titulo" android:defaultValue="6" android:entries="@array/pref_numArticulos_entradas" android:entryValues="@array/pref_numArticulos_valores" android:summary="@string/pref_numArticulos_resumen" />
Esta vez tenemos atributos especiales para una preferencia con estilo de lista:
- entries: Representa cada uno de los nombres que verá el usuario para las opciones.
- entryValues: Valores para cada opción contenida en entries.
Ahora veamos cómo declarar la preferencia para establecer el password y el usuario de twitter:
<!-- Usuario de Twitter --> <EditTextPreference android:key="usuario_twitter" android:title="@string/pref_usuario_titulo" android:summary="@string/pref_usuario_resumen" android:selectAllOnFocus="true" android:singleLine="true" /> <!-- Password de Twitter --> <EditTextPreference android:key="contraseña_twitter" android:title="@string/pref_contraseña_titulo" android:summary="@string/pref_contraseña_resumen" android:selectAllOnFocus="true" android:inputType="textWebPassword" android:singleLine="true" />
Cada <EditTextPreference> puede poseer los siguientes atributos especiales:
- selectAllOnFocus: Muestra seleccionado todo el texto en el edit text al iniciar el diálogo.
- inputType: Controla el comportamiento del teclado dependiendo de la bandera usada. Para enmascarar con asteriscos los caracteres de la contraseña puedes usar textWebPassword.
- singleLine: Establece que si el EditText debe aceptar una sola línea de texto (true). Adicionalmente dirige el foco hacia los botones de acción por si presionas el botón de confirmación en el teclado.
Crear Archivo De Strings Para Las Preferencias
La definición XML de todas las preferencias vistas en la tarea anterior se basan en ítems de un archivo de strings.xml que aún no hemos visto cómo se constituye.
Separar este archivo de los strings corrientes nos permitirá tener una legibilidad mucho más comprensiva de la jerarquía de preferencias.
A continuación ve a tu carpeta values, presiona click derecho, ve a la opción New > Values resource file. Pon el nombre de strings_settings.xml y define cada una de las cadenas necesarias para títulos, resumen, valores, etc.
Veamos cómo queda el archivo de RevistaTempor:
<resources> <!-- Nombre Actividad Ajustes--> <string name="title_activity_settings">Configuración</string> <!-- Titulos de Grupos--> <string name="pref_gen">General</string> <string name="pref_twitter">Twitter</string> <!-- GENERALES --> <!-- Preferencia Miniaturas --> <string name="pref_miniaturas">Mostrar Miniaturas</string> <!-- Preferencia Número de Articulos --> <string name="pref_numArticulos_titulo">Número de Artículos</string> <string-array name="pref_numArticulos_entradas"> <item>6</item> <item>8</item> <item>12</item> <item>16</item> </string-array> <string-array name="pref_numArticulos_valores"> <item>6</item> <item>8</item> <item>12</item> <item>16</item> </string-array> <string name="pref_numArticulos_resumen">Número máximo de artículos en la lista</string> <!-- Preferencias Cuenta Twitter --> <string name="pref_usuario_titulo">Usuario</string> <string name="pref_usuario_resumen">Tu nombre de usuario en Twitter</string> <string name="pref_contraseña_titulo">Contraseña</string> <string name="pref_contraseña_resumen">Tu Contraseña de Twitter</string> </resources>
Agrupar Ajustes Por títulos O Subscreens
En el momento que la cantidad de ajustes sea mayor a 7 elementos debes considerar agruparlas.
Una de las formas de agrupación es a través de separadores de sección, los cuales se componen de un título más una línea divisoria que representa una categoría. Es ideal para agrupar entre 8 y 15 elementos. Por ejemplo la siguiente imagen muestra tres categorías en la sección de ajustes:
Otro método es el uso de subscreens (será el método que usaremos para el resultado final), el cual es muy útil para simplificar un número mayor a 16 ajustes. Se trata de incluir una nueva actividad por cada grupo que exista en los ajustes. En la siguiente imagen vemos como del grupo Date & Time se desprende una nueva pantalla para ver los ajustes interiores:
Aunque nuestro ejemplo solo tiene 4 ajustes, usaremos la agrupación por cada método para que puedas implementarla en tus futuros proyectos.
Para ello crearemos dos grupos, uno para las características generales y otro para los datos de la cuenta de twitter.
IMPLEMENTAR DIVISORES DE SECCIÓN
La implementación de divisores de sección se realiza a través de la etiqueta <PreferenceCategory>. Esta representa una categoría contenedora de preferencias, donde solo debemos incluir como elementos hijos todas las preferencias que pertenezcan a ella.
Las categorías necesitan que declaremos un atributo title para el título de la sección y un atributo key para almacenamiento en el archivo SharedPreferences.
Veamos cómo hacerlo:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <!-- PREFERENCIAS GENERALES --> <PreferenceCategory android:key="pref_gen" android:title="@string/pref_gen"> <!-- Visibilidad de Miniaturas de las Entradas --> <CheckBoxPreference android:key="miniaturas" android:title="@string/pref_miniaturas" android:defaultValue="true" /> <!-- Cantidad máxima de entradas --> <ListPreference android:key="numArticulos" android:title="@string/pref_numArticulos_titulo" android:defaultValue="6" android:entries="@array/pref_numArticulos_entradas" android:entryValues="@array/pref_numArticulos_valores" android:summary="@string/pref_numArticulos_resumen" /> </PreferenceCategory> <PreferenceCategory android:key="pref_twitter" android:title="@string/pref_twitter"> <!-- Usuario de Twitter --> <EditTextPreference android:key="usuario_twitter" android:title="@string/pref_usuario_titulo" android:summary="@string/pref_usuario_resumen" android:selectAllOnFocus="true" android:singleLine="true" /> <!-- Password de Twitter --> <EditTextPreference android:key="contraseña_twitter" android:title="@string/pref_contraseña_titulo" android:summary="@string/pref_contraseña_resumen" android:selectAllOnFocus="true" android:inputType="textWebPassword" android:singleLine="true" /> </PreferenceCategory> </PreferenceScreen>
IMPLEMENTAR SUBSCREENS
Para usar subscreens solo usamos etiquetas hijas <PreferenceScreen> dentro del elemento raíz. Con esta convención, se crearán automáticamente las actividades o fragmentos necesarios para navegar a los ajustes agrupados.
En ese caso solo reemplazamos las categorías por subscreens de la siguiente forma:
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <!-- PREFERENCIAS GENERALES --> <PreferenceScreen android:key="pref_gen" android:title="@string/pref_gen"> <!-- Visibilidad de Miniaturas de las Entradas --> <CheckBoxPreference android:key="miniaturas" android:title="@string/pref_miniaturas" android:defaultValue="true" /> <!-- Cantidad máxima de entradas --> <ListPreference android:key="numArticulos" android:title="@string/pref_numArticulos_titulo" android:defaultValue="6" android:entries="@array/pref_numArticulos_entradas" android:entryValues="@array/pref_numArticulos_valores" android:summary="@string/pref_numArticulos_resumen" /> </PreferenceScreen> <PreferenceScreen android:key="pref_twitter" android:title="@string/pref_twitter"> <!-- Usuario de Twitter --> <EditTextPreference android:key="usuario_twitter" android:title="@string/pref_usuario_titulo" android:summary="@string/pref_usuario_resumen" android:selectAllOnFocus="true" android:singleLine="true" /> <!-- Password de Twitter --> <EditTextPreference android:key="contraseña_twitter" android:title="@string/pref_contraseña_titulo" android:summary="@string/pref_contraseña_resumen" android:selectAllOnFocus="true" android:inputType="textWebPassword" android:singleLine="true" /> </PreferenceScreen> </PreferenceScreen>
Para optimizar la creación de subscreens en versiones superiores a la versión 3.0 es recomendable usar otro tipo de agrupamiento llamado agrupamiento por encabezados. Lo verás en la otra sección.
3. Codificación De Las Preferencias En La Aplicación
Al inicio mencioné que las preferencias se implementan dependiendo de la versión de Android. Si deseas soportar dispositivos con Android 3.0 hacia abajo, entonces se usa una actividad especial para preferencias llamada PreferenceActivity.
Por otro lado, si deseas soportar solo dispositivos de la versión 3.0 hacia arriba, entonces usas el patrón de fragmentos con la subclase PreferenceFragment. Esta clase no hace parte de la librería de soporte, así que no funcionará con versiones inferiores.
La causa por la que Google desea que los desarrolladores usen las preferencias con fragmentos, se debe a la versatilidad que proporcionan al agruparse las preferencias. Es mucho mejor que cada subscreen se genere por reemplazos de fragmentos que creando varias actividades por pantalla.
Además los fragmentos permiten crear patrones maestro-detalle en los dispositivos móviles con dimensiones largas como las tablets.
Crear Una Actividad De Preferencias En Android
Este no es el enfoque que deseo usar para mostrarte el resultado final de la aplicación RevistaTempor. Sin embargo me parece importante que aquellas personas que lo necesiten puedan tener acceso a un ejemplo.
Para crear la actividad solo debemos ir a nuestro paquete java, dar click derecho, seleccionar New y presionar Java Class. Puedes usar el nombre de SettingsActivity para el archivo.
Esta actividad debe heredar de PreferenceActivity y en vez de inflar su vista desde un layout con setContentView(), se infla con el método addPreferencesFromResource().
Veamos:
import android.preference.PreferenceActivity; import android.os.Bundle; public class SettingsActivity extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.settings); } }
Probablemente el método addPreferencesFromResource() aparecerá subrayado indicando que está obsoleto debido a la API que se está usando.
Ahora ve a tu actividad principal y sobrescribe el método onOptionsItemSelected() para que se inicie la actividad de ajustes cuando se presione el botón action_settings que se encuentra en tu archivo menú_main.xml(O el nombre que hayas usado).
@Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings) { startActivity(new Intent(this, SettingsActivity.class)); return true; } return super.onOptionsItemSelected(item); }
Crear Un Fragmento De Preferencias
La creación de un fragmento de preferencias requiere que tengamos una actividad en la cual se añadirá su contenido. Por lo que debes crear una actividad de preferencias que extienda de ActionBarActivity o la clase Activity.
Esta actividad no requiere un layout para inflar su contenido, por ende tampoco implementa el método setContentView().
import android.app.FragmentTransaction; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; public class SettingsActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.add(android.R.id.content, new SettingsFragment()); ft.commit(); } }
El fragmento se añade de forma normal pero se debe adjuntar al contenido principal de la aplicación refiriéndonos al identificador general android.R.id.content.
Lo que sigue es crear un nuevo fragmento que extienda de la clase PreferenceFragment. Lo llamaremos SettingsFragment y simplemente añadiremos el método addPreferencesFromResource() en onCreate() para la visualización. Debido a esto, no es necesario sobrescribir nada en el método onCreateView():
import android.annotation.TargetApi; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceFragment; @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class SettingsFragment extends PreferenceFragment { public SettingsFragment() { // Constructor Por Defecto } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.settings); } }
Ahora solo configura las acciones del botón action_settings e inicia la actividad contenedora SettingsActivity. Con ello obtendremos el mismo resultado que cuando creamos la PreferenceActivity.
Establecer Los Valor Por Defecto Al Iniciar La Aplicación
Es importante asegurar que en la primera ejecución de la aplicación para cada dispositivo móvil las preferencias sean ajustadas a los valores predeterminados que decidimos, para cubrir el perfil de la mayoría de usuarios.
Por consiguiente llamaremos al método estático setDefaultValues() de la clase PreferenceManager(administrador del inflado y almacenamiento de preferencias) en el onCreate() de tu actividad principal.
Este método guardará todos los valores de los atributos defaultValue de las preferencias en SharedPreferences.
// Cargar valores por defecto PreferenceManager.setDefaultValues(this, R.xml.settings, false);
El primer parámetro del método se refiere al contexto de la Aplicación. El segundo es el archivo donde tenemos la jerarquía de nuestras preferencias y el último determina si se debe ignorar la llamada de este método cuando los valores por defecto ya han sido establecidos (usa false).
Implementar Agrupamiento Con Encabezados
Añadir etiquetas <PreferenceScreen> dentro de otras no es una buena práctica para crear subscreens en versiones mayores o iguales a la 3.0.
El agrupamiento por encabezados aprovecha el poder de los fragmentos para optimizar las subpantallas y además permite crear con facilidad patrones de dos paneles (maestro-detalle) en dispositivos amplios.
Pero… ¿Cómo debes crear este diseño?
Sigue los siguientes pasos:
- Crear un archivo de preferencias por cada grupo.
- Crear una definición XML que contenga los encabezados de cada grupo.
- Derivar la actividad contenedora de PreferenceActivity.
- Sobrescribir el método callback onBuildHeaders() para visualizar los grupos.
CREAR ARCHIVOS DE PREFERENCIAS
Lo primero que haremos será dividir el archivo settings.xml en dos partes diferentes. Uno para el grupo de las preferencias generales y los datos de twitter.
Veamos el archivo settings_gen.xml para las preferencias generales:
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <!-- PREFERENCIAS GENERALES --> <PreferenceCategory android:key="pref_gen" android:title="@string/pref_gen"> <!-- Visibilidad de Miniaturas de las Entradas --> <CheckBoxPreference android:key="miniaturas" android:title="@string/pref_miniaturas" android:defaultValue="true" /> <!-- Cantidad máxima de entradas --> <ListPreference android:key="numArticulos" android:title="@string/pref_numArticulos_titulo" android:defaultValue="6" android:entries="@array/pref_numArticulos_entradas" android:entryValues="@array/pref_numArticulos_valores" android:summary="@string/pref_numArticulos_resumen" /> <!-- Nivel de volumen de los sonidos --> <com.herprogramacion.revistatempor.VolumePreference android:key="pref_vol" android:defaultValue="75"/> </PreferenceCategory> </PreferenceScreen>
El siguiente es el contenido del archivo settings_twitter.xml:
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <!-- DATOS DE TWITTER --> <PreferenceCategory android:key="pref_twitter" android:title="@string/pref_twitter"> <!-- Usuario de Twitter --> <EditTextPreference android:key="usuario_twitter" android:title="@string/pref_usuario_titulo" android:summary="@string/pref_usuario_resumen" android:selectAllOnFocus="true" android:singleLine="true" /> <!-- Password de Twitter --> <EditTextPreference android:key="contraseña_twitter" android:title="@string/pref_contraseña_titulo" android:summary="@string/pref_contraseña_resumen" android:selectAllOnFocus="true" android:inputType="textWebPassword" android:singleLine="true" /> </PreferenceCategory> </PreferenceScreen>
CREAR DEFINICIÓN XML PARA LOS ENCABEZADOS
Ahora crearemos una jerarquía de los encabezados a través de un nodo principal <preference-headers> y los grupos los representaremos con la etiqueta <header>.
<?xml version="1.0" encoding="utf-8"?> <preference-headers xmlns:android="http://schemas.android.com/apk/res/android"> <header android:fragment="com.herprogramacion.revistatempor.SettingsActivity$SettingsFragment" android:title="@string/pref_gen" android:summary="Preferencias Generales" > <extra android:name="settings" android:value="generales"/> </header> <header android:fragment="com.herprogramacion.revistatempor.SettingsActivity$SettingsFragment" android:title="@string/pref_twitter" android:summary="Datos de Twitter" > <extra android:name="settings" android:value="twitter" /> </header> </preference-headers>
Esta definición tiene algo muy especial y es la asociación del fragmento de preferencias a cada elemento <header>.
Se debe establecer la ruta del paquete donde se encuentra el fragmento, ya sea si lo has declarado como clase separada o como una clase anidada dentro de la actividad de preferencias.
En mi caso he declarado un fragmento estático dentro de SettingsActivity y para indicar esta situación uso el carácter ‘$’.
¿Por qué se usa el mismo fragmento para ambos grupos?
Porque es posible reciclar un fragmento para los grupos que tengamos. Para ello se usa un filtro basado en el valor de la etiqueta <extra>. Esta etiqueta es equivalente a los valores extra que se usan como parámetros en la inicialización de un fragmento.
Si notas, ambos headers tienen un extra con la clave “settings”, pero sus valores son “generales” y “twitter” respectivamente. Así que solo usamos una sentencia if para diferenciarlos.
@TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String settings = getArguments().getString("settings"); if ("generales".equals(settings)) { addPreferencesFromResource(R.xml.settings_gen); } else if ("twitter".equals(settings)) { addPreferencesFromResource(R.xml.settings_twitter); } } }
Los argumentos extras se obtienen con el método local getArguments(). Recuerda que debes usar la clave correspondiente para obtener el valor correcto. Luego de comprobar inflas la subscreen con addPrefrencesFromResource() y el archivo de preferencias correspondiente
IMPLEMENTAR ACTIVIDAD DE PREFERENCIAS
El siguiente paso es configurar nuestra actividad de preferencias. Esta debe extender de la clase PreferenceActivity.
Esta clase tiene algunos métodos obsoletos, pero contiene otros que pueden ayudarnos a implementar los subscreens de forma efectiva en versiones mayores a la 10.
El primero de ellos es onBuildHeaders(), quién será el encargado de incrustar los encabezados en nuestra actividad de preferencias. Dentro de este llamaremos al método loadHeadersFromResource(), el cual recibe la definición xml de los headers y una lista de objetos Header para extraer su contenido:
@Override public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.xml.pref_headers, target); }
Ahora si deseas implementar el patrón maestro detalle debes sobrescribir el método callback onIsMultiPane() para que retorne en un booleano que activará o desactivará esta característica:
@Override public boolean onIsMultiPane() { // Determinar que siempre sera multipanel DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); return ((float)metrics.densityDpi / (float)metrics.widthPixels) < 0.30; }
En el código anterior uso como condición la razón entre la densidad del dispositivo y su ancho. Si es menor a 0,30 se activará el multipanel, de lo contrario no se usará. Esta no es una medida ni método formal. Usa tus propias condiciones según sea la situación.
Veamos cómo queda el código final:
import android.annotation.TargetApi; import android.os.Build; import android.preference.PreferenceActivity; import android.os.Bundle; import android.preference.PreferenceFragment; import android.util.DisplayMetrics; import java.util.List; public class SettingsActivity extends PreferenceActivity { @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.xml.pref_headers, target); } @Override protected boolean isValidFragment(String fragmentName) { // Comprobar que el fragmento esté relacionado con la actividad return SettingsFragment.class.getName().equals(fragmentName); } @Override public boolean onIsMultiPane() { // Determinar que siempre sera multipanel DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); return ((float)metrics.densityDpi / (float)metrics.widthPixels) < 0.30; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String settings = getArguments().getString("settings"); if ("generales".equals(settings)) { addPreferencesFromResource(R.xml.settings_gen); } else if ("twitter".equals(settings)) { addPreferencesFromResource(R.xml.settings_twitter); } } } }
El método isValidFragment() comprueba si el nombre de los fragmentos relacionados en la definición de los headers es correcto. Por eso usamos getName() para comprobar con el parámetro de entrada fragmentName.
Si no sobrescribes este método se arrojará una excepción en tiempo de ejecución.
Obtener Preferencias Actuales De La Aplicación
Ya habiendo establecido todas las características de nuestras preferencias, solo queda realizar los cambios necesarios al contenido de la aplicación basados en ellas.
Las preferencias de RevistaTempor afectan la forma en que el adaptador infla el recycler view que se está usando para la lista de artículos.
Por esta razón debes modificar el adaptador para que se apliquen los cambios. En cuanto a la preferencia de las miniaturas hallé una solución al incluir un atributo booleano que active y desactive el ImageView que representa la miniatura:
public class ListaAdapter extends RecyclerView.Adapter<ListaAdapter.RevistaViewHolder> { private List<Lista> items; /* Habilita o Desahabilita la aparición de miniaturas */ private boolean conMiniaturas; ... public ListaAdapter(List<Lista> items, boolean conMiniaturas) { this.items = items; this.conMiniaturas = conMiniaturas; } ... @Override public void onBindViewHolder(RevistaViewHolder viewHolder, int i) { if (conMiniaturas) { viewHolder.imagen.setVisibility(View.VISIBLE); viewHolder.imagen.setImageResource(items.get(i).getIdImagen()); } else viewHolder.imagen.setVisibility(View.GONE); viewHolder.titulo.setText(items.get(i).getTitulo()); viewHolder.votos.setText("Votos: " + String.valueOf(items.get(i).getVotos())); } public void setConMiniaturas(boolean conMiniaturas) { if (conMiniaturas != this.conMiniaturas) { this.conMiniaturas = conMiniaturas; // Notificar que las miniaturas fueron afectadas notifyDataSetChanged(); } } }
Como ves, antes de asignar el contenido al elemento imagen del ViewHolder se comprueba el valor del atributo conMiniaturas. Si es verdadero, entonces se restaura la visibilidad con la bandera View.VISIBLE, de lo contrario desaparece con View.GONE.
Ahora debemos cargar las preferencias en el método onCreate() de la actividad principal. Luego obtener la preferencia que representa las miniaturas y asignar este valor al adaptador.
// Procesar valores actuales de las preferencias. SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); boolean miniaturasPref = sharedPref.getBoolean("miniaturas", true); ... // Crear un nuevo adaptador adapter = new ListaAdapter(ConjuntoListas.randomList(cantidadItems), miniaturasPref);
El código anterior muestra cómo se obtienen todas las preferencias actuales de la aplicación con el método estático getDefaultSharedPreferences() que nos brinda el PreferenceManager. El método recibe el contexto y retorna en un objeto SharedPreferences.
Para retornar el valor de una preferencia usamos métodos get*() , el cual obtiene el tipo de datos necesario. Como la preferencia de las miniaturas es booleana, entonces usamos getBoolean().
Si te fijas bien, el primer parámetro es la clave de la preferencia y el segundo es un valor por defecto, por si la preferencia no existe.
En el otro caso para la preferencia que varía la cantidad de ítems mostrados, se trata de forma similar a la anterior. Obtenemos el valor de la preferencia numMiniaturas para el constructor del adaptador.
cantidadItems = Integer.parseInt(sharedPref.getString("numArticulos", "8"));
Ahora, si deseamos percibir los cambios en tiempo real luego de que se hayan modificado las preferencias, entonces actualizamos el contenido del adaptador en el método onResume():
@Override protected void onResume() { super.onResume(); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); // Actualizar visibilidad de miniaturas boolean miniaturasPref = sharedPref.getBoolean("miniaturas", true); adapter.setConMiniaturas(miniaturasPref); // Actualizar cantidad de items cantidadItems = Integer.parseInt(sharedPref.getString("numArticulos", "8")); updateAdapter(ConjuntoListas.randomList(cantidadItems)); }
Recuerda que la actividad principal pierde el foco cuando el usuario se dirige a la actividad de preferencias. Pero sabemos que antes de visualizar de nuevo, esta debe pasar por onResume() como se ha establecido en su ciclo de vida.
Registrar Escucha Para Los Cambios De Preferencias
Por cuestiones de diseño es importante actualizar el estado (summary) de algunas preferencias cuando el usuario cambia su valor. Debido a que esta gestión debe ser en tiempo real, es necesario tener una escucha que nos provea la oportunidad de actuar cuando dicha situación suceda.
Así que usaremos la escucha SharedPreference.OnSharedPreferenceChangeListener sobre nuestro fragmento de preferencias para el manejo de eventos de cambio.
Solo debes sobrescribir el método callback onSharedPreferenceChanged() para dictaminar las acciones que actualicen el estado. Veamos cómo hacerlo:
public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener{ … @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d(TAG, "Escucha de cambios"); if(key.equals("numArticulos")){ Preference preference = findPreference(key); preference.setSummary(sharedPreferences.getString(key,"")); } } … }
onSharedPreferenceChanged() tiene dos parámetros. El primero es un acceso al archivo SharedPreference y el segundo es la clave de la preferencia que se cambió.
Lo primero que debes hacer es comprobar si el valor de key es igual a la clave de la preferencia que buscas. En nuestro caso es «numArticulos».
Crea un objeto Preference y luego usa el método findPrefence() para encontrar la preferencia por su clave. Luego actualiza su atributo de resumen con setSummary(). En este caso puse como estado el valor actual seleccionado en la lista.
La guía de diseño de Android expone como buena práctica colocar el estado actual en el resumen de las preferencias con listas o edit texts.
Si intentas correr el proyecto verás que aún no actualiza el contenido. Esto se debe a que aún no se ha registrado la escucha como tal. Para ello usamos el método registerOnSharedPreferenceChangeListener en onResume(). Y a su vez debemos eliminar el registro con unregisterOnSharedPreferenceChangeListener en onPause().
@Override public void onResume() { super.onResume(); // Registrar escucha getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { super.onPause(); // Eliminar registro de la escucha getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); }
Finalmente el código de la actividad para las preferencias tiene el siguiente aspecto:
import android.annotation.TargetApi; import android.content.SharedPreferences; import android.os.Build; import android.preference.Preference; import android.preference.PreferenceActivity; import android.os.Bundle; import android.preference.PreferenceFragment; import android.util.DisplayMetrics; import java.util.List; public class SettingsActivity extends PreferenceActivity { @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.xml.pref_headers, target); } @Override protected boolean isValidFragment(String fragmentName) { // Comprobar que el fragmento esté relacionado con la actividad return SettingsFragment.class.getName().equals(fragmentName); } @Override public boolean onIsMultiPane() { // Obtener caracteristicas del dispositivo DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); return ((float) metrics.densityDpi / (float) metrics.widthPixels) < 0.30; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Reutilización del fragmento String settings = getArguments().getString("settings"); if ("generales".equals(settings)) { addPreferencesFromResource(R.xml.settings_gen); } else if ("twitter".equals(settings)) { addPreferencesFromResource(R.xml.settings_twitter); } } @Override public void onResume() { super.onResume(); // Registrar escucha getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { super.onPause(); // Eliminar registro de la escucha getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // Actualizar el resumen de la preferencia if (key.equals("numArticulos")) { Preference preference = findPreference(key); preference.setSummary(sharedPreferences.getString(key, "")); } } } }
4. Enfoque Problema-Solución Para Preferencias En Android
Crear Preferencias Personalizadas
Tal vez las subclases de Preference no te permitan representar los ajustes que tú quisieras incorporar en tu aplicación. Así que no queda más que fabricar tus preferencias personalizadas.
Este proceso es similar a cuando se inflan los ítems de una lista a través de un adaptador. Los pasos a tener en cuenta son los siguientes:
- Crea el layout para tu preferencia.
- Crea una nueva clase que extienda de Preference u otra subclase.
- Incorpora la etiqueta correspondiente en tu archivo settings.xml.
Basado en estos tres pasos crearé una preferencia de ejemplo que controla el volumen general de la aplicación. Para ello usaremos un slider del tipo SeekBar para variar su progreso entre 0 y 100 unidades.
CREAR UN LAYOUT PERSONALIZADO PARA UNA PREFERENCIA
Dirígete a tu carpeta res/layout, presiona click derecho y selecciona New > Layout resource file. Elige como nodo principal un RelativeLayout y asígnale el nombre preference_volume.xml y abre su contenido.
Como se ilustró en la imagen anterior, el layout debe componerse de tres elementos. Un TextView que contenga el título “Volumen”, un ImageView para añadir el icono referente al sonido y una SeekBar por debajo del título.
Veamos cómo quedaría:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp"> <!-- Titulo --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/pref_vol_title" android:id="@+id/titulo" android:layout_toRightOf="@+id/icono" /> <!-- Icono --> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/icono" android:layout_alignParentLeft="true" android:src="@mipmap/ic_volume" android:layout_marginRight="10dp" /> <!-- Selector --> <SeekBar android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/selector" android:layout_alignLeft="@+id/titulo" android:layout_below="@+id/titulo" android:progress="75" /> </RelativeLayout>
CREAR CLASE JAVA PARA LA PREFERENCIA PERSONALIZADA
En este punto es importante decidir si deseas usar un diálogo para la interfaz de tu preferencia o si solo deseas un layout normal.
Usar una preferencia con diálogo requiere que extendamos la preferencia de DialogPreference. De lo contrario elige Preference o la subclase que se acomode más a tus necesidades.
Ahora… ¿Qué debe hacer nuestra clase?
Bueno, necesitamos que:
- Almacene el valor por defecto y el actual de forma persistente.
- Infle el layout sobre la preferencia.
- Setee los datos por defecto sobre los views del layout (como el título, resumen, etc.).
- Actualice el valor actual en el momento que se cambie el valor de la SeekBar.
Con estas tareas puestas sobre la mesa vamos a proceder a crear una nueva clase llamada VolumePreference que extienda de Preference.
A esta le añadiremos un atributo variable para el progreso actual de la preferencia. También añadiremos una constante para el valor por defecto y un constructor que reciba el contexto y un conjunto de atributos (AttributeSet):
public class VolumePreference extends Preference { /* Progreso por defecto */ private static final int DEFAULT_PROGRESS = 75; /* Progreso actual */ private int currentProgress; public VolumePreference(Context context, AttributeSet attrs) { super(context, attrs); currentProgress = DEFAULT_PROGRESS; } … }
El siguiente paso es sobrescribir los métodos onCreateView() y onBindView(). Como bien ya sabemos, el primer método permite inflar el layout y el segundo asigna los datos que tenemos a cada view.
@Override protected View onCreateView(ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(R.layout.preference_volume, parent, false); } @Override protected void onBindView(@NonNull View view) { // Setear el progreso actual a la seekbar SeekBar seekBar = (SeekBar)view.findViewById(R.id.selector); seekBar.setProgress(currentProgress); // Setear la escucha al seekbar seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { currentProgress = progress; persistInt(currentProgress); } @Override public void onStartTrackingTouch(SeekBar seekBar) { // No es necesario } @Override public void onStopTrackingTouch(SeekBar seekBar) { // No es necesario } }); }
En onBindView() obtuvimos la instancia de la SeekBar y seteamos el progreso actual. Además de ello le asociamos una escucha del tipo OnSeekBarChangeListener para definir las acciones a realizar cuando el usuario mueva el selector.
Por esa razón se debe implementar onProgressChanged(), ya que él es quien detecta cuando hay un cambio. Justo en ese momento se actualiza el progreso actual y se usa el método persistInt() para almacenar el entero en el archivo SharedPreferences.
Usa persist*() dependiendo del tipo de valor que vayas a almacenar. Por ejemplo, para almacenar un String se usa persistString().
Ahora debemos implementar el método onGetDefaultValue() para retornar nuestro valor por defecto (android:defaultValue) cuando este sea requerido desde la actividad de preferencias:
@Override protected Object onGetDefaultValue(TypedArray a, int index) { return a.getInt(index, DEFAULT_PROGRESS); }
El parámetro a representa el conjunto de todos los atributos de las preferencias e index es la posición donde se encuentra el atributo. Se retorna en el valor de getInt(), el cual recibe el índice para búsqueda y el progreso por defecto en caso de que no se encuentre el valor.
El ultimo método a sobrescribir es onSetInitialValue(). Con este método verificaremos si el valor del progreso debe ser obtenido desde el archivo SharedPreferences debido a que ya ha sido almacenado. O si debe restaurarse por defecto.
@Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { if (restorePersistedValue) { // Obtener el progreso actual currentProgress = this.getPersistedInt(DEFAULT_PROGRESS); } else { // Reiniciar al valor por defecto currentProgress = (Integer) defaultValue; persistInt(currentProgress); } }
El parámetro restorePersistedValue define si es necesario obtener el valor actual o reiniciarlo a su valor por defecto, el cual viene almacenado en defaultValue (segundo parámetro).
Si deseas obtener el progreso actual puedes usar los métodos getPersisted*(). Estos reciben como parámetro un valor suplente por si no se obtiene el requerido.
USAR PREFERENCIA PERSONALIZADA
Finalmente puedes incluir tu nueva preferencia de control de volumen en el archivo de ajustes que hayas creado.
Para referirte a esta como etiqueta debes indicar la ruta del paquete donde se encuentra la clase VolumePreference:
<!-- Nivel de volumen de los sonidos --> <com.herprogramacion.revistatempor.VolumePreference android:key="pref_vol" android:defaultValue="75"/>
Añades la clave para identificarla y el valor por defecto que desees. El resultado final será el siguiente:
Crear Actividad De Ajustes Con El Wizard De Android Studio
Android Studio trae consigo una plantilla de ejemplo para una actividad de preferencias llamada Settings Activity.
Para crearla simplemente debes ir a tu paquete java, presionar click derecho, seleccionar New > Activity > Settings Activity.
Declárala como hija de tu actividad principal.
Al presionar Finish verás que esta actividad está configurada para tener compatibilidad con versiones anteriores a la 3.0 y para versiones posteriores. Además implementa ejemplo de categorías prefabricado.
Es supremamente útil para crear tus propios ajustes. De verdad te recomiendo que uses este método, ya que partes de una estructura definida que soporta la mayoría de versiones.
Conclusiones
Añade preferencias que sean exclusivamente necesarias siguiendo el flujograma que recomienda Google.
Usa la clase PreferenceFragment si tu aplicación está dirigida para versiones de Android superior a HONEYCOMB. Recuerda que esta clase no está en la librería de soporte.
Si deseas añadir compatibilidad para versiones anteriores, entonces usa la clase PreferenceActivity. Puedes usar el formato condicionado que genera el wizard de Android Studio para soportar todas las versiones.
Recuerda que si son varias preferencias puedes agruparlas por categorías y a su vez crear subpantallas para agrupar más de 16 ajustes.
Únete Al Discord De Develou
Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!