2. Создание списка с помощью виджета ListView

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

Создание тестового проекта

Давайте реализует вышеописанный сценарий работы с виджетом списка. Создадим тестовое приложение с одним Activity и следующим макетом

В классе MainActivity.java получаем доступ к виджетам окна, а также создаем метод для генерации данных, которые будут помещены в список.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // Генерируем данные
        List<String> data = createList();
        
        // Виджет ListView
        ListView list = findViewById(R.id.list);
        
        // Кнопка ADD
        findViewById(R.id.add).setOnClickListener(v -> {});

        // Кнопка DELETE
        findViewById(R.id.delete).setOnClickListener(v -> {});

        // Кнопка CLEAR
        findViewById(R.id.clear).setOnClickListener(v -> {});

    }

    private List<String> createList() {
        return new Random().doubles(25, 0, 1000)
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());
    }
}

Сценарий использования адаптера вместе с виджетом списка ListView следующий:

  1. Разработчик описывает класс адаптера (в случае использования адаптера для ListView, наш адаптер наследуется от базового класса BaseAdapter) либо использует один из стандартных классов адаптера, которые в данном курсе не рассматриваются;

  2. В класс адаптера передаются данные для отображения в списке (как правило, данные передаются через конструктор);

  3. В классе адаптера реализуются методы, в которых разработчик указывает количество пунктов списка, данные для вывода в каждом пункте и так далее;

  4. Создается объект адаптера и передается виджету списка с помощью метода setAdapter();

  5. Если данные списка были модифицированы, необходимо оповестить об этом адаптер (например, с помощью метода notifyDataSetChanged() или класса DiffUtils), чтобы виджет списка обновился с учетом изменения данных.

Написание класса адаптера

Создадим класс адаптера, который наследуется от абстрактного класса BaseAdapter. Переопределим абстрактные методы, передадим через конструктор контекст и список с данными. Также, получим от операционной системы объект LayoutInflater, который будем использовать для получения дерева объектов UI из макета.

MyAdapter.java
public class MyAdapter extends BaseAdapter {

    private List<String> data;
    private LayoutInflater inflater;

    public MyAdapter(Context context, List<String> data) {
        this.data = data;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}

Метод getCount() должен возвращать количество элементов в списке. В нашем случае, количество элементов совпадает с длиной списка с данными.

MyAdapter.java
@Override
public int getCount() {
    return data.size();
}

Методы getItem() и getItemId() мы модифицировать не будем. Вопрос их предназначения и сценарии использования выносятся на самостоятельное обучение. На данный момент нас интересует метод getView(), который является наиболее важным в данном классе.

Метод getView() возвращает корень дерева объектов UI, которые отображают пункт списка. Этот метод вызывается виджетом списка при отрисовке каждого видимого пункта списка (+ один невидимый пункт сверху и снизу для обеспечения плавности прокрутки).

Метод принимает три аргумента: порядковый номер списка, который запрашивается виджетом, корень дерева объектов для повторного использования (этот вопрос будет рассмотрен ниже) и ссылка на родительский объект, куда будет прикрепляться наше дерево объектов (он нужен для объекта LayoutInflater).

На данный момент, сценарий работы метода следующий:

  1. с помощью объекта LayoutInflater преобразуем макет пункта списка в дерево объектов;

  2. получаем доступ к объектам дерева с помощью метода View.findViewById();

  3. заполняем объекты UI данными;

  4. полученное дерево объектов передаем в качестве результата работы метода.

Сначала придумаем макет для пункта списка

Далее программируем тело метода getView()

MyAdapter.java
@Override
public View getView(int position, View convertView, ViewGroup parent) {

    // 1. Преобразуем макет в дерево объектов
    View view = inflater.inflate(R.layout.list_item, parent, false);

    // 2. Получаем доступ к виджетам дерева объектов
    TextView number = view.findViewById(R.id.number);
    TextView text = view.findViewById(R.id.text);

    // 3. Меняем содержимое виджетов
    number.setText(String.valueOf(position));
    text.setText(data.get(position));

    // 4. Возвращаем модифицированное дерево объектов
    return view;
}

Вернемся в класс MainActivity. Создадим объект адаптера и передадим его виджету списка.

MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    List<String> data = createList();
    ListView list = findViewById(R.id.list);

    // Создаем объект адаптера
    MyAdapter adapter = new MyAdapter(this, data);

    // Передаем его виджету списка
    list.setAdapter(adapter);

    // Кнопка ADD
    findViewById(R.id.add).setOnClickListener(v -> {});

    // Кнопка DELETE
    findViewById(R.id.delete).setOnClickListener(v -> {});

    // Кнопка CLEAR
    findViewById(R.id.clear).setOnClickListener(v -> {});
}

Запустим приложение и посмотрим результат его работы

Модификация данных списка

Теперь реализуем обработчики нажатий на кнопки. По нажатию кнопки "ADD" мы добавляем еще один пункт списка, по нажатию кнопки "DELETE" мы удаляем последний пункт списка, по нажатию кнопки "CLEAR" мы очищаем весь список. Напишем код обработчиков

MainActivity.java
// Кнопка ADD
findViewById(R.id.add).setOnClickListener(v -> {
    data.add(new Random()
            .doubles(1, 0, 1000)
            .mapToObj(String::valueOf)
            .findFirst().get());
});

// Кнопка DELETE
findViewById(R.id.delete).setOnClickListener(v -> {
    data.remove(data.size() - 1);
});

// Кнопка CLEAR
findViewById(R.id.clear).setOnClickListener(v -> {
    data.clear();
});

Запустим приложение и обнаружим, что, хотя источник данных меняется, виджет списка не изменяется. Это происходит потому что мы не указали виджету, что данные изменились и необходимо обновить список. Для того чтобы оповестить виджет списка, мы вызываем метод адаптера notifyDataSetChanged(). Каждый раз, когда мы модифицируем данные для виджета списка, необходимо вызывать метод notifyDataSetChanged().

MainActivity.java
// Кнопка ADD
findViewById(R.id.add).setOnClickListener(v -> {
    data.add(new Random()
            .doubles(1, 0, 1000)
            .mapToObj(String::valueOf)
            .findFirst().get());
    adapter.notifyDataSetChanged();
});

// Кнопка DELETE
findViewById(R.id.delete).setOnClickListener(v -> {
    data.remove(data.size() - 1);
    adapter.notifyDataSetChanged();
});

// Кнопка CLEAR
findViewById(R.id.clear).setOnClickListener(v -> {
    data.clear();
    adapter.notifyDataSetChanged();
});

Теперь еще раз запустим приложение и посмотрим на результат. В этот раз виджет списка синхронизируется с источником данных.

Механизм повторного использования элементов списка

При большом количестве элементов в списке вы можете заметить, что при прокрутке списка, приложение начинает подтормаживать.

Это связано с тем, что ListView не создает View для всех пунктов списка сразу, а вызывает метод getView() лишь для тех пунктов, которые видны в данный момент на экране (+ первый невидимый элемент сверху и снизу).

Когда вы прокручиваете список, ListView вынужден очень часто обращаться к методу getView() так как появляются новые пункты списка, которые необходимо вывести на экран и преобразовывать макет в объекты View, что является достаточно долгой операцией. Это приводит к большой нагрузке на устройство.

Чтобы преодолеть этот недостаток, в виджетах списка предусмотрен механизм повторного использования пунктов списка. Он состоит в следующем – как только пункт списка выходит за пределы экрана, он может быть использован повторно для показа пункта списка, который нужно в данный момент вывести на экран.

В виджете ListView этот механизм не реализован автоматически, нам необходимо отредактировать метод getView().

Когда ListView вызывает метод getView(), он может передать через аргумент аргумент convertView тот View, который доступен для повторного использования (если таковой имеется). Если же в данный момент нет View для повторного использования, то аргумент будет равен null и мы должны создать новый View.

Таким образом, мы реализуем механизм повторного использования View следующим образом : проверяем, равен ли convertView null:

  • если convertView не равен null – значит нам передали View для повторного использования и мы не создаем новый View, а используем существующий. Если для всех пунктов списка используется один и тот же макет, то мы гарантированно знаем, что там будут одинаковые виджеты. Тогда мы просто заполняем их нужной информацией;

  • если convertView равен null, значит сейчас нет View для повторного использования, поэтому мы создаем новый View из файла макета.

Реализуем эту логику в методе getView()

MyAdapter.java
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    
    // 1. Проверяем, есть ли у нас пункт списка для повторного использования
    if (convertView == null)
        convertView = inflater.inflate(R.layout.list_item, parent, false);

    // 2. Получаем доступ к виджетам дерева объектов
    TextView number = convertView.findViewById(R.id.number);
    TextView text = convertView.findViewById(R.id.text);

    // 3. Меняем содержимое виджетов
    number.setText(String.valueOf(position));
    text.setText(data.get(position));

    // 4. Возвращаем модифицированное дерево объектов
    return convertView;
}

В конце приведем листинги созданных классов

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        List<String> data = createList();
        ListView list = findViewById(R.id.list);

        // Создаем объект адаптера
        MyAdapter adapter = new MyAdapter(this, data);

        // Передаем его виджету списка
        list.setAdapter(adapter);

        // Кнопка ADD
        findViewById(R.id.add).setOnClickListener(v -> {
            data.add(new Random()
                    .doubles(1, 0, 1000)
                    .mapToObj(String::valueOf)
                    .findFirst().get());
            adapter.notifyDataSetChanged();
        });

        // Кнопка DELETE
        findViewById(R.id.delete).setOnClickListener(v -> {
            data.remove(data.size() - 1);
            adapter.notifyDataSetChanged();
        });

        // Кнопка CLEAR
        findViewById(R.id.clear).setOnClickListener(v -> {
            data.clear();
            adapter.notifyDataSetChanged();
        });
    }

    private List<String> createList() {
        return new Random().doubles(25, 0, 1000)
                .mapToObj(String::valueOf)
                .collect(Collectors.toList());
    }
}

Last updated