Русский
Русский
English
Статистика
Реклама

Из песочницы Кодовая база. Расширяем RecyclerView

image
Всем привет!

Меня зовут Антон Князев, senior Android-разработчик компании Omega-R. В течение последних семи лет я профессионально занимаюсь разработкой мобильных приложений и решаю сложные проблемы нативной разработки.

Хочу поделиться способами расширения RecyclerView, наработанным нашей командой и мной. Они станут надежной базой для создания нестандартных списков в приложениях.

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

Команда решила создавать маленькие библиотеки, которые улучшают и ускоряют разработку приложений, и выкладывать их в публичный репозиторий GitHub. Это позволяет легко подключать библиотеку в проектах через JitPack и дает заказчикам гарантию, что в коде нет ничего криминального.

Первая библиотека, которую мы выложили на GitHub, является простым расширением RecyclerView.

Начнем с проблем, которые она решала:

  1. Нет дефолтного layoutManager это неудобно, так как часто приходится выбирать один и тот же LinearLayoutManager;
  2. Нет возможности добавлять divider и item space через xml тоже неудобно, так как необходимо добавлять либо в item layout, либо через ItemDecorator;
  3. Нельзя просто добавить header и footer через xml это возможно только через отдельный ViewHolder.

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

1. Проблема: нет дефолтного layoutManager


Разработчики RecyclerView не предусмотрели возможности выбора LayoutManager по умолчанию. Задать layoutManager можно следующими способами:

1. через XML в атрибуте app:layoutManager=LinearLayoutManager:

<?xml version="1.0" encoding="utf-8"?><androidx.recyclerview.widget.RecyclerView   ...    app:layoutManager="LinearLayoutManager"/>

2. через код:

recyclerView.layoutManager = LinearLayoutManager(this)

По нашему опыту, в большинстве случаев нужен именно LinearLayoutManager.

Вот несколько примеров таких списков из наших приложений ITProTV, Простой Мир и Dexen:

image

Решение: добавим дефолтный layoutManager


В OmegaRecyclerView добавляется лишь 3 строчки:

 if (layoutManager == null)  {            layoutManager = LinearLayoutManager(context, attrs, defStyleAttr, 0) }

Таким образом, когда требуется LinearLayoutManager, то ничего добавлять не надо, то есть про layoutManager можно забыть.

<?xml version="1.0" encoding="utf-8"?><com.omega_r.libs.omegarecyclerview.OmegaRecyclerView    android:id="@+id/recyclerview"    android:layout_width="match_parent"    android:layout_height="match_parent" />

2. Проблема: нет возможности добавлять divider и item space через xml


Добавлять divider приходится довольно часто при использовании RecyclerView. Например, в проекте Простой Мир один из экранов был с таким нестандартным divider:

image

Из этого макета видно, что:

  • используются divider между элементами и в самом конце;
  • используется item space.

Каким образом это можно реализовать в Android стандартным путем?

Способ 1


Самый очевидный способ включить divider как элемент ImageView:

 <RelativeLayout   ...   android:paddingStart="20dp"   android:paddingTop="12dp"   android:paddingEnd="20dp"   android:paddingBottom="12dp">...   <ImageView       ...       android:layout_alignParentBottom="true"       android:src="@drawable/divider"/></RelativeLayout>

Может случиться так, что необходимо делать divider только между элементами. В таком случае придется убрать последний divider и дописать в адаптере код его скрытия.

Способ 2


Другим способом является использование DividerItemDecoration, который может нарисовать этот divider. Для него необходимо дополнительно создать drawable:

<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item android:left="32dp">        <shape android:shape="rectangle">            <size                    android:width="1dp"                    android:height="1dp" />            <solid android:color="@color/gray_dark" />        </shape>    </item></layer-list>

Для добавления отступа требуется написать свой ItemDecoration:

class VerticalSpaceItemDecoration(private val verticalSpaceHeight: Int): RecyclerView.ItemDecoration {    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,            state: RecyclerView.State) {        outRect.bottom = verticalSpaceHeight    }}

DividerItemDecoration прост: он рисует divider всегда под каждым элементом списка.
Но в случае изменения требований придется искать другое решение.

Решение: дополним возможностью добавлять divider и item space через xml


Итак, наш OmegaRecyclerView должен уметь добавлять divider с помощью следующих атрибутов:

  1. divider определяет drawable, может быть назначен и цвет напрямую;
  2. dividerShow (beginning, middle, end) флаги, которые определяют, где рисовать;
  3. dividerHeight задает высоту divider, в случае с цветом становится особенно нужным;
  4. dividerPadding, dividerPaddingStart, dividerPaddingEnd отступы: общий, с начала, с конца;
  5. dividerAlpha определяет прозрачность;
  6. itemSpace отступы между элементами списка.

Все эти атрибуты применяются непосредственно к нашему OmegaRecyclerView с помощью двух специальных ItemDecoration.

Один из ItemDecoration добавляет отступы между элементами, второй рисует сами divider. Необходимо учитывать, что при наличии отступа между элементами второй ItemDecoration рисует divider посередине отступа. Возможности поддерживать все возможные варианты LayoutManager нет, поэтому поддерживаем только LinearLayoutManager и GridLayoutManager. Также следует учитывать ориентацию списка для LinearLayoutManager.

Для того чтобы упростить код, выделим специальный DividerDecorationHelper, который будет читать и записывать относительные данные в Rect, зависящие от ориентации и порядка следования (обратный или прямой). В нем будут методы setStart, setEnd и getStart, getEnd.

Создадим базовый ItemDecoration для двух классов, в котором будет содержаться общая логика, а именно:

  1. проверка, что layoutManager является наследником LinearLayoutManager;
  2. вычисление текущей ориентации и порядка следования;
  3. определение подходящего DividerDecorationHelper.

В SpaceItemDecoration переопределим только один метод getItemOffset, который будет добавлять отступы:

override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) {        if (isShowBeginDivider() || countBeginEndPositions <= position) helper.setStart(outRect, space)        if (isShowEndDivider() && position == itemCount - countBeginEndPositions) helper.setEnd(outRect, space)    }

Следующий DividerItemDecoration будет непосредственно рисовать divider. Он должен учитывать отступ между элементами и рисовать divider посередине. Для начала переопределим метод getItemOffset для того случая, когда отступ не задан, но divider требуется для рисования.

    override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) {        if (position == 0 && isShowBeginDivider()) {            helper.setStart(outRect, dviderSize)        }        if (position != 0 && isShowMiddleDivider()) {            helper.setStart(outRect, dividerSize)        }        if (position == itemCount - 1 && isShowEndDivider()) {            helper.setEnd(outRect, dividerSize)        }    }

Также добавим такую опцию, которая позволит DividerItemDecoration спрашивать adapter, можно ли рисовать выше или ниже выбранного элемента. Для реализации такой возможности создадим свой адаптер, наследуемый от стандартного со следующими методами:

   open fun isDividerAllowedAbove(position: Int): Boolean {        return true    }    open fun isDividerAllowedBelow(position: Int): Boolean {        return true    }

Далее переопределим метод onDrawOver, чтобы рисовать divider поверх нарисованных элементов. В этом методе надо пройтись по всем элементам, видимым на экране (через getChildAt), и при необходимости нарисовать этот divider. Также надо учесть, что из атрибута dividerDrawable может прийти и цвет, у которого нет высоты. Для такого случая высоту можно взять из атрибута dividerHeight.

3. Проблема: нельзя напрямую добавить header и footer через xml


image

В RecyclerView невозможно добавлять view через xml, но есть другие способы сделать это.

Способ 1


Один из очевидных способ добавлении view через adapter. Причем необходимо отличать header и footer в adapter при введении своего идентификатора для viewType.

 fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {  val inflater = LayoutInflater.from(parent.context)        return when (viewType) { TYPE_HEADER -> {                val headerView: View = inflater.inflate(R.layout.item_header, parent, false)                HeaderViewHolder(itemView)            }            TYPE_ITEM -> {                val itemView: View = inflater.inflate(R.layout.item_view, parent, false)                ItemViewHolder(itemView)            }                       else -> null        }    }

Способ 2


Немного другой способ, но тоже через adapter. Начиная с recyclerview:1.2.0-alpha02, появился MergeAdapter, который позволяет соединять несколько адаптеров в один, делая код чище.

val mergeAdapter = MergeAdapter(headerAdapter, itemAdapter,  footerAdapter)recyclerView.adapter = mergeAdapter

Решение: дополним возможностью простого добавления header и footer через xml


Первое, что нужно сделать перехватить добавление view в нашем OmegaRecyclerView, когда идет процесс inflate. Для этого следует переопределить метод addView и добавить себе все header и footer view. Этот метод используется самим RecyclerView для дополнения видимыми элементами списка. Но view, добавленные через xml, не будут иметь ViewHolder, что в конечном итоге вызовет NullPointerException.

Итак, нам надо определить, когда view добавляется во время inflate. К счастью, существует protected метод onFinishInflate, который вызывается при завершении процесса inflate. Поэтому при вызове этого метода помечаем, что процесс inflate завершен.

  protected override fun onFinishInflate() {        super.onFinishInflate()        finishedInflate = true    }

Таким образом, метод addView будет выглядеть следующим образом:

  override fun addView(view: View, index: Int, params: ViewGroup.LayoutParams) {        if (finishedInflate) {            super.addView(view, index, params)        } else {            // save header and footer views        }    }

Далее необходимо запомнить все эти добавочные view и передать в специальный адаптер по типу MergeAdapter.

Также нам удалось решить еще одну проблему: при вызове метода findViewById наши view возвращаться не будут. Для решения этой проблемы переопределим метод findViewTraversal: в нем необходимо сравнить id найденных нами view и вернуть view при совпадении. Поскольку этот метод скрыт, просто пишем его, не указывая, что он override.

С этими и другими полезными фичами с подробным описанием вы можете познакомиться в нашей библиотеке OmegaRecyclerView:

  1. Мы уже в 2018 году создали свой ViewPager из RecyclerView. Причем, в нашем ViewPager есть бесконечный скролл;
  2. ExpandableRecyclerView специальный класс для добавления раскрывающего списка, с возможностью выбора анимации раскрытия;
  3. StickyHeader специфический элемент списка, который можно добавлять через адаптер.

Всё это является результатом наработанного опыта Omega-R. Эволюция мастерства разработчиков проходит через несколько стадий. Сначала появляется желание скопировать код из другого проекта или сделать что-то похожее на него. Потом приходит стадия, когда необходимо зафиксировать накопленный опыт и создать отдельный репозиторий.

На следующей стадии начинаешь целенаправленно создавать фичи, которые не решался делать в проектах. Это может занять значительное время, но позволяет создать задел на будущее и ускорить разработку в новых проектах. Приглашаю каждого, кто сталкивается с трудностями в разработке, познакомиться с нашими решениями в GitHub-репозитории Omega-R.
Источник: habr.com
К списку статей
Опубликовано: 26.06.2020 18:17:05
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Github

Kotlin

Программирование

Разработка мобильных приложений

Разработка под android

Omega-r

Apps

Android development

Itprotv

Простой мир

Dexen

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru