Close

Lista Agrupada

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

© 2021 vladymix | WordPress Theme: Annina Free by CrestaProject.