Кодовая база. Расширяем 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://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.

ссылка на оригинал статьи https://habr.com/ru/post/508454/


Комментарии

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

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