Как стать автором
Обновить

Альтернативный подход к отображению загрузки во время пагинации

Время на прочтение4 мин
Количество просмотров6.7K
image

Работа с постраничной загрузкой в мобильных приложениях — тема довольно простая и никаких подводных камней не таит.

С подобной задачей разработчик встречается довольно часто и соответственно раз за разом делать одно и тоже скучно и лениво.

Но любую даже самую обычную задачу можно решить другим, более интересным путем, получить новые знания и просто размять мозги.

Пагинация


Если вкратце, то вся работа по реализации пагинации состоит в пунктах, перечисленных ниже:

  1. Добавить слушателя к recycler view;
  2. Загрузить первичные данные;
  3. Словить коллбэк, когда пользователь прокрутил список;
  4. Показать загрузку в списке после всех элементов;
  5. Отправить запрос на получение новых элементов;
  6. Снова отобразить данные.

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

Окей, сделать это один раз в одном приложении не проблема. Если есть несколько экранов, где необходим данный функционал, то при можно написать базовый адаптер, который умеет работать с дополнительным типом 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.

Видео с демонстрацией загрузки:


Благодарю за прочтение, приятного кодинга :)

Теги:
Хабы:
Всего голосов 10: ↑9 и ↓1+8
Комментарии6

Публикации

Истории

Работа

Swift разработчик
19 вакансий
iOS разработчик
17 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань