El secreto de los SparseBooleanArray
Cuando desarrollamos una aplicación Android que utiliza listas de elementos (y ¿qué aplicación que se precie no lo hace?), una de las funciones más recurrentes de las que queremos dotar a nuestra lista es de la capacidad de seleccionar los ítems que deseemos para poder realizar otras funciones con ellos. Seleccionarlos y que, por supuesto, al hacer scroll y volver a generar las vistas, la selección permanezca y no se cambie aleatoriamente. En este tutorial vamos a aprender a realizar una selección de los ítems de un RecyclerView para su posterior procesado, y para ello utilizaremos un SparseBooleanArray (conjunto de valores booleanos dispersos) junto con un recurso de tipo selector para marcar el estado seleccionado del ítem.Paso a paso
La selección de ítems consiste básicamente en ejecutar el métodosetSelected(true)
sobre una vista cuando hacemos clic sobre ella, cuando marcamos su CheckBox o de cualquier otra manera que se nos pueda ocurrir. Para el usuario, un ítem se percibe como seleccionado cuando destaca de alguna manera frente a los demás (para lo que utilizaremos un selector —un archivo xml que contendrá los colores del ítem según su estado). Además, tendremos que guardar un valor que nos indique cuál es este ítem para recuperarlo cuando llegue el momento de trabajar con él (para lo cual se emplea el SparseBooleanArray).
Estos son los pasos que seguiremos para implementar una selección múltiple
- Definir un layout para nuestra Activity
- Crear un selector
- Crear un layout para cada ítem del RecyclerView
- Crear una clase de prueba
- Crear un adaptador para el RecyclerView
- Definir la Activity
1. Definir el layout para nuestra Activity
Comenzaremos creando un RecyclerView en la MainActivity. Para ello lo primero es importar la biblioteca necesaria añadiendo la siguiente dependencia a nuestro build.gradle:compile 'com.android.support:recyclerview-v7:22.2.0'
Es conveniente que la versión de la biblioteca sea la misma que la de nuestra biblioteca Support:
compile 'com.android.support:appcompat-v7:22.2.0'
Ahora ya podemos definir nuestro layout de la siguiente manera:
activity_main_layout.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:orientation="horizontal"> <Button android:id="@+id/btn_obtener" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Seleccionados"/> <TextView android:id="@+id/tv_marcados" android:layout_width="wrap_content" android:layout_height="wrap_content" android:lines="1"/> </LinearLayout> <android.support.v7.widget.RecyclerView android:id="@+id/rv_ejemplo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scrollbars="vertical"/> </LinearLayout> |
2. Crear un selector
Un selector es un archivo de recursos xml que contiene la definición de estados y un recurso asociado a cada uno. Cuando establezcamos este selector como valor de la propiedadbackground
de una vista, el background
de esta vista tendrá el valor del ítem que se haya definido en el selector del para el estado en el que se encuentre la vista.
En nuestro caso vamos a definir un color para cada uno de los tres estados que nos interesan: uno para el estado normal, uno para cuando se haga clic sobre el ítem y otro para cuando el ítem esté seleccionado. Debemos definir los colores previamente en el archivo res/values/colors.xml, ya que no nos permite incluir el código hexadecimal del color directamente en el selector:
colors.xml
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="normal">#FF2700</color> <color name="presionado">#1E42FF</color> <color name="seleccionado">#2F8DFF</color> </resources> |
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/normal"/> <item android:drawable="@color/pressed" android:state_pressed="true"/> <item android:drawable="@color/selected" android:state_selected="true"/> </selector> |
state_pressed
) hemos definido el color presionado, para el estado seleccionado (state_selected
) hemos definido el color seleccionado y para el estado por defecto (sin atributo), el color normal. Existe una gran cantidad de estados para los cuales podemos definir colores o recursos drawable.
3. Crear un layout para cada ítem del RecyclerView
Vamos a definir un layout bastante simple para cada elemento del RecyclerView. Contendrá únicamente un texto. item_layout.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/selector"> <TextView android:id="@+id/tv_texto" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#FF2700" android:textColor="#E8B30C" android:textSize="50sp" android:padding="16dp" android:text="Texto" /> </LinearLayout> |
background
de la vista del elemento raíz, de forma que el fondo del ítem cambiará de color ante sus cambios de estado. Es importante que las vistas que contiene el layout no tengan color de fondo, o si no, al seleccionar el ítem, este color permanecerá, resultando en un efecto poco agradable.
4. Crear una clase de prueba
Para poblar el RecyclerView vamos a crear la clase ObjetoSimple, que solo contenga un String.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class ObjetoSimple { private String texto; public ObjetoSimple(String texto) { this.texto = texto; } public String getTexto() { return texto; } public void setTexto(String texto) { this.texto = texto; } } |
5. Crear un adaptador para el RecyclerView
Será en este adaptador donde definiremos el comportamiento de la selección de ítems. Básicamente, lo que haremos será establecer un onClickListener para el elemento raíz del ítem de forma que, al hacer clic sobre este (si está activado el modo selección), se guarde su posición en un SparseBooleanArray y así tener una referencia que no desaparezca al destruirse el ítem. Presta especial atención al métodobindView()
y a los listener.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
public class AdaptadorObjetoSimple extends RecyclerView.Adapter<AdaptadorObjetoSimple.ViewHolder> { private List<ObjetoSimple> datos; private AppCompatActivity context; private int resource; private boolean modoSeleccion; private SparseBooleanArray seleccionados; public AdaptadorObjetoSimple(AppCompatActivity context, LinkedList<ObjetoSimple> datos) { this.context = context; this.datos = datos; seleccionados = new SparseBooleanArray(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(context).inflate(R.layout.item_layout, parent, false); ViewHolder viewHolder = new ViewHolder(v); return viewHolder; } @Override public void onBindViewHolder(ViewHolder holder, int position) { ObjetoSimple os = datos.get(position); holder.bindView(os); } /**VIEWHOLDER*/ class ViewHolder extends RecyclerView.ViewHolder{ private TextView tv_texto; private View item; public ViewHolder(View itemView) { super(itemView); this.item = itemView; } public void bindView(ObjetoSimple os){ tv_texto = (TextView) item.findViewById(R.id.tv_texto); tv_texto.setText(os.getTexto()); //Selecciona el objeto si estaba seleccionado if (seleccionados.get(getAdapterPosition())){ item.setSelected(true); } else item.setSelected(false); /**Activa el modo de selección*/ item.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (!modoSeleccion){ modoSeleccion = true; v.setSelected(true); seleccionados.put(getAdapterPosition(), true); } return true; } }); /**Selecciona/deselecciona un ítem si está activado el modo selección*/ item.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (modoSeleccion) { if (!v.isSelected()) { v.setSelected(true); seleccionados.put(getAdapterPosition(), true); } else { v.setSelected(false); seleccionados.put(getAdapterPosition(), false); if (!haySeleccionados()) modoSeleccion = false; } } } }); } } public boolean haySeleccionados() { for (int i = 0; i <= datos.size(); i++) { if (seleccionados.get(i)) return true; } return false; } /**Devuelve aquellos objetos marcados.*/ public LinkedList<ObjetoSimple> obtenerSeleccionados(){ LinkedList<ObjetoSimple> marcados = new LinkedList<>(); for (int i = 0; i < datos.size(); i++) { if (seleccionados.get(i)){ marcados.add(datos.get(i)); } } return marcados; } } |
bindView()
y en el uso del SparseBooleanArray. Esto es a grosso modo lo que sucederá: cuando hagamos un clic largo en uno de los ítems de la lista, este se seleccionará y se entrará en el modo selección. Desde ese momento, podremos seleccionar y deseleccionar ítems haciendo clic sobre ellos. El modo selección finaliza cuando hayamos deseleccionado todos los elementos. Podemos ver cuáles son los ítems seleccionados pulsando el botón superior Seleccionados.
¿Cómo se logra esto? El modo selección se activa mediante un booleano que ponemos a true
o a false
según convenga, y que, si está activo, al hacer clic en un ítem, se guardará un par valor número-booleano en el SparseBooleanArray indicando que esa posición del adaptador está seleccionada o deseleccionada. Veámoslo sobre el código.
1 2 3 4 5 6 7 8 9 10 11 12 |
/**Activa el modo de selección*/ item.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (!modoSeleccion){ modoSeleccion = true; v.setSelected(true); seleccionados.put(getAdapterPosition(), true); } return true; } }); |
true
y usar el método put()
del SparseBooleanArray, al cual le pasaremos la posición del ítem en el adaptador y el valor true
. De esta manera el SparseBooleanArray contendrá una posición y un valor que indicará si está seleccionada o no. Vamos a continuación al principio del método bindView()
.
1 2 3 4 5 |
/**Selecciona el objeto si estaba seleccionado*/ if (seleccionados.get(getAdapterPosition())) item.setSelected(true); else item.setSelected(false); |
bindView()
, es decir, cada vez que se genere la vista del ítem, se comprobará si en el SparseBooleanArray figura un valor de true
para la posición que se está creando. Si está, la vista se seleccionará. Si, por el contrario, figura false
o no figura nada (devolverá false
igualmente), se deseleccionará. Al ejecutar una de estas dos líneas, el selector coloreará la vista del color seleccionado o del color normal respectivamente.
El ítem hace referencia a la vista padre del item_layout.xml, es decir, al LinearLayout, ya que la hemos inflado en el método onCreateViwHolder()
:
View v = LayoutInflater.from(context).inflate(R.layout.item_layout, parent, false);
Inflar significa que la vista v
va a convertirse en la vista padre del layout, que le pasemos. Por este motivo, al poner un listener sobre item, lo estamos poniendo sobre el LinearLayout de item_layout.xml y dado que este es el que tiene el selector en su atributo background
, todo encaja. Lo siguiente es saber cómo seleccionar ítems.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/**Selecciona un item si está activado el modo selección*/ item.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (modoSeleccion) { if (!v.isSelected()) { v.setSelected(true); seleccionados.put(getAdapterPosition(), true); } else { v.setSelected(false); seleccionados.put(getAdapterPosition(), false); if (!haySeleccionados()) modoSeleccion = false; } } } }); |
true
en el SparseBooleanArray para marcar el ítem como seleccionado cuando se vuelva a generar la vista. El selector detectará que está seleccionada y coloreará la vista.
Si está seleccionada, se deseleccionará, se almacenará el valor false
para la posición del ítem para marcarlo como deseleccionado cuando se vuelva a generar el ítem, el selector coloreará de nuevo el ítem del color correspondiente y además se comprobará si quedan más ítems seleccionados.
Esto último se hace con el método haySeleccionados()
, el cual recorrerá el conjunto de datos del adaptador y comprobará por cada posición si está marcada o no. En el momento en que haya una marcada, devolverá true
, indicando que hay al menos un ítem seleccionado. Si no hubiera ninguno seleccionado, se desactivaría el modo selección y entonces, no podríamos volver a seleccionar ítems hacendo clic hasta que lo volviésemos a activarlo haciendo un clic largo en alguno de los ítems.
De igual manera que estamos cambiando el color al seleccionar los ítems, podríamos cambiar cualquier de sus características. Podríamos mostrar botones o imágenes ocultas, sustituir el texto por otro, cambiar su tamaño, su fuente…
6. Definir la Activity
Por último, este es el código de la MainActivity. MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class MainActivity extends AppCompatActivity { private RecyclerView rv_ejemplo; private Button btn_obtener; private TextView tv_marcados; private AdaptadorObjetoSimple adaptador; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_layout); rv_ejemplo = (RecyclerView) findViewById(R.id.rv_ejemplo); btn_obtener = (Button) findViewById(R.id.btn_obtener); tv_marcados = (TextView) findViewById(R.id.tv_marcados); //Cargamos una lista con 8 objetos de ejemplo LinkedList objetosSimples = new LinkedList(); for (int i = 0 ; i < 8 ; i++) objetosSimples.add(new ObjetoSimple(String.valueOf(i + 1))); //Asignamos un LayoutManager y un adaptador al RecyclerView RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); rv_ejemplo.setLayoutManager(layoutManager); adaptador = new AdaptadorObjetoSimple(this, objetosSimples); rv_ejemplo.setAdapter(adaptador); //Asignamos una función al botón btn_obtener.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { LinkedList marcados = adaptador.obtenerSeleccionados(); String contenidoMarcados = "Marcados: "; for (ObjetoSimple os : marcados){ contenidoMarcados += os.getTexto() + ", "; } tv_marcados.setText(contenidoMarcados); } }); } } |
btn_obtener
se hará una llamada al método obtenerSeleccionados()
del adaptador, el cual recorre todas las posiciones de la colección comprobando si para cada una de ellas hay un valor true
en el SparseBooleanArray. Para aquellas posiciones con el valor true
, se guarda el objeto ObjetoSimple de la colección datos del adaptador en una LinkedList y cuando los haya comprobado todos, los devuelve. Una vez obtenidos, podemos hacer lo que queramos con nuestros objetos, ya sea modificarlos y actualizarlos en nuestra base de datos, borrarlos o simplemente mostrarlos como es el caso.
Ahora, podemos marcar cualquier elemento de la lista sin que la selección cambie de ítem al hacer scroll en el RecyclerView.
Hola Como puedo crear un reciclerview en el cual sea tomado desde dos diferentes botones (Laboratorio y Clínica) la idea es que cada botón me envíe una información diferente como debería hacerlo.
Te agradezco la ayuda. Gracias
Perdón por no haber visto el comentario antes. Lo has resuelto ya o aún quieres que te mande código de ejemplo?
mandamelo a mi porfa lo de ottobus , recycler view y volley que se caayeron los proyectos
nvillasanchez@gmail.com
Esta tarde buscaré dónde tengo los proyectos y restauraré los enlaces para que los veas. Gracias por avisar, Jesús.
Buenas Tardes,
por favor , podria mandarme el codigo que utiliza?
Buenos dias…
Tomando este ejemplo, como mantener seleccionados los elementos marcados (ejem: Selecciono dos items) luego de ir a otro fragment y volver a este… al volver a este debe seguir seleccionado los elementos
En el caso de que tu activity no sea destruida (es decir, que puedas volver a ella pulsando «Atrás»), no hay que hacer nada, porque el contenido de esta no es destruido. Pero si es destruida (porque cierras la activity), según tu propósito, tienes que guardar los elementos seleccionados en memoria (una clase singleton) o en el dispositivo (una base de datos, o un fichero de texto) y luego recuperarlos. Tienes que guardar algo que te permita identificar los elementos que estaban marcados para, en el momento de cargar los elementos, consultar cuáles estaban marcados y volver a ponerlos marcados en el
onBindView()
.Me gustaria probar el código, trabajo en un chat
Se puede descargar?