ListView con secciones o cabeceras.
En ocaciones necesitamos visualizar una lista de datos agrupados por alguna característica en común, en el siguiente post enseñare lo necesario para crear una lista con cabeceras.
Creamos nuestra actividad.
En este caso contendría un ListView para poder visualizar los datos
ActivityMain.java
public class MainActivity extends AppCompatActivity { ListView listView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.list_view); ArrayList<User> allUsers = Utils.getAllUser(); List<Object> listGrouping = Utils.getGroupByName(allUsers); listView.setAdapter(new UserGroupAdapter(MainActivity.this, listGrouping)); listView.setOnItemClickListener(this.onItemClick()); } }
Creamos nuestro layout
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" tools:context="com.example.exaccta.listgrouping.MainActivity"> <ListView android:id="@+id/list_view" android:divider="@android:color/transparent" android:dividerHeight="0dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout>
Vamos a definir nuestra cabecera y nuestro item de la lista.
item_header.xml
<?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="wrap_content"> <TextView android:textColor="@color/colorPrimaryDark" android:layout_marginLeft="23dp" android:layout_marginRight="5dp" android:layout_marginBottom="5dp" android:layout_marginTop="5dp" android:textStyle="bold" android:textSize="18sp" android:text="A" android:id="@+id/txt_header" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <LinearLayout android:background="@color/colorPrimaryDark" android:layout_centerVertical="true" android:layout_toRightOf="@id/txt_header" android:layout_width="match_parent" android:layout_height="1.5dp"/> </RelativeLayout>
item_content.xml
<?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="wrap_content"> <RelativeLayout android:id="@+id/item_image" android:padding="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:src="@drawable/ic_circle" android:layout_width="40dp" android:layout_height="40dp"/> <TextView android:textColor="@color/colorPrimaryDark" android:id="@+id/txt_item_start" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:text="N" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout> <LinearLayout android:layout_toRightOf="@+id/item_image" android:layout_centerVertical="true" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/txt_name" android:text="Name" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/txt_last_name" android:text="LastName" android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> </RelativeLayout>
Bueno hasta aquí nada nuevo ¿verdad?.
Sigamos.
Agrupar nuestros items (Lista de usuarios).
En este caso tenemos varias formas de hacerlo.
1. Añadiendo una característica a nuestro objeto para saber si es un item o es una cabecera (is_header).
public class User { private String name; private String last_name; private Boolean is_header; public User(String name, String last_name) { this.name = name; this.last_name = last_name; } public void isHeader(Boolean header){ this.is_header = header; } public String getLast_name() { return last_name; } public String getName() { return name; } public void setLast_name(String last_name) { this.last_name = last_name; } public void setName(String name) { this.name = name; } }
2. Creando un objeto diferente para añadirlo a la lista como cabecera.
public class Header { private String key; public Header(String key){ this.key = key; } public String getKey() { return key; } public void setKey(String key) { this.key = key.substring(0,1); } }
Aquí explicare como usar la segunda opción.
Una vez que tenemos nuestros recursos vamos a codificar un poco.
Crear una List<Object> agrupada.
Creamos un método que nos ayude a agrupar la lista e ir creando la lista agrupada. para ello creamos el método que reciba una lista de usuarios y los ordene por nombre.
public static void orderListByName(ArrayList<User> allUsers){ Collections.sort(allUsers, new Comparator<User>() { @Override public int compare(User o1, User o2) { return o1.getName().compareTo(o2.getName()); } }); }
Ahora como ya tenemos ordenada nuestra lista (por nombre), vamos a recorrerla y por cada nuevo item que encontremos vamos a crear un objeto header
List<Object> items= new ArrayList<>(); String key = allUsers.get(0).getName().toLowerCase().substring(0,1); items.add(new Header(key)); for(User item: allUsers){ if(item.getName().toLowerCase().startsWith(key)){ items.add(item); }else{ key = item.getName().toLowerCase().substring(0,1); items.add(new Header(key)); items.add(item); } }
De este modo recogemos la clave por la que queremos agrupar, en este caso solo compruebo la primera letra, como nos da igual si es mayúscula o minúscula usamos toLowerCase() para comparar con minúsculas y comprobar si el siguiente empieza por la letra buscada, si nos fijamos por cada letra nueva que encuentre creamos un objeto Header y añadimos a nuestros items agrupados.
El método completo nos quedaria asi.
public static List<Object> getGroupByName(ArrayList<User> allUsers) { orderListByName(allUsers); List<Object> items= new ArrayList<>(); // Start group String key = allUsers.get(0).getName().toLowerCase().substring(0,1); items.add(new Header(key)); for(User item: allUsers){ if(item.getName().toLowerCase().startsWith(key)){ items.add(item); }else{ key = item.getName().substring(0,1); // New group items.add(new Header(key)); items.add(item); } } return items; }
Creación de nuestro ArrayAdapter (La parte más compleja).
Lo mas importante aquí es distinguir que tipo de objeto vamos a representar, y en función de este pintamos una vista u otra.
para usar la opción 1 usaremos esto
if(getItem(position).getIsHeader()){ // Pintamos el header }else{ // Pintamos el contenido }
Como en este ejemplo estamos usando la opción 2
if(getItem(position) instanceof Header){ // Pintamos el header }else{ // Pintamos el contenido }
Tenemos que usar el reciclado de la lista para ello tenemos que comprobar si la vista que vamos a usar es nula o es del tipo header o tipo contenido, para ello tenemos que tener un campo único en el item_header e item_contenido para poder distinguirlo
En este caso:
El item_header tengo un TextView txt_header que no existe en el item_contenido y
if (convertView == null || convertView.findViewById(R.id.txt_header)==null) { convertView = layoutInflater.inflate(R.layout.item_header, parent,false); }
En el item_contenido tengo un TextView txt_name que no existe en el item_header
if (convertView == null || convertView.findViewById(R.id.txt_header)==null) { convertView = layoutInflater.inflate(R.layout.item_header, parent,false); }
De este modo podemos reciclar la vista actual (View contentview).
UserGroupAdapter.java
public class UserGroupAdapter extends ArrayAdapter<Object> { private final LayoutInflater layoutInflater; public UserGroupAdapter(Activity context, List<Object> objects) { super(context,0, objects); layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @NonNull @Override public View getView(int position, View convertView, ViewGroup parent) { //header if(getItem(position) instanceof Header) { if (convertView == null || convertView.findViewById(R.id.txt_header)==null) { convertView = layoutInflater.inflate(R.layout.item_header, parent,false); } TextView textView = convertView.findViewById(R.id.txt_header); Header header = (Header) getItem(position); textView.setText(header.getKey()); } else //ViewHolder Pattern for content { Holder holder = null; //check if this view contained a header if (convertView == null || convertView.findViewById(R.id.txt_name) == null) { holder = new Holder(); convertView = layoutInflater.inflate(R.layout.item_content, parent,false); holder.setName((TextView) convertView.findViewById(R.id.txt_name)); holder.setLast_name((TextView) convertView.findViewById(R.id.txt_last_name)); holder.setTxt_item_start((TextView) convertView.findViewById(R.id.txt_item_start)); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } User content = (User) getItem(position); holder.getTxt_item_start().setText(content.getName().substring(0,1)); holder.getName().setText(content.getName()); holder.getLast_name().setText(content.getLast_name()); } return convertView; } class Holder { private TextView name; private TextView txt_item_start; private TextView last_name; public TextView getName() { return name; } public TextView getTxt_item_start() { return txt_item_start; } public void setName(TextView textView) { this.name = textView; } public TextView getLast_name() { return last_name; } public void setLast_name(TextView textView) { this.last_name = textView; } public void setTxt_item_start(TextView txt_item_start) { this.txt_item_start = txt_item_start; } } }
Con esto ya estaria todo.
Efecto desplazamiento.
En algunas listas que tienen este tipo de contenido agrupado vemos que si presionamos en su header nos pone la el header arriba del todo haciendo un efecto de barrido, esto queda muy chulo, y es una linea de código que lo hace automaticamente depende del contenido de nuestra lista.
Codificando método OnItemClickListener del listView
listView.setOnItemClickListener(this.onItemClick());
Yo lo he codificado como un método fuera para que el código esté un poco más limpio.
En este caso también tenemos que distinguir el tipo de objeto. (Ya lo vimos en el apartado de arriba)
private AdapterView.OnItemClickListener onItemClick(){ return new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Object item = listView.getAdapter().getItem(position); if (item instanceof Header) { Toast.makeText(MainActivity.this, ((Header) item).getKey(), Toast.LENGTH_SHORT).show(); // back to header, see // https://danielme.com/tip-android-17-listview-back-to-top-volver-al-inicio/ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { listView.setSelection(position); } else { listView.smoothScrollToPositionFromTop(position, 0, 300); } } else { Toast.makeText(MainActivity.this, ((User) item).getName(), Toast.LENGTH_SHORT).show(); } } }; }
Espero que les sirva.