Розширення списків і створення адаптера

Традиційні списки ListView, які використовують стандартні адаптери ArrayAdapter, чудово працюють з масивами рядків. Однак частіше ми будемо стикатися з більш складними структурами списків, де один елемент представляє не одну строку, а кілька рядків, зображень та інших компонентів.

Для створення складного списку нам потрібно перевизначити один із використовуваних адаптерів. Оскільки, як правило, використовується ArrayAdapter, то саме його ми і перевизначимо.

Але на початку визначимо модель, дані якої будуть відображатися в списку. Для цього додамо до того ж каталогу, де знаходиться клас MainActivity, новий клас.

package com.example.listapp;
 
public class State {
 
    private String name;
    private String capital;
    private int flagResource;
 
    public State(String name, String capital, int flag){
 
        this.name=name;
        this.capital=capital;
        this.flagResource=flag;
    }
 
    public String getName() {
        return this.name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getCapital() {
        return this.capital;
    }
 
    public void setCapital(String capital) {
        this.capital = capital;
    }
 
    public int getFlagResource() {
        return this.flagResource;
    }
 
    public void setFlagResource(int flagResource) {
        this.flagResource = flagResource;
    }
}

Створення складного списку з використанням кастомного адаптера

Цей клас зберігає два рядкових поля — назву держави та її столицю, а також числове поле, яке вказує на ресурс зображення з папки drawable, що відображатиме прапор держави.

Далі додамо в папку res/layout новий файл list_item.xml, який представлятиме розмітку одного елемента в списку:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <ImageView
        android:id="@+id/flag"
        android:layout_width="70dp"
        android:layout_height="50dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/name"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
 
    <TextView
        android:id="@+id/name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:text="Название"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/flag"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/capital" />
 
    <TextView
        android:id="@+id/capital"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:text="Столица"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/flag"
        app:layout_constraintTop_toBottomOf="@+id/name"
        app:layout_constraintBottom_toBottomOf="parent" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

Кожен елемент буде мати зображення у вигляді ImageView та два компоненти TextView для відображення назви та столиці держави.

Після цього додамо до каталогу, де знаходяться класи MainActivity та State, новий клас, який називатимемо StateAdapter:

package com.example.listapp;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class StateAdapter extends ArrayAdapter<State> {

    private LayoutInflater inflater;
    private int layout;
    private List<State> states;

    public StateAdapter(Context context, int resource, List<State> states) {
        super(context, resource, states);
        this.states = states;
        this.layout = resource;
        this.inflater = LayoutInflater.from(context);
    }

    public View getView(int position, View convertView, ViewGroup parent) {

        View view = inflater.inflate(this.layout, parent, false);

        ImageView flagView = view.findViewById(R.id.flag);
        TextView nameView = view.findViewById(R.id.name);
        TextView capitalView = view.findViewById(R.id.capital);

        State state = states.get(position);

        flagView.setImageResource(state.getFlagResource());
        nameView.setText(state.getName());
        capitalView.setText(state.getCapital());

        return view;
    }
}

Все взаємодія зі списком буде відбуватися через клас StateAdapter. У конструкторі StateAdapter нам потрібно передати три параметри:

  1. Контекст, в якому використовується клас (як правило, це клас Activity).
  2. Ресурс розмітки інтерфейсу, який буде використовуватися для створення одного елемента в ListView.
  3. Набір об'єктів, які будуть відображатися в ListView.

У конструкторі StateAdapter ми отримуємо ресурс розмітки та набір об'єктів і зберігаємо їх у окремі змінні. Також для створення об'єкта View по отриманому ресурсу розмітки нам потрібен об'єкт LayoutInflater, який зберігається у змінній.

Метод getView() відповідає за відображення елемента списку. Цей метод приймає три параметри:

  • position: позиція елемента в адаптері, для якого створюється представлення.
  • convertView: старе представлення елемента, яке при наявності використовує ListView для оптимізації.
  • parent: батьківський компонент для представлення елемента.

В даному випадку за допомогою об'єкта LayoutInflater створюємо об'єкт View для кожного окремого елемента в списку:

View view = inflater.inflate(this.layout, parent, false);

З створеного об'єкта View отримуємо елементи ImageView та TextView за їхнім id:

ImageView flagView = (ImageView) view.findViewById(R.id.flag);
TextView nameView = (TextView) view.findViewById(R.id.name);
TextView capitalView = (TextView) view.findViewById(R.id.capital);

Далі, використовуючи параметр position, отримуємо об'єкт State, для якого створюється розмітка:

State state = states.get(position);

Потім заповнюємо елементи ImageView та TextView з отриманого об'єкта State:

flagView.setImageResource(state.getFlagResource());
nameView.setText(state.getName());
capitalView.setText(state.getCapital());

І в кінці повертаємо створений елемент View для відображення об'єкта State:

return view;

Для використання зображень додамо в папку res/drawable кілька зображень, у моєму випадку це п'ять зображень прапорів держав. У підсумку проєкт матиме такий вигляд:

У файлі activity_main.xml визначимо ListView, у який будуть завантажуватися дані:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/countriesList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

А у файлі MainActivity з'єднаємо StateAdapter з ListView:

package com.example.listapp;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    ArrayList<State> states = new ArrayList<State>();
    ListView countriesList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // початкова ініціалізація списку
        setInitialData();
        
        // отримуємо елемент ListView
        countriesList = findViewById(R.id.countriesList);
        
        // створюємо адаптер
        StateAdapter stateAdapter = new StateAdapter(this, R.layout.list_item, states);
        
        // встановлюємо адаптер
        countriesList.setAdapter(stateAdapter);
        
        // слухач вибору в списку
        AdapterView.OnItemClickListener itemListener = new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
                // отримуємо вибраний пункт
                State selectedState = (State) parent.getItemAtPosition(position);
                Toast.makeText(getApplicationContext(), "Був обраний пункт " + selectedState.getName(),
                        Toast.LENGTH_SHORT).show();
            }
        };
        
        countriesList.setOnItemClickListener(itemListener);
    }

    private void setInitialData() {
        states.add(new State("Бразилія", "Бразилія", R.drawable.brazilia));
        states.add(new State("Аргентина", "Буенос-Айрес", R.drawable.argentina));
        states.add(new State("Колумбія", "Богота", R.drawable.columbia));
        states.add(new State("Уругвай", "Монтевідео", R.drawable.uruguai));
        states.add(new State("Чилі", "Сантьяго", R.drawable.chile));
    }
}

Як джерело даних тут використовується клас ArrayList, який отримує дані в методі setInitialData. Кожному додаваному об'єкту State у списку передається назва держави, її столиця та ресурс зображення з папки res/drawable, яке представляє прапор держави.

При створенні адаптера йому передається раніше створений ресурс розмітки list_item.xml та список states:

StateAdapter stateAdapter = new StateAdapter(this, R.layout.list_item, states);