Оптимизация адаптера и View Holder В прошлой теме был создан кастомный адаптер, который позволял работать со сложными списками объектов: 1 import android.content.Context; 2 import android.view.LayoutInflater; 3 import android.view.View; 4 import android.view.ViewGroup; 5 import android.widget.ArrayAdapter; 6 import android.widget.ImageView; 7 import android.widget.TextView; 8 9 import java.util.List; 10 11 public class StateAdapter extends ArrayAdapter<State> { 12 13 private LayoutInflater inflater; 14 private int layout; 15 private List<State> states; 16 17 public StateAdapter(Context context, int resource, List<State> states) { 18 super(context, resource, states); 19 this.states = states; 20 this.layout = resource; 21 this.inflater = LayoutInflater.from(context); 22 } 23 public View getView(int position, View convertView, ViewGroup parent) { 24 25 View view=inflater.inflate(this.layout, parent, false); 26 27 ImageView flagView = view.findViewById(R.id.flag); 28 TextView nameView = view.findViewById(R.id.name); 29 TextView capitalView = view.findViewById(R.id.capital); 30 31 State state = states.get(position); 32 33 flagView.setImageResource(state.getFlagResource()); 34 nameView.setText(state.getName()); 35 capitalView.setText(state.getCapital()); 36 37 return view; 38 } 39 } Но этот адаптер имеет один очень большой минус - при прокрутке в ListView, если в списке очень много объектов, то для каждого элемента, когда он попадет в зону видимости, будет повторно вызываться метод getView, в котором будет заново создаваться новый объект View. Соответственно будет увеличиваться потребление памяти и снижаться производительность. Поэтому оптимизируем код метода getView: 1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 if(convertView==null){ 4 convertView = inflater.inflate(this.layout, parent, false); 5 } 6 7 ImageView flagView = convertView.findViewById(R.id.flag); 8 TextView nameView = convertView.findViewById(R.id.name); 9 TextView capitalView = convertView.findViewById(R.id.capital); 10 11 State state = states.get(position); 12 13 flagView.setImageResource(state.getFlagResource()); 14 nameView.setText(state.getName()); 15 capitalView.setText(state.getCapital()); 16 17 return convertView; 18 } Параметр convertView указывает на элемент View, который используется для объекта в списке по позиции position. Если ранее уже создавался View для этого объекта, то параметр convertView уже содержит некоторое значение, которое мы можем использовать. В этом случае мы будем повторно использовать уже созданные объекты и увеличим производительность, однако этот код можно еще больше оптимизировать. Дело в том, что получение элементов по id тоже относительно затратная операция. Поэтому дальше оптимизируем код StateAdapter, изменив его следующим образом: 1 import android.content.Context; 2 import android.view.LayoutInflater; 3 import android.view.View; 4 import android.view.ViewGroup; 5 import android.widget.ArrayAdapter; 6 import android.widget.ImageView; 7 import android.widget.TextView; 8 9 import java.util.List; 10 11 public class StateAdapter extends ArrayAdapter<State> { 12 13 private LayoutInflater inflater; 14 private int layout; 15 private List<State> states; 16 17 public StateAdapter(Context context, int resource, List<State> states) { 18 super(context, resource, states); 19 this.states = states; 20 this.layout = resource; 21 this.inflater = LayoutInflater.from(context); 22 } 23 public View getView(int position, View convertView, ViewGroup parent) { 24 25 ViewHolder viewHolder; 26 if(convertView==null){ 27 convertView = inflater.inflate(this.layout, parent, false); 28 viewHolder = new ViewHolder(convertView); 29 convertView.setTag(viewHolder); 30 } 31 else{ 32 viewHolder = (ViewHolder) convertView.getTag(); 33 } 34 State state = states.get(position); 35 36 viewHolder.imageView.setImageResource(state.getFlagResource()); 37 38 39 40 41 42 43 44 viewHolder.nameView.setText(state.getName()); viewHolder.capitalView.setText(state.getCapital()); return convertView; } private class ViewHolder { final ImageView imageView; final TextView nameView, capitalView; ViewHolder(View view){ imageView = view.findViewById(R.id.flag); nameView = view.findViewById(R.id.name); capitalView = view.findViewById(R.id.capital); } } } Для хранения ссылок на используемые элементы ImageView и TextView определен внутренний приватный класс ViewHolder, который в конструкторе получает объект View, содержащий ImageView и TextView. В методе getView, если convertView равен null (то есть если ранее для объекта не создана разметка) создаем объект ViewHolder, который сохраняем в тег в convertView: 1 convertView.setTag(viewHolder); Если же разметка для объекта в ListView уже ранее была создана, то обратно получаем ViewHolder из тега: 1 viewHolder = (ViewHolder) convertView.getTag(); Затем также для ImageView и TextView во ViewHolder устанавливаются значения из объекта State: 1 viewHolder.imageView.setImageResource(state.getFlagResource()); 2 viewHolder.nameView.setText(state.getName()); 3 viewHolder.capitalView.setText(state.getCapital()); И теперь ListView особенно при больших списках будет работать плавнее и производительнее, чем в прошлой теме: Сложный список с кнопками Ранее были расмотрены кастомные адаптеры, которые позволяют выводить в списки сложные данные. Теперь пойдем дальше и рассмотрим, как мы можем добавить в списки другие элементы, например, кнопки, и обрабатывать их события. Для этого вначале определим следующий класс Product: 1 package com.example.listapp; 2 3 public class Product { 4 private final String name; 5 private int count; 6 private final String unit; 7 8 Product(String name, String unit){ 9 this.name = name; 10 this.count=0; 11 this.unit = unit; 12 } 13 public String getUnit() { 14 return this.unit; 15 } 16 public void setCount(int count) { 17 this.count = count; 18 } 19 20 public int getCount() { 21 return count; 22 } 23 public String getName(){ 24 return this.name; 25 } 26 } Данный класс хранит название, количество продукта, а также единицу измерения. И объекты этого классы будем выводить в список. Для этого в папку res/layout добавим новый файл list_item.xml: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="wrap_content" 7 android:padding="16dp" > 8 9 <TextView 10 android:id="@+id/nameView" 11 android:layout_width="0dp" 12 android:layout_height="wrap_content" 13 android:textSize="18sp" 14 app:layout_constraintHorizontal_weight="2" 15 app:layout_constraintBottom_toBottomOf="parent" 16 app:layout_constraintLeft_toLeftOf="parent" 17 app:layout_constraintRight_toLeftOf="@+id/countView" 18 app:layout_constraintTop_toTopOf="parent"/> 19 <TextView 20 android:id="@+id/countView" 21 android:layout_width="0dp" 22 android:layout_height="wrap_content" 23 android:textSize="18sp" 24 app:layout_constraintHorizontal_weight="2" 25 app:layout_constraintBottom_toBottomOf="parent" 26 app:layout_constraintLeft_toRightOf="@+id/nameView" 27 app:layout_constraintRight_toLeftOf="@+id/addButton" 28 app:layout_constraintTop_toTopOf="parent" /> 29 <Button 30 android:id="@+id/addButton" 31 android:layout_width="0dp" 32 android:layout_height="wrap_content" 33 android:text="+" 34 app:layout_constraintHorizontal_weight="1" 35 app:layout_constraintBottom_toBottomOf="parent" 36 app:layout_constraintLeft_toRightOf="@+id/countView" 37 app:layout_constraintRight_toLeftOf="@+id/removeButton" 38 app:layout_constraintTop_toTopOf="parent" /> 39 <Button 40 android:id="@+id/removeButton" 41 android:layout_width="0dp" 42 android:layout_height="wrap_content" 43 android:text="-" 44 app:layout_constraintHorizontal_weight="1" 45 app:layout_constraintBottom_toBottomOf="parent" 46 app:layout_constraintLeft_toRightOf="@+id/addButton" 47 app:layout_constraintRight_toRightOf="parent" 48 app:layout_constraintTop_toTopOf="parent"/> 49 50 </androidx.constraintlayout.widget.ConstraintLayout> Здесь определены два текстовых поля для вывода названия и количества продукта и две кнопки для добавления и удаления однйо единицы продукта. Теперь добавим класс адаптера, который назовем ProductAdapter: 1 package com.example.listapp; 2 3 import android.content.Context; 4 import android.view.LayoutInflater; 5 import android.view.View; 6 import android.view.ViewGroup; 7 import android.widget.ArrayAdapter; 8 import android.widget.Button; 9 import android.widget.TextView; 10 11 import java.util.ArrayList; 12 13 class ProductAdapter extends ArrayAdapter<Product> { 14 private final LayoutInflater inflater; 15 private final int layout; 16 private final ArrayList<Product> productList; 17 18 ProductAdapter(Context context, int resource, ArrayList<Product> products) { 19 super(context, resource, products); 20 this.productList = products; 21 this.layout = resource; 22 this.inflater = LayoutInflater.from(context); 23 } 24 public View getView(int position, View convertView, ViewGroup parent) { 25 26 final ViewHolder viewHolder; 27 if(convertView==null){ 28 convertView = inflater.inflate(this.layout, parent, false); 29 viewHolder = new ViewHolder(convertView); 30 convertView.setTag(viewHolder); 31 } 32 else{ 33 viewHolder = (ViewHolder) convertView.getTag(); 34 } 35 final Product product = productList.get(position); 36 37 viewHolder.nameView.setText(product.getName()); 38 viewHolder.countView.setText(product.getCount() + " " + product.getUnit()); 39 40 viewHolder.removeButton.setOnClickListener(new View.OnClickListener() { 41 @Override 42 public void onClick(View v) { 43 int count = product.getCount()-1; 44 if(count<0) count=0; 45 product.setCount(count); 46 viewHolder.countView.setText(count + " " + product.getUnit()); 47 } 48 }); 49 viewHolder.addButton.setOnClickListener(new View.OnClickListener() { 50 @Override 51 public void onClick(View v) { 52 int count = product.getCount()+1; 53 product.setCount(count); 54 viewHolder.countView.setText(count + " " + product.getUnit()); 55 } 56 }); 57 58 return convertView; 59 } 60 private static class ViewHolder { 61 final Button addButton, removeButton; 62 final TextView nameView, countView; 63 ViewHolder(View view){ 64 addButton = view.findViewById(R.id.addButton); 65 removeButton = view.findViewById(R.id.removeButton); 66 nameView = view.findViewById(R.id.nameView); 67 countView = view.findViewById(R.id.countView); 68 } 69 } 70 } Для каждой кнопки здесь определен обработчик нажатия, в котором мы уменьшаем, либо увеличиваем количество продукта на единицу и затем переустанавливаем текст в сооветствующем текстовом поле. Далее в файле activity_main.xml определим элемент ListView: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent"> 7 <ListView 8 android:id="@+id/productList" 9 android:layout_width="0dp" 10 android:layout_height="0dp" 11 app:layout_constraintBottom_toBottomOf="parent" 12 app:layout_constraintLeft_toLeftOf="parent" 13 app:layout_constraintRight_toRightOf="parent" 14 app:layout_constraintTop_toTopOf="parent" /> 15 16 </androidx.constraintlayout.widget.ConstraintLayout> И изменим класс MainActivity: 1 package com.example.listapp; 2 3 import androidx.appcompat.app.AppCompatActivity; 4 import android.os.Bundle; 5 import android.widget.ListView; 6 import java.util.ArrayList; 7 8 public class MainActivity extends AppCompatActivity { 9 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 setContentView(R.layout.activity_main); 14 15 ArrayList<Product> products = new ArrayList<Product>(); 16 products.add(new Product("Картофель", "кг.")); 17 products.add(new Product("Чай", "шт.")); 18 products.add(new Product("Яйца", "шт.")); 19 products.add(new Product("Молоко", "л.")); 20 products.add(new Product("Макароны", "кг.")); 21 ListView productList = findViewById(R.id.productList); 22 ProductAdapter adapter = new ProductAdapter(this, R.layout.list_item, products); 23 productList.setAdapter(adapter); 24 } 25 } В итоге получится следующий проект: И после запуска приложения мы сможем управлять количеством продуктов через кнопки: Выпадающий список Spinner Spinner представляет собой выпадающий список. Определим в файле разметки activity_main.xml элемент Spinner: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:padding="16dp"> 8 <Spinner 9 android:id="@+id/spinner" 10 android:layout_width="wrap_content" 11 android:layout_height="wrap_content" 12 app:layout_constraintLeft_toLeftOf="parent" 13 app:layout_constraintTop_toTopOf="parent" /> 14 15 </androidx.constraintlayout.widget.ConstraintLayout> В качестве источника данных, как и для ListView, для Spinner может служить простой список или массив, соданный программно, либо ресурс string-array. Взаимодействие с источником данных также будет идти через адаптер. В данном случае определим источник программно в виде массива в коде MainActivity: package com.example.listapp; 1 2 import androidx.appcompat.app.AppCompatActivity; 3 import android.os.Bundle; 4 import android.widget.ArrayAdapter; 5 import android.widget.Spinner; 6 7 public class MainActivity extends AppCompatActivity { 8 9 String[] countries = { "Бразилия", "Аргентина", "Колумбия", "Чили", "Уругвай"}; 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 setContentView(R.layout.activity_main); 14 15 Spinner spinner = findViewById(R.id.spinner); 16 // Создаем адаптер ArrayAdapter с помощью массива строк и стандартной разметки элемета 17 spinner 18 ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, 19 countries); 20 // Определяем разметку для использования при выборе элемента 21 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 22 // Применяем адаптер к элементу spinner 23 spinner.setAdapter(adapter); 24 } } Используемый при создании ArrayAdapter ресурс android.R.layout.simple_spinner_item предоставляется платформой и является стандартной разметкой для создания выпадающего списка. С помощью метода adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) устанавлив аются дополнительные визуальные возможности списка. А передаваемый в метод ресурс android.R.layout.simple_spinner_dropdown_item используется для визуализации выпадающего списка и также предоставляется платформой. Обработка выбора элемента Используя слушатель OnItemSelectedListener, в частности его метод onItemSelected(), мы можем обрабатывать выбор элемента из списка. Вначале добавим в разметку интерфейса текстовое поле, которое будет выводить выбранный элемент: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:padding="16dp"> 8 9 <TextView 10 android:id="@+id/selection" 11 android:layout_width="wrap_content" 12 android:layout_height="wrap_content" 13 android:textSize="26sp" 14 app:layout_constraintLeft_toLeftOf="parent" 15 app:layout_constraintTop_toTopOf="parent"> 16 </TextView> 17 <Spinner 18 android:id="@+id/spinner" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 app:layout_constraintLeft_toLeftOf="parent" 22 app:layout_constraintTop_toBottomOf="@+id/selection" /> 23 24 </androidx.constraintlayout.widget.ConstraintLayout> И изменим код MainActivity, определив для элемента Spinner слушатель OnItemSelectedListener: package com.example.listapp; 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 import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; import android.widget.TextView; public class MainActivity extends AppCompatActivity { String[] countries = { "Бразилия", "Аргентина", "Колумбия", "Чили", "Уругвай"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView selection = findViewById(R.id.selection); Spinner spinner = findViewById(R.id.spinner); // Создаем адаптер ArrayAdapter с помощью массива строк и стандартной разметки элемета spinner ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, countries); // Определяем разметку для использования при выборе элемента adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // Применяем адаптер к элементу spinner spinner.setAdapter(adapter); AdapterView.OnItemSelectedListener itemSelectedListener = AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { // Получаем выбранный объект String item = (String)parent.getItemAtPosition(position); selection.setText(item); } @Override public void onNothingSelected(AdapterView<?> parent) { } }; spinner.setOnItemSelectedListener(itemSelectedListener); } } Метод onItemSelected слушателя OnItemSelectedListener получает четыре параметра: parent: объект Spinner, в котором произошло событие выбора элемента view: объект View внутри Spinnera, который представляет выбранный элемент new position: индекс выбранного элемента в адаптере id: идентификатор строки того элемента, который был выбран Получив позицию выбранного элемента, мы можем найти его в списке: 1 String item = (String)parent.getItemAtPosition(position); Для установки слушателя OnItemSelectedListener в классе Spinner применяется метод setOnItemSelectedListener. Виджет автодополнения AutoCompleteTextView AutoCompleteTextView представляет элемент, созданный на основе класса EditText и обладающий возможностью автодополнения Во-первых, объявим в ресурсе разметке данный элемент: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:padding="16dp"> 8 9 <AutoCompleteTextView 10 android:id="@+id/autocomplete" 11 android:layout_width="0dp" 12 android:layout_height="wrap_content" 13 android:completionHint="Введите город" 14 android:completionThreshold="1" 15 app:layout_constraintLeft_toLeftOf="parent" 16 app:layout_constraintRight_toRightOf="parent" 17 app:layout_constraintTop_toTopOf="parent" 18 /> 19 20 </androidx.constraintlayout.widget.ConstraintLayout> Атрибут android:completionHint позволяет задать надпись, которая отображается внизу списка, а свойство android:completionThreshold устанавливает, какое количество символов надо ввести, чтобы начало работать автодополнение. То есть в данном случае уже после ввода одного символа должен появться список с подстановками. Как и в случае с элементами ListView и Spinner, AutoCompleteTextView подключается к источнику данных через адаптер. Источником данных опять же может служить массив или список объектов, либо ресурс string-array. Теперь подключим к виджету массив строк в классе MainActivity: 1 package com.example.listapp; 2 3 import androidx.appcompat.app.AppCompatActivity; 4 import android.os.Bundle; 5 import android.widget.ArrayAdapter; 6 import android.widget.AutoCompleteTextView; 7 8 public class MainActivity extends AppCompatActivity { 9 10 String[] cities = {"Москва", "Самара", "Вологда", "Волгоград", "Саратов", "Воронеж"}; 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.activity_main); 15 16 // Получаем ссылку на элемент AutoCompleteTextView в разметке 17 AutoCompleteTextView autoCompleteTextView = findViewById(R.id.autocomplete); 18 // Создаем адаптер для автозаполнения элемента AutoCompleteTextView 19 ArrayAdapter<String> adapter = new ArrayAdapter (this, 20 R.layout.support_simple_spinner_dropdown_item, cities); 21 autoCompleteTextView.setAdapter(adapter); 22 } } После ввода в текстовое поле одной буквы отобразится список с вариантами автодополнения, где можно выбрать предпочтительный: MultiAutoCompleteTextView Этот виджет дополняет функциональность элемента AutoCompleteTextView. MultiAutoCompleteTextView позволяет использовать автодополнения не только для одной строки, но и для отдельных слов. Например, если вводится слово и после него ставится запятая, то автозаполнение все равно будет работать для вновь вводимых слов после запятой или другого разделителя. MultiAutoCompleteTextView имеет такую же форму объявления, как и AutoCompleteTextView: 1 <?xml version="1.0" encoding="utf-8"?> 2 <androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:padding="16dp"> 8 9 <MultiAutoCompleteTextView 10 android:id="@+id/autocomplete" 11 android:layout_width="0dp" 12 android:layout_height="wrap_content" 13 android:completionHint="Введите город" 14 android:completionThreshold="1" 15 app:layout_constraintLeft_toLeftOf="parent" 16 app:layout_constraintRight_toRightOf="parent" 17 app:layout_constraintTop_toTopOf="parent" 18 /> 19 20 </androidx.constraintlayout.widget.ConstraintLayout> Чтобы включить MultiAutoCompleteTextView в коде, надо установить токен разделителя: package com.example.listapp; 1 2 import androidx.appcompat.app.AppCompatActivity; 3 import android.os.Bundle; 4 import android.widget.ArrayAdapter; 5 import android.widget.MultiAutoCompleteTextView; 6 7 public class MainActivity extends AppCompatActivity { 8 9 String[] cities = {"Москва", "Самара", "Вологда", "Волгоград", "Саратов", "Воронеж"}; 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 setContentView(R.layout.activity_main); 14 // Получаем ссылку на элемент AutoCompleteTextView в разметке 15 MultiAutoCompleteTextView autoCompleteTextView = findViewById(R.id.autocomplete); 16 // Создаем адаптер для автозаполнения элемента MultiAutoCompleteTextView 17 ArrayAdapter<String> adapter = new ArrayAdapter(this, 18 R.layout.support_simple_spinner_dropdown_item, cities); 19 autoCompleteTextView.setAdapter(adapter); 20 // установка запятой в качестве разделителя 21 autoCompleteTextView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer()); 22 } 23 } Здесь в качестве разделителя используется встроенный разделитель на основе запятой CommaTokenizer(). При необходимости мы можем создать свои разделители.