Работа с постраничной загрузкой в мобильных приложениях — тема довольно простая и никаких подводных камней не таит.
С подобной задачей разработчик встречается довольно часто и соответственно раз за разом делать одно и тоже скучно и лениво.
Но любую даже самую обычную задачу можно решить другим, более интересным путем, получить новые знания и просто размять мозги.
Пагинация
Если вкратце, то вся работа по реализации пагинации состоит в пунктах, перечисленных ниже:
- Добавить слушателя к recycler view;
- Загрузить первичные данные;
- Словить коллбэк, когда пользователь прокрутил список;
- Показать загрузку в списке после всех элементов;
- Отправить запрос на получение новых элементов;
- Снова отобразить данные.
В нашем случае механизм постраничной подгрузки данных был реализован, но не было отображения загрузки во время прокрутки пользователем списка.
Окей, сделать это один раз в одном приложении не проблема. Если есть несколько экранов, где необходим данный функционал, то при можно написать базовый адаптер, который умеет работать с дополнительным типом view.
Но, что если задача несколько сложнее?
Допустим есть несколько не новых проектов, у которых есть один core модуль с базовым функционалом.
Так как проектам далеко не первый год, то как следствие накопилось немало легаси кода, связанного с этой задачей, а именно множество адаптеров с различными базовыми классами.
Как можно решить задачу?
Создать новый базовый адаптер, который будет уметь показывать прогресс загрузки, переписать все старые адаптеры + экраны, на которых используется адаптеры, потому что придется на каждом экране, в каждом презентере или вью модели управлять состоянием отображения загрузки.
Это может быть довольно трудозатратно. А также крупный рефакторинг может привести к некоторому количеству новых ошибок, особенно если покрытие тестами в проектах стремится к нулю. Плюс высокая нагрузка на отдел QA с регресс-тестированием всех экранов с пагинацией по нескольким приложениям.
Как было бы здорово написать такой код, который требует от клиента-разработчика минимум временных вложений для интеграции базового решения в своей фиче. Чтобы не пришлось реализовывать логику управления отображения прогресса загрузки — показа и скрытия.
В этот момент я подумал об использовании класса ItemDecoration.
Почему бы не использовать этот класс для инкапсуляции всей работы связанной с
- определением, когда нужно показать загрузку и когда нужно ее скрыть
- отрисовкой прогресса загрузки
К сожалению, в интернете не нашлось ни одного готового решения — либо никто не пытался это реализовать, либо просто не поделился опытом после реализации.
Что же нужно для реализации отображения загрузки при пагинации при помощи ItemDecoration?
- Сделать отступ для отрисовки загрузки;
- Понимать в реализации ItemDecoration, когда нам необходимо показать прогресс;
- Отрисовать загрузку;
Отступ
Как сделать отступ знает любой разработчик, который хоть раз сам создавал SpacingItemDecoration. В этом нам поможет метод ItemDecoration getItemOffsets:
override fun getItemOffsets(outRect: Rect, view: View,
recyclerView: RecyclerView,
state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
}
У любого элемента списка мы можем добавить отступы с любой стороны. Конкретно в нашем случае нам необходимо понять, что пользователь докрутил список до конца и у последнего элемента сделать снизу отступ равный величине будущего отображаемого прогресса загрузки + отступы от самого прогресса.
Как понять, что список прокручен до самого низа?
Нам поможет в этом код, представленный ниже:
private fun isLastItem(recyclerView: RecyclerView, view: View): Boolean {
val lastItemPos = recyclerView.getChildAdapterPosition(view)
return lastItemPos == recyclerView.adapter!!.itemCount - 1
}
Итого мы имеем код для определения и реализации отступа:
override fun getItemOffsets(outRect: Rect, view: View,
recyclerView: RecyclerView,
state: RecyclerView.State) {
super.getItemOffsets(outRect, view, recyclerView, state)
when (isLastItem(recyclerView, view)) {
true -> outRect.set(Rect(0, 0, 0, 120))
else -> outRect.set(Rect(0, 0, 0, 0))
}
}
private fun isLastItem(recyclerView: RecyclerView, view: View): Boolean {
val lastItemPos = recyclerView.getChildAdapterPosition(view)
return lastItemPos == recyclerView.adapter!!.itemCount - 1
}
Треть дела сделано!
Определяем время показа прогресса загрузки и вызываем отрисовку прогресса
В этом нам поможет метод ItemDecoration onDrawOver:
override fun onDrawOver(canvas: Canvas,
recyclerView: RecyclerView,
state: RecyclerView.State) {
super.onDrawOver(canvas, recyclerView, state)
}
Метод onDrawOver отличается от метода onDraw только порядком отрисовки. В onDrawOver decorations будут отрисованы только после отрисовки самого элемента списка.
В методе onDrawOver нам необходимо понять в какой момент нам необходимо отрисовать прогресс, в какой момент его убрать и вызвать обновления у элементов списка с целью скрытия уже ненужного отступа.
Код для реализации действий, описанных выше:
override fun onDrawOver(canvas: Canvas,
recyclerView: RecyclerView,
state: RecyclerView.State) {
super.onDrawOver(canvas, recyclerView, state)
when (showLoading(recyclerView)) {
true -> {
PaginationProgressDrawer.drawSpinner(recyclerView, canvas)
isProgressVisible = true
}
else -> {
if (isProgressVisible) {
isProgressVisible = false
recyclerView.invalidateItemDecorations()
}
}
}
}
private fun showLoading(recyclerView: RecyclerView): Boolean {
val manager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItemPos = manager.findLastCompletelyVisibleItemPosition()
return lastVisibleItemPos != -1 &&
lastVisibleItemPos >= recyclerView.adapter!!.itemCount - 1
}
Отрисовка прогресса
Код отрисовки довольно объемный и будет представлен в отдельном файле, ссылку на который я представлю ниже.
Хочется остановиться только на нюансах, которые необходимы для реализации.
Вся работа по отрисовке происходит на канвасе. Соответственно, будет необходимо настроить экземпляр объекта Paint и рисовать дуги, с указанием начального и конечного углов.
Важный нюанс в том, что для отрисовки максимально похожей на обычный ProgressBar необходимо создать собственный аналог интерполятора для того, чтобы анимация отрисовки происходила нелинейно.
Для понимания работы интерполяторов и осознания того, что вы хотите получить от своей анимации я рекомендую ознакомиться с графиками, на которых изображена работа различных видов интерполяторов, например, вот в этой статье
Ссылки на код:
PaginationLoadingDecoration
PaginationProgressDrawer
При необходимости можно создать ProgressDrawer интерфейс и подменять реализации в PaginationLoadingDecoration.
Видео с демонстрацией загрузки: