Android studio recyclerview layoutmanager

Лошадинная сила в Android или еще раз о RecyclerView.LayoutManager

По мнению автора, статья может быть полезна таким же как он начинающим Android-разработчикам, совершающим свои первые шаги в такой увлекательной области. История предмета этой заметки началась с идеи оснастить учебный проект так называемым “вау-эффектом”. Насколько это удалось, судить вам. Всех любопытствующих прошу под кат.

Демонстрационный проект со всем этим безобразием можно найти на GitHub по ссылке.

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

Впрочем, лучше один раз увидеть.

Рассмотрим элемент списка детально. В проекте он реализован как класс LaunchItemView унаследованный от CardView. Его разметка содержит следующие компоненты:

  • Изображение — класс ScaledImageView, предназначен для отрисовки изображения с заданной высотой (масштабированием).
  • Заголовок.
  • Поясняющий текст.

Рис. 2. Структура элемента списка (LaunchItemView).

В процессе прокручивания списка происходит следующее:

  1. Изменяется высота элемента от минимальной величины (равна высоте заголовка с декорированием) до максимальной величины (равна высоте, позволяющей отобразить заголовок и поясняющий текст, с декорированием).
  2. Высота изображения равна высоте элемента за вычетом декорирования, ширина изменяется пропорционально.
  3. Относительное положение внутри элемента и размер поясняющего текста остается неизменным.
  4. Величина масштабирования ограничена сверху минимальным размером, достаточным для отображения всего контента с учетом декорирования и снизу минимальным размером, достаточным для отображения заголовка с учетом декорирования.
  5. Масштабирование, отличное от граничных значений, применяется к верхнему полностью видимому элементу списка. Элементы выше него имеют максимальный масштаб, ниже — минимальный.

Таким образом при прокручивании вверх создается эффект постепенного “вытягивания” содержимого элемента с пропорциональным масштабированием изображения. При пролистывании вниз наблюдается обратный эффект.

Поставленную таким образом задачу я решил путем создания LayoutManager-а для RecyclerView и двух дополнительных компонентов. Но обо всем по порядку.

LaunchLayoutManager

Мой учебный проект посвящен космической тематике, поэтому компоненты получили соответствующие имена.

Изучая тему создания произвольного LayoutManager-а нашел две хорошие статьи на эту тему [1, 2]. Повторять их содержимое я не буду. Вместо этого остановлюсь на наиболее интересных моментах моего решения.

Выполняя декомпозицию задачи, я выделил два основных этапа:

  1. Определение индекса первого и последнего элемента, которые полностью или частично видимы на экране.
  2. Отрисовка видимых элементов с необходимым масштабированием.

В общем случае расположение и размер элементов списка выглядит следующим образом:

Рис 3. RecyclerView и его элементы.

Определение замкнутого интервала индексов видимых элементов

На рис. 3 к видимым относятся элементы с индексами от 3 до 11 включительно. Причем, согласно нашему алгоритму, элементы с индексами 0-3 имеют максимальный размер, элементы 5-12 — минимальный, а элемент с индексом 4 — промежуточный между минимальным и максимальным.

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

Рассмотрим метод calculateVisiblePositions, предназначенный для определения этих величин.

Строка 2 — проверяем, определена ли высота элемента максимального размера, отображающего весь контент (заголовок, поясняющий текст и изображение). Если нет, то и продолжать смысла нет.

Строка 3 — вычисляем, сколько займут места прокрученные вверх все элемента списка, кроме одного — максимально допустимое смещение. Эта величина будет ограничивать значение смещения в методе scrollVerticallyBy.

Строка 4 — вычисляем индекс первого видимого элемента. Поскольку переменная mFirstVisibleViewPosition принадлежит к целочисленному типу, то за счет отбрасывания дробной части автоматически учитываем случай частично видимого первого элемента.

Строки 5-8 — проверяется, не превышает ли индекс первого видимого элемента индекс последнего имеющегося элемента в списке. Такое может случится, например, когда список сначала был прокручен вверх, а потом количество элементов уменьшилось, например, за счет применения фильтра. В этом случае просто “перематываем” список на начало.

Строка 10 — используем индекс первого видимого элемента как отправную точку для поиска индекса последнего.

Строка 11 — устанавливаем высоту видимой области. Эта величина будет уменьшаться в ходе поиска максимального индекса видимых элементов.

Строки 12, 13 — определяем координаты top и bottom прямоугольника отрисовки первого элемента.

Строка 14 — уменьшаем величину свободной видимой области на размер видимой части первого элемента. Т.е. как бы виртуально размещаем первый элемент на экране.

Строка 15 — вычисляем высоту второго видимого элемента. Именно этот элемент потенциально подлежит масштабированию (см. п.5 алгоритма). Метод getViewHeightByTopValue детально рассмотрен ниже.

Строка 16 — проверяем, останется ли еще свободное место после “виртуального размещения” второго элемента на экране.

Строка 17 — фиксируем, сколько осталось свободного места.

Строка 18 — инкрементируем индекс последнего видимого элемента.

Строка 19 — вычисляем наибольшее число элементов минимального размера, которые могут поместится в оставшееся свободное место и при этом будут полностью видимы.

Строка 20 — увеличиваем индекс последнего видимого элемента на вычисленную величину.

Строки 21-24 — проверяем, есть ли место для частичного размещения еще одного элемента. Если да, увеличиваем индекс еще на единицу.

Читайте также:  Сколько процентов мобильных компьютеров используют операционные системы google android ios

Теперь о методе, который вычисляет высоту второго видимого элемента в зависимости от позиции на экране — координаты top прямоугольника отображения этого элемента.

Строка 2 — отбрасываем нижний и верхний margin.

Строки 3-7 — для корректного вычисления масштаба ограничиваем сверху величину top максимальной высотой элемента, а снизу нулем.

Строка 8 — вычисляем коэффициент масштабирования, который принимает значение 1 для максимально развернутого элемента и 0 — для минимального. Для корректности именно этого результата нам требуются ограничения в строках 3-7.

Строка 9 — вычисляем высоту элемента как прибавку к минимальной высоте и разницу между максимальной и минимальной высотой с учетом коэффициента масштабирования. Т.е. при коэффициенте 0 — высота минимальная, а при 1 — минимальная + (максимальная — минимальная) = максимальная.

Итак, теперь мы знаем первый и последний индексы элементов, подлежащих отрисовке. Самое время этим заняться!

Отрисовка элементов с необходимым масштабированием

Поскольку процесс отрисовки носит циклический характер, то непосредственно перед отрисовкой мы прогреем кэш уже существующими элементами RecyclerView (если таковые конечно есть). Такой прием освещен в [1, 2] и здесь на нем останавливаться не буду.

Рассмотрим метод fillDown, предназначенный для отрисовки элементов двигаясь сверху вниз по имеющейся видимой области.

Строка 3 — инициируем переменную topValue координатой top первого видимого элемента. От печки этого значения и будем плясать дальше.

Строка 7 — инициируем цикл по индексам элементов, подлежащих отрисовке.

Строка 8 — оптимистично предполагаем, что найдем нужный нам элемент в кэше.

Строка 9 — смотрим в кэш (с надеждой).

Строка 10-12 — если в кэше нужного нам элемента не оказалось, запрашиваем его у экземпляра класса RecyclerView.Recycler, который возвращает view, инициализированной данными из адаптера для конкретной позиции.

Строка 14 — если элемент все же был в кэше, удаляем его оттуда.

Строка 16 — вычисляем высоту элемента в зависимости от его положения на экране.

Строка 17 — вычисляем нижнюю границу элемента.

Строки 18-20 — масштабируем контент элемента, если он умеет это делать.

Строка 21 — для нас важно понять, была ли ранее отрисована текущая view (взята из кэша) или же мы получили новый экземпляр. Эти два варианта требуют различных подходов.

Строки 22-28 — если view получена из кэша, то при необходимости мы изменяем значения координат top и bottom, и присоединяем view.

Строка 30 — если view не из кэша, то для отображения элемента мы используем метод layoutView, который рассмотрен ниже.

Строка 32 — смещаем topValue на нижнюю границу только что отрисованной view, чтобы это значение стало отправной точкой для следующей итерации цикла.

Теперь о методе layoutView, предназначенном для отображения новенького элемента списка, полученного у экземпляра класса RecyclerView.Recycler.

Строка 2 — добавляем view в RecyclerView.

Строка 3 — измеряем view.

Строка 4 — определяем ширину view.

Строка 5 — получаем layout-параметры view.

Строка 7 — собственно отрисовываем view в полученных координатах.

Масштабирование содержимого

Из всей структуры элемента списка масштабированию подлежит только изображение. Логика этого масштабирования инкапсулирована в класс ScaledImageView, унаследованный от View.

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

Будем использовать PublishProcessor для создания потока целочисленных значений, которые определяют требуемую высоту изображения:

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

А вот как происходит асинхронная обработка этого потока:

Строка 3 — выполняем первоначальную фильтрацию:

  • Отбрасываем значения меньше или равные нулю.
  • Не рассматриваем значения, величина которых равна высоте изображения, масштабированного ранее. Т.е. тот случай, когда масштабирование уже не требуется.
  • Не выполняем масштабирование, когда оригинальное изображение не инициализировано. Инициализация рассмотрена ниже в методе setBitmap.

Строка 4 — используем обратное давление с размером буфера 1 элемент и стратегией вытеснения из буфера более старого элемента. За счет этого мы будем получать всегда самое актуальное значение высоты для масштабирования. В нашем случае это очень важно, поскольку имеем горячий источник, который в ответ на действия пользователя (например, интенсивное прокручивание списка), будет порождать элементы быстрее, чем сможем их обработать (выполнять масштабирование). В таких условиях нет смысла накапливать значения в буфере и обрабатывать эти элементы последовательно, поскольку они уже устарели, пользователь уже “промотал” это состояние.

Для иллюстрации и усиления эффекта я добавил задержку 25 мс в метод масштабирования изображения (createScaledBitmap) и ниже привел две визуализации: без использования обратного давления (слева) и с обратным давлением (справа). Интерфейс слева явно отстает от действий пользователя, живет какой-то своей жизнью. Справа — потерял в плавности из-за дополнительной задержке в методе масштабирования, но не в отзывчивости.

Без обратного давления С обратным давлением

Строка 6 — переносим работу в поток Schedulers.computation() с указанием размера буфера.

Строка 7 — выполняем масштабирование (описание метода см. ниже).

Строка 8 — устанавливаем масштабированную картинку для отображения.

Строка 9 — подписываемся на поток.

Строка 11 — в завершении масштабирования выполняем перерисовку элемента.

Метод createScaledBitmap, непосредственно занимающийся получением изображения нужного размера:

Строки 3-5 — ограничим максимальную высоту размером view, который вычисляется в методе onMeasure.

Читайте также:  Поисковик google для android

Строка 6 — создаем изображение нужного размера из оригинала.

В методе setScaledBitmap сохраняем масштабированное изображения для отображения во view:

Строки 3, 12 — используем блокировку для синхронизации обращения к переменной, содержащей изображение, которое подлежит отрисовке на экране.

Строки 4-6 — утилизируем ранее созданное изображение.

Строки 7-8 — запоминаем новое изображение и его размер.

Метод setBitmap устанавливает оригинальное изображение:

Строка 3 — масштабируем оригинальное изображение до размеров view. Это позволит нам сохранить ресурсы при выполнении масштабирования в методе createScaledBitmap если оригинальное изображение превосходит view по размерам.

Строка 4-6 — утилизируем старое оригинальное изображение.

Строки 7-9 — обнуляем высоту для масштабирования, чтобы преодолеть фильтр в методе initObserver, и продуцируем элемент потока для перерисовки нового изображения в требуемом масштабе.

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

Источник

Рецепты под Android: Как вкусно приготовить LayoutManager

Мы любим разрабатывать мобильные приложения, отличающиеся от своих собратьев как по функциям, так и по пользовательскому интерфейсу. В прошлый раз мы рассказали о клиент-серверном взаимодействии в одном из наших приложений, а в этот раз поделимся реализацией его UI фичи с помощью написанного с нуля LayoutManager. Думаем, что статья будет полезна не только начинающим андроид-разработчикам, но и более продвинутым специалистам.

Начнём-с

Если вы — android-разработчик, то вы наверняка уже использовали RecyclerView, мощную и невероятно кастомизируемую замену ListView и GridView. Одна из степеней кастомизации RecyclerView заключается в том, что он ничего не знает о расположении элементов внутри себя. Эта работа делегирована его LayoutManager’у. Google предоставил нам 3 стандартных менеджера: LinearLayoutManager для списков как в ListView, GridLayoutManager для плиток, сеток или таблиц и StaggeredGridLayoutManager для лэйаута как в Google+. Для нашего приложения нужно было реализовать лэйаут, который не вписывался в рамки доступных лэйаут менеджеров, поэтому мы решили попробовать написать свой. Оказалось, создавать свой LayoutManager подобно наркотику. Однажды попробовав, уже сложно остановиться — настолько он оказался полезным при решении нестандартных задач верстки.

Итак, задача. В нашем учебном приложении будут статьи очень простого формата: картинка, заголовок и текст. Мы хотим иметь вертикальный список статей, каждая карточка в котором будет занимать 75% от высоты экрана. Помимо вертикального, будет и горизонтальный список, в котором каждая статья будет открыта на весь экран. Переход из вертикального режима в горизонтальный будет происходить анимировано по клику на какую-либо карточку и кнопкой back — обратно в вертикальный. А еще, для красоты, в вертикальном режиме нижняя карточка при прокрутке будет выезжать с эффектом масштабирования. Кстати, наш учебный проект вы можете посмотреть здесь: https://github.com/forceLain/AwesomeRecyclerView, в нём уже есть фэйковый DataProvider, возращающий 5 ненастоящих статей, все лэйауты и, собственно, сам LayoutManager 🙂
Представим, что Activity с RecyclerView в ней, а также RecyclerView.Adapter, создающий и заполняющий статьи-карточки мы уже написали (или скопировали из учебного проекта) и пришло время создавать свой LayoutManager.

Пишем основу

Первое, что придется сделать — это реализовать метод generateDefaultLayoutParams(), который будет возвращать нужные LayoutParams для views, чьи LayoutParams нам не подходят

Основная магия происходит в методе onLayoutChildren(. ), который является точкой старта для добавления и расположения наших вьюшек. Для начала научимся располагать хотя бы одну статью.

В первой строчке мы просим recycler дать нам view для первой позиции. Затем, Recycler сам определяет, вернуть ли её из внутреннего кэша или создать новую. На второй строчке задержимся подольше.

Если вам уже доводилось создавать собственные view, вы наверняка знаете как добавить внутри своей view еще одну дочернюю. Для этого нужно дочернюю view добавить в свой layout (как во второй строчке), затем измерить, вызвав у неё метод measure(. ) и, в конце концов, расположить в нужном месте, вызвав у неё метод layout(. ) с нужными размерами. Если же вы еще ни разу не делали ничего подобного, то теперь вы примерно представляете, как это происходит 🙂 Что касается RecyclerView, здесь дело принимает другой оборот. Почти для всех стандартных методов View-класса, связанных с размерами и лэйаутом у RecyclerView есть альтернативные, которые и нужно использовать. Прежде всего они нужны потому, что в RecyclerView есть класс ItemDecoration, с помощью которого можно менять размеры вьюшек, а эти альтернативные методы берут в расчет все установленные декораторы.
Вот некоторые примеры альтернативных методов:

Итак, в третей строчке мы позволяем вьюшке посчитать свои размеры, а в четвертой располагаем её в лэйауте от верхнего левого угла (0, 0) до нижнего правого (getWidth(), getHeight()).
Для измерения размеров вьюшки мы воспользовались готовым методом measureChildWithMargins(. ). На самом деле он не совсем нам подходит, поскольку выполняет измерения, принимая в расчет ширину и высоту, указанную в LayoutParams у дочерней вьюшки. А там может быть что угодно: wrap_content, match_parent или даже задан в dp. Но мы то условились, что все карточки у нас будут фиксированного размера! Так что придется нам написать свой measure, не забыв при этом про существование декораторов:

Теперь наш onLayoutChildren() выглядит так:

С помощью MeasureSpec мы сообщаем нашей view, что её высота и ширина должна и будет равна высоте и ширине RecyclerView. Разумеется, чтобы нарисовать статью высотой в 75% высоты экрана, нужно в layoutDecorated() передать эту самую высоту:

Читайте также:  Dark reader для андроид

Теперь, если мы установим наш LayoutManager в RecyclerView и запустим проект мы увидим одну статью на три четверти экрана

Теперь попробуем рисовать вюшки-статьи, начиная с первой (нулевой) и располагая их друг под другом, до тех пор, пока не кончится экран по вертикали или не кончатся элементы в адаптере.

Выглядит готовым, однако до сих пор мы не сделали одну очень важную вещь. Ранее мы говорили, что recycler сам определяет, брать ли ему вьюшки из кэша либо создавать новые, но на самом то деле кэш у него до сих пор пуст, так как мы в него еще ничего не положили. Добавим вызов detachAndScrapAttachedViews(recycler) в onLayoutChildren() на первое место перед fillDown().

Этот метод убирает все view из нашего лэйаута и помещает их в свой специальный scrap-кэш. При необходимости можно вернуть view, используя метод recycler.getViewForPosition(pos).

They See Me Rollin’

Теперь хорошо бы научить наш LayoutManager скроллиться.
Во-первых, скажем нашему LayoutManager’у, что мы хотим прокручиваться по вертикали:

Затем реализуем сам вертикальный скролл

На вход этого метода мы получаем dy — расстояние, на которое нужно проскроллить. Вернуть мы должны то расстояние, на которое мы действительно прокрутили наши вьюшки. Это нужно для того, что бы не дать контенту уехать за границы экрана. Давайте сразу напишем алгоритм, определяющий можем ли мы еще скроллить и на какое расстояние:

Теперь мы можем проскроллить наши 2 добавленные статьи, но при прокручивании новые статьи на экран не добавляются. Алгоритм добавления новых вьюшек во время скролла может показаться заковыристым, но это только на первый взгляд. Попытаемся сначала описать его словами:

  1. Сначала сдвигаем все имеющиеся вьюшки на dy с помощью offsetChildrenVertical(-dy)
  2. Выбираем одну из имеющихся в лэйауте вьюшек как “якорную” и запоминаем её и её позицию. В нашем случае мы будем выбирать в качестве якорной вьюшки ту, которая полностью видна на экране. Если такой нет, то выбираем ту, видимая площадь которой максимальна. Такой способ определения якорной вьюшки поможет нам и в будущем, при реализации смены ориентации нашего лэйаут-менеджера
  3. Убираем все имеющиеся в лэйауте вьюшки, помещая их в собственный кэш и запоминая на каких позициях они были
  4. Добавляем в лэйаут вьюшки выше той позиции, которую взяли за якорную. Потом добавляем якорную и всё, что должно быть ниже неё. Вьюшки в первую очередь берем из своего кэша и, если не находим, просим у recycler

ПРИМЕЧАНИЕ: реализация скролла и добавление view в лэйаут — дело индивидуальное. С тем же успехом можно было бы взять за якорную вьюшку самую верхнюю и заполнять экран вниз от неё. А если бы вы хотели сделать такой LayoutManager, который ведет себя как ViewPager, вам бы вообще не пришлось добавлять вьюшки во время прокрутки, а только в перерывах между свайпами.

Обратите внимание, что внутри fillUp() вьюшки добавляются методом addView(view, 0), а не addView(view), как раньше. Сделано это было для того, чтобы сохранить порядок элементов внутри лэйаута — чем выше view, тем меньше должен быть её порядковый номер.

Wow-эффект

К этому моменту у нас получился вполне рабочий LayoutManager, который ведет себя как ListView. Теперь добавим в него эффект масштабирования нижней карточки. Для этого достаточно всего одного метода!

Разместите этот метод внутри fill() в последнюю очередь. Он устанавливает scale Animation

ViewAnimationInfo это просто класс-структура для удобного хранения разных значений:

Вот, что происходит внутри openView: для каждой вьюшки на экране мы запоминаем её верх и низ, а так же рассчитываем её верх и низ, на которые эта вьюшка должна “уехать”. Затем создаем и запускаем ValueAnimator, который отдает нам прогресс от 0 до 1, на основе которого мы считаем верх и низ для каждой вьюшки во время анимации и выполняем layoutDecorated(. ) с нужными значениями в каждом цикле анимации. В тот момент, когда анимация закончится, вызываем setOrientation(Orientation.HORIZONTAL) для окончательного перехода в горизонтальный режим. Плавно и незаметно.

Снимаем пробу

Жаль, что не удастся разместить все полезные сведения о LayoutManager’е в рамках всего одной статьи. При надобности, что-то вы сможете подглядеть в нашем учебном проекте (например, реализацию smoothScrollToPosition()), а что-то придется поискать самостоятельно.

В заключении хочется сказать, что LayoutManager это чрезвычайно мощный и гибкий инструмент. RecyclerView + CustomLayoutManager уже не раз приходил нам на помощь при решении очень нестандартных дизайнерских задач. Он открывает просторы для анимации как самих вьюшек, так и контента в них. К тому же, он значительно расширяет возможности оптимизации. К примеру, если пользователь хочет выполнить smoothScroll() от 1-го элемента до 100-го, совершенно необязательно по-честному прокручивать все 99 элементов. Можно схитрить и перед началом скролла добавить 100-ый элемент в лэйаут, а затем проскроллить до него, экономя кучу ресурсов!
Однако, LayoutManager совсем не так прост для освоения с нуля. Для эффективного его использования нужно хорошо представлять, как создаются кастомные view, как работают measure/layout циклы, как пользоваться MeasureSpec и прочее в таком же духе.

Источник

Оцените статью