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

в 11:46, , рубрики: android, explained RecyclerView, recyclerview, Деревовидный список, разработка мобильных приложений, Разработка под android

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

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

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

Принцип формирования списка элементов заключается в том, что будут показываться или скрываться дочерние элементы.

Хоть я и сказал, что реализация будет без дополнительных библиотек, однако стандартные библиотеки все же нужно подключить.

Подключенные библиотеки

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.

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

Автор: Евгений

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js