Uploaded by itas metod

Оптимизация адаптера и View Holder

advertisement
Оптимизация адаптера и 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(). При необходимости мы можем создать свои разделители.
Download