При работе с виджетом RecyclerView неизбежно встанет вопрос, а как обработать выбор элемента в RecyclerView. И тут надо заметить, что в отличие от других типов виджетов для работы со списками RecyclerView по умолчанию не предоставляет какого-то специального метода, с помощью которого можно было бы определить слушатель нажатия на элемент в списке. Поэтому всю инфраструктуру необходимо определять самому разработчику. Но к счастью в реальности все не так сложно. Для примера возьмем проект из прошлой темы:
Итак, для представления данных в проекте есть класс State, который представляет государство:
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; } }
Класс State содержит поля для хранения названия и столицы страны, а также ссылку на ресурс изображения флага страны. В данном случае предполагается, что в папке res/drawable будут располагаться файлы изображений флагов для используемых стран.
В папке res/layout для вывода одного объекта State в списке определен следующий файл 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" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" 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>
Теперь перейдем к классу StateAdapter и следующим образом определим его код:
package com.example.listapp; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class StateAdapter extends RecyclerView.Adapter<StateAdapter.ViewHolder>{ interface OnStateClickListener{ void onStateClick(State state, int position); } private final OnStateClickListener onClickListener; private final LayoutInflater inflater; private final List<State> states; StateAdapter(Context context, List<State> states, OnStateClickListener onClickListener) { this.onClickListener = onClickListener; this.states = states; this.inflater = LayoutInflater.from(context); } @Override public StateAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = inflater.inflate(R.layout.list_item, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(StateAdapter.ViewHolder holder, int position) { State state = states.get(position); holder.flagView.setImageResource(state.getFlagResource()); holder.nameView.setText(state.getName()); holder.capitalView.setText(state.getCapital()); holder.itemView.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { onClickListener.onStateClick(state, position); } }); } @Override public int getItemCount() { return states.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { final ImageView flagView; final TextView nameView, capitalView; ViewHolder(View view){ super(view); flagView = view.findViewById(R.id.flag); nameView = view.findViewById(R.id.name); capitalView = view.findViewById(R.id.capital); } } }
Здесь я остановлюсь на тех моментах, которые были добавлены по сравнению с кодом из прошлой статьи.
Прежде всего нам надо определить интерфейс слушателя события нажатия. Для этого в классе StateAdapter определен интерфейс:
interface OnStateClickListener{ void onStateClick(State state, int position); }
Интерфейс определяет один метод onStateClick()
, который, как предполагается, будет вызываться при выборе объекта State и который
будет получать выбранный объект State и его позицию в списке.
Следующий момент - определение в классе адаптера переменной для хранения объекта этого интерфейса и получение для нее значения в конструкторе:
private final OnStateClickListener onClickListener; StateAdapter(Context context, List<State> states, OnStateClickListener onClickListener) { this.onClickListener = onClickListener; // ........................ }
Таким образом, вне кода адаптера мы можем установить любой объект слушателя и передать его в адаптер.
И третий момент - вызов метода слушателя при нажатии на элемент в списке в методе onBindViewHolder:
public void onBindViewHolder(StateAdapter.ViewHolder holder, int position) { State state = states.get(position); holder.flagView.setImageResource(state.getFlagResource()); holder.nameView.setText(state.getName()); holder.capitalView.setText(state.getCapital()); // обработка нажатия holder.itemView.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { // вызываем метод слушателя, передавая ему данные onClickListener.onStateClick(state, position); } }); }
Класс ViewHolder имеет поле itemView, которое представляет интерфейс для одного объекта в списке и фактически
объект View. А у этого объекта есть метод setOnClickListener()
, через который можно подлючить стандартный слушатель
нажатия OnClickListener и в его методе onClick()
вызвать метод нашего интерфейса, передав ему необходимые данные - выбранный объект State и его
позицию в списке.
Может возникнуть вопрос, а почему бы сразу тут и не обработать нажатие на элемент? К чему создавать дополнительный интерфейс, его переменную и вызывать его метод? Конечно, мы можем попыться прямо тут обработать нажатия, но это не является хорошей или распространенной практикой, поскольку, возможно, мы захотим определить обработку нажатия в классе MainActivity исходя из того, кода, который там определен (или из какого-то другого места извне).
В файле activity_main.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="match_parent" android:padding="16dp"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list" android:layout_width="0dp" android:layout_height="0dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
И в конце изменим класс MainActivity:
package com.example.listapp; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.RecyclerView; import android.os.Bundle; import android.widget.Toast; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { ArrayList<State> states = new ArrayList<State>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // начальная инициализация списка setInitialData(); RecyclerView recyclerView = findViewById(R.id.list); // определяем слушателя нажатия элемента в списке StateAdapter.OnStateClickListener stateClickListener = new StateAdapter.OnStateClickListener() { @Override public void onStateClick(State state, int position) { Toast.makeText(getApplicationContext(), "Был выбран пункт " + state.getName(), Toast.LENGTH_SHORT).show(); } }; // создаем адаптер StateAdapter adapter = new StateAdapter(this, states, stateClickListener); // устанавливаем для списка адаптер recyclerView.setAdapter(adapter); } 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)); } }
При создании адаптера ему передается определенный в классе MainActivity слушатель:
StateAdapter.OnStateClickListener stateClickListener = new StateAdapter.OnStateClickListener() { @Override public void onStateClick(State state, int position) { Toast.makeText(getApplicationContext(), "Был выбран пункт " + state.getName(), Toast.LENGTH_SHORT).show(); } };
Здесь просто выводится всплывающее сообщение о выбранном элементе списка.