При работе с виджетом 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();
}
};
Здесь просто выводится всплывающее сообщение о выбранном элементе списка.