Хабрахабр

Деревовидный список RecyclerView (без сторонних библиотек и дочерних массивов)

Доброго времени суток, уважаемые читатели.

Без использования каких-либо дополнительных библиотек и без использования дочернего массива.
Кому интересно, прошу под кат. В своей статье я хочу поделиться реализацией деревовидного списка с помощью 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>

Дополнительно понадобится отдельный класс, с помощью которого будем хранить значения списка.

Класс для данных Data.java

public final class Data //установить значение родительского элемента 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.

Следующим подготовительным этапом является создание разметки для самого элемента списка.

Разметка для элемента item.xml

<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 — просто для разделения.

Последним подготовительным этапом является создания класса для обработки адаптера списка.

Адаптер для списка RecyclerViewAdapter.java

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.

Всем спасибо за внимание и успешных проектов.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»