Доброго времени суток, уважаемые читатели.
В своей статье я хочу поделиться реализацией деревовидного списка с помощью RecyclerView. Без использования каких-либо дополнительных библиотек и без использования дочернего массива.
Кому интересно, прошу под кат. Постараюсь описать как можно подробнее что да как.
Принцип формирования списка элементов заключается в том, что будут показываться или скрываться дочерние элементы.
Хоть я и сказал, что реализация будет без дополнительных библиотек, однако стандартные библиотеки все же нужно подключить.
dependencies {
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'
implementation 'com.android.support:recyclerview-v7:26.1.0'
}
Разметка будет самой минимальной — только список RecyclerView.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler_list">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
Дополнительно понадобится отдельный класс, с помощью которого будем хранить значения списка.
public final class Data {
private String valueText = ""; //название значения
private int valueId = 0; //идентификатор значения
private boolean itemParent = false; //родительский или нет элемент
private int parentId = -1; //id элемента, который является родительским
private boolean childVisibility = false; //видимость дочерних элементов
//проверить родительский элемент или нет
public boolean isItemParent() {
return itemParent;
}
//установить значение родительского элемента
public void setItemParent(boolean newItemParent) {
itemParent = newItemParent;
}
//проверить видимость дочерних элементов
public boolean isChildVisibility() {
return childVisibility;
}
//установить видимость для дочерних элементов
public void setChildVisibility(boolean newChildVisibility) {
childVisibility = newChildVisibility;
}
//получить номер родительского элемента
public int getParentId() {
return parentId;
}
//установить номер родительского элемента
public void setParentId(int newParentId) {
parentId = newParentId;
}
//получить название значения
public String getValueText() {
return valueText;
}
//установить название значения
public void setValueText(String newValueText) {
valueText = newValueText;
}
//получить идентификатор значения
public int getValueId() {
return valueId;
}
//установить идентификатор значения
public void setValueId(int newValueId) {
valueId = newValueId;
}
}
По комментариям должно быть понятно, но поясню. Для каждого элемента списка мы будем хранить его некий идентификатор valueId, название valueText, идентификатор родительского элемента parentId, метку о том, что элемент является родительским itemParent и значение видимости для дочерних элементов childVisibility.
Следующим подготовительным этапом является создание разметки для самого элемента списка.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- иконка для родительского значения -->
<android.support.v7.widget.AppCompatImageView
android:id="@+id/icon_tree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/icon_hide"
android:visibility="gone"
app:backgroundTint="@color/colorPrimary"
android:layout_centerVertical="true"/>
<LinearLayout
android:id="@+id/block_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/icon_tree">
<!-- название -->
<TextView
android:id="@+id/value_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground"
android:text="sdfdsf"/>
</LinearLayout>
<!-- разделитель -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorPrimary"
android:layout_below="@+id/block_text"
android:layout_marginTop="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"/>
</RelativeLayout>
</LinearLayout>
AppCompatImageView нужен для отображения состояния родительского элемента. TextView — для отображения значения элемента. View — просто для разделения.
Последним подготовительным этапом является создания класса для обработки адаптера списка.
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private View vv;
private List<Data> allRecords; //список всех данных
public RecyclerViewAdapter(List<Data> records) {
allRecords = records;
}
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
return new RecyclerViewAdapter.ViewHolder(v);
}
@Override
public void onBindViewHolder(final RecyclerViewAdapter.ViewHolder viewHolder, int i) {
Data record = allRecords.get(i);
String value = record.getValueText();
int id = record.getValueId();
int parentId = record.getParentId();
final int position = i;
final String text = "#" + id + ": " + value + " (id родительского элемента: " + parentId + ")";
//покажем или скроем элемент, если он дочерний
if (parentId >= 0) {
//видимость делаем по параметру родительского элемента
setVisibility(viewHolder.item, allRecords.get(parentId).isChildVisibility(), parentId);
}
else { //элемент не дочерний, показываем его
setVisibility(viewHolder.item, true, parentId);
}
//покажем или скроем иконку деревовидного списка
if (record.isItemParent()) {
viewHolder.iconTree.setVisibility(View.VISIBLE);
//показываем нужную иконку
if (record.isChildVisibility()) //показываются дочерние элементы
viewHolder.iconTree.setBackgroundResource(R.drawable.icon_show);
else //скрыты дочерние элементы
viewHolder.iconTree.setBackgroundResource(R.drawable.icon_hide);
}
else //элемент не родительский
viewHolder.iconTree.setVisibility(View.GONE);
//устанавливаем текст элемента
if (!TextUtils.isEmpty(value)) {
viewHolder.valueText.setText(value);
}
//добавляем обработку нажатий по значению
viewHolder.valueText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Data dataItem = allRecords.get(position);
if (dataItem.isItemParent()) { //нажали по родительскому элементу, меняем видимость дочерних элементов
dataItem.setChildVisibility(!dataItem.isChildVisibility());
notifyDataSetChanged();
}
else { //нажали по обычному элементу, обрабатываем как нужно
Snackbar snackbar = Snackbar.make(vv, text, Snackbar.LENGTH_LONG);
snackbar.show();
}
}
});
}
//установка видимости элемента
private void setVisibility(View curV, boolean visible, int parentId) {
//найдем блок, благодаря которому будем сдвигать текст
LinearLayout vPadding = curV.findViewById(R.id.block_text);
LinearLayout.LayoutParams params;
if (visible) {
params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (vPadding != null) {
if (parentId >= 0) { //это дочерний элемент, делаем отступ
vPadding.setPadding(80, 0, 0, 0);
}
else {
vPadding.setPadding(0, 0, 0, 0);
}
}
}
else
params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
curV.setLayoutParams(params);
}
@Override
public int getItemCount() {
return allRecords.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private LinearLayout item;
private TextView valueText;
private AppCompatImageView iconTree;
public ViewHolder(View itemView) {
super(itemView);
vv = itemView;
item = vv.findViewById(R.id.id_item);
valueText = vv.findViewById(R.id.value_name);
iconTree = vv.findViewById(R.id.icon_tree);
}
}
}
Основная обработка происходит в процедуре onBindViewHolder. Для каждого элемента списка получается его идентификатор, значение и параметры родительского значения. Показываются или скрываются дочерние элементы, а так же иконка состояния для родительского элемента. Ну и вешается обработка нажатий по списку. Тут каждый сам решает, как ему нужно обрабатывать список. В примере просто показывается сообщение с id и значением элемента.
В процедуре показа или скрытия дочернего элемента setVisibility дополнительно делается отступ текста для дочернего элемента в 80 пикселей.
Осталось только заполнить список в нужном месте.
List<Data> records = new ArrayList<Data>(); //список значений
Data record;
RecyclerViewAdapter adapter;
int parentId;
RecyclerView recyclerView = findViewById(R.id.recycler_list);
record = new Data();
record.setValueId(1);
record.setValueText("Родительское значение 1");
record.setItemParent(true); //родительское значение
records.add(record);
parentId = records.size() -1;
for (int ind = 1; ind <= 3; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Текст " + ind);
record.setParentId(parentId);
records.add(record);
}
record = new Data();
record.setValueId(1);
record.setValueText("Второе родительское значение");
record.setItemParent(true); //родительское значение
records.add(record);
parentId = records.size() -1;
for (int ind = 4; ind <= 7; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Дочерний текст " + ind);
record.setParentId(parentId);
records.add(record);
}
record = new Data();
record.setValueId(1);
record.setValueText("Еще родительское значение");
record.setItemParent(true); //родительское значение
records.add(record);
parentId = records.size() -1;
for (int ind = 8; ind <= 12; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Значение " + ind);
record.setParentId(parentId);
records.add(record);
}
for (int ind = 13; ind <= 18; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Текст без родителя" + ind);
records.add(record);
}
for (int ind = 19; ind <= 21; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Элемент тоже без родителя" + ind);
records.add(record);
}
record = new Data();
record.setValueId(1);
record.setValueText("Опять родительское значение");
record.setItemParent(true); //родительское значение
records.add(record);
parentId = records.size() -1;
for (int ind = 22; ind <= 30; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Дочернее: " + ind);
record.setParentId(parentId);
records.add(record);
}
for (int ind = 31; ind <= 45; ind ++) {
record = new Data();
record.setValueId(ind);
record.setValueText("Последние без родителя " + ind);
records.add(record);
}
adapter = new RecyclerViewAdapter(records);
RecyclerView.ItemAnimator itemAnimator = new DefaultItemAnimator();
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(itemAnimator);
В итоге получается такой простой список с поддержкой дочерних элементов. Такая реализация позволяет заполнять несколько вложенных элементов. Но нужно совсем чуток доработать отступы для дочерних элементов, если уровень вложенности будет больше 1.
Всем спасибо за внимание и успешных проектов.
Автор: Евгений