Доброго времени суток, уважаемые читатели.
В своей статье я хочу поделиться реализацией деревовидного списка с помощью RecyclerView. Без использования каких-либо дополнительных библиотек и без использования дочернего массива.
Кому интересно, прошу под кат. Постараюсь описать как можно подробнее что да как.

Принцип формирования списка элементов заключается в том, что будут показываться или скрываться дочерние элементы.
Хоть я и сказал, что реализация будет без дополнительных библиотек, однако стандартные библиотеки все же нужно подключить.
Разметка будет самой минимальной — только список RecyclerView.
Дополнительно понадобится отдельный класс, с помощью которого будем хранить значения списка.
По комментариям должно быть понятно, но поясню. Для каждого элемента списка мы будем хранить его некий идентификатор valueId, название valueText, идентификатор родительского элемента parentId, метку о том, что элемент является родительским itemParent и значение видимости для дочерних элементов childVisibility.
Следующим подготовительным этапом является создание разметки для самого элемента списка.
AppCompatImageView нужен для отображения состояния родительского элемента. TextView — для отображения значения элемента. View — просто для разделения.
Последним подготовительным этапом является создания класса для обработки адаптера списка.
Основная обработка происходит в процедуре onBindViewHolder. Для каждого элемента списка получается его идентификатор, значение и параметры родительского значения. Показываются или скрываются дочерние элементы, а так же иконка состояния для родительского элемента. Ну и вешается обработка нажатий по списку. Тут каждый сам решает, как ему нужно обрабатывать список. В примере просто показывается сообщение с id и значением элемента.
В процедуре показа или скрытия дочернего элемента setVisibility дополнительно делается отступ текста для дочернего элемента в 80 пикселей.
Осталось только заполнить список в нужном месте.
В итоге получается такой простой список с поддержкой дочерних элементов. Такая реализация позволяет заполнять несколько вложенных элементов. Но нужно совсем чуток доработать отступы для дочерних элементов, если уровень вложенности будет больше 1.
Всем спасибо за внимание и успешных проектов.
В своей статье я хочу поделиться реализацией деревовидного списка с помощью 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 {
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.
Следующим подготовительным этапом является создание разметки для самого элемента списка.
Разметка для элемента 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.
Всем спасибо за внимание и успешных проектов.