Android recyclerview item height
While playing around with RecyclerView I found that there was no easy method to determine proportional height (with the height of the screen) of the list items. After trying various approaches I found that the best way to achieve this and the way that worked the best for the application that I was doing at the moment, was determining height of the item of the RecyclerView in the adapter.
I’ve written simple utility function that determines the height of the screen and then I have predefined ratio (that can be subject to change from cached value that is inputted by the user at some point).
Here is the method:
The method is applied in the adapter (in the onBindViewHolder method):
The item layout is defined in the following manner:
The ratio can also be determined in correlation to the downloaded image dimensions (landscape/portrait), something that will yield additional changes
The example application is actually a reddit client (feed fetching interaction is managed with rest2mobile) that displays graphic submittions (images) and respective submittion title. When the user clicks on settings it can change the ratio of the items in the RecyclerView and change the subreddit.
The code of the application can be found on GitHub.
Источник
О RecyclerView и выделении элементов
Содержание
1. Немного о ViewHolder’ах
До выхода в свет Android SDK 5.0 Lollipop для отображения списков и таблиц использовались виджеты ListView и GridView. Общей рекомендацией при работе с этим виджетом было использование паттерна ViewHolder. Суть паттерна заключается в том, что для каждого элемента списка создаётся объект, хранящий ссылки на отдельные вьюхи внутри элемента. Таким образом, приходится выполнять достаточно дорогой вызов findViewById(int) только один раз при создании элемента.
Пример типичного ViewHolder’а прямиком из руководств гугла:
Cсылка на такой холдер для каждого элемента сохраняется в корневом layout’е, используя метод setTag(int, Object) (с моей точки зрения тот ещё костыль).
2. Вкратце о RecyclerView
К выходу Android SDK 5.0 Lollipop разработчиков Google наконец-то озарило, что два вышеперечисленных виджета морально устарели и нужно бы заменить их на нечто более стильное, модное и молодёжное. Было принято решение не переделывать старые виджеты, а написать новый. Так и появился на свет RecyclerView. Так в чём же его отличия, спросите вы?
Я приведу вкратце основные, а для более полного их раскрытия советую к ознакомлению вот эту статью на хабре. Итак:
- Сам виджет больше не берёт на себя обязанность по размещению элементов. Для этого появились LayoutManager’ы.
- Паттерн ViewHolder стал обязательным. Причём виджет научился заново использовать уже созданные ViewHolder’ы и удалять уже не используемые (отсюда и название), что благоприятно сказывается на быстродействии и размере используемой памяти.
- Новый, удобный способ работы с анимацией.
Я попробовал его и виджет оставил у меня противоречивые впечатления. С одной стороны, да, здорово, что теперь использование ViewHolder’а является обязательным, работает вроде тоже быстрей, памяти жрёт меньше. С другой стороны, есть проблемы со сложностью и недоделанностью виджета.
Что я понимаю под сложностью? Если что-то не работало в ListView (или работало не так как задумано) всегда можно было залезть в исходники, разобраться, в чём ошибка, исправить её, подоткнуть костылей тут и там и всё начинало работать. RecyclerView гораздо сложнее в плане логики работы, мозг сломаешь, пока разберёшься. Я пытался, но забросил, уж слишком много времени и сил для этого нужно.
Вторая проблема — банальное отсутствие функционала, присутствовавшего в ListView и GridView. За примерами далеко ходить не надо — стандартный функционал выделения элементов (дальнейшая тема этой статьи), отступы между элементами. Раньше, чтобы добавить всё это, нужно было написать буквально пару строчек кода, теперь на это уйдут уже десятки строк. Есть анимации, но только для добавления/удаления/редактирования элемента. Если же вы хотите, например, анимировать частичное изменение элемента, то к вам в дверь уже стучится птица обломинго. Виджет не поддерживает анимацию части элемента, и если анимировать элемент извне (из адаптера, например), то лучше этого не делать — подобные манипуляции оставляют элементы виджета (те самые ViewHolder’ы) в неопределённом состоянии, что приводит к совершенно фантастическому поведению вашего списка.
Резюмируя — если у вас в проекте используются старые виджеты и вы не используете анимации, то лучше пока оставить всё как есть и дождаться, когда виджет наполнят отсутствующим функционалом. Если же вы хотите простые анимации и при этом взаимодействие пользователя с виджетом подразумевается простое — попробуйте RecyclerView, вдруг понравиться.
3. Выделяем элементы
Итак, перейдём к главному — к технической части статьи. Поговорим о том, как выделять элементы в RecyclerView. Сразу оговорюсь — идея реализации почерпнута из замечательной серии статей Билла Филлипса про RecyclerView (ссылки в конце), так что всё нижеследующее можно считать вольным кратким пересказом.
В ListView для выделения элементов использовался метод setChoiceMode(int), RecyclerView же понятия не имеет, что элементы могут выделяться, поэтому мы должны научить этому наш адаптер.
Схема такая:
На диаграмме я схематично обозначил связи между объектами. Пунктирные стрелки — ссылки, остальные — вызовы методов. Зелёным я обозначил объекты, которые непосредственно реализуют логику выделения.
Принцип работы получается следующий:
- ViewHolderWrapper устанавливает себя в качестве ClickListener’а для корневой вьюхи ViewHolder’а и начинает получать события onClick и onLongClick. В зависимости от реализации он может просто проксировать эти события в HolderClickObservable (ViewHolderClickWrapper), либо, исходя из текущего статуса SelectionHelper’а выделять элемент вызовом setItemSelected(ViewHolder, boolean) (ViewHolderMultiSelectionWrapper).
- SelectionHelper сохраняет информацию о выделенных элементах и оповещает слушателей (SelectionObserver) об изменении выделения.
- Слушатель (в нашем случае адаптер) отвечает за визуальное отображение выделения элемента, а также взаимодействия с ним (на диаграмме вызов startActionMode у Activity).
В самом адаптере необходимо сделать следующие изменения:
1. Создать SelectionHelper и зарегистрировать слушателей (в данном случае сам адаптер, но это может быть и Activity, например)
2. Обернуть создаваемые ViewHolder’ы во ViewHolderWrapper нужного типа.Метод wrapSelectable(ViewHolder) у SelectionHelper’а:
3. Не забывать прицеплять наши ViewHolder’ы к SelectionHelper’у в методе onBindViewHolder(ViewHolder, int) нашего адаптера!
Это нужно по причине того, что пока нет другого способа получить от RecyclerView список используемых в настоящий момент ViewHolder’ов. Если не вести их учёт, при необходимости обновить отрисовку выделения у всех выбранных элементов (пользователь закрыл ActionMode, например), SelectionHelper просто не сможет этого сделать. Вьюхи останутся выглядеть выделенными, когда по факту таковыми не будут.
Вы спросите — «А почему бы просто не запоминать выделяемые ViewHolder’ы в методе setItemSelected(ViewHolder, boolean)?». Тут как раз сказывается особенность RecyclerView — он использует заново уже созданные ViewHolder’ы.
Выглядит это примерно так:
- Открываем приложение. На экране 10 элементов — 10 ViewHolder’ов создано для них.
- Запускаем ActionMode, начинаем выделять элементы — 1,2,3.
- Прокручиваем вьюху вниз, видим элементы с 10 по 20. Думаете, что теперь в памяти висит 20 ViewHolder’ов? Как бы ни так! Для части данных RecyclerView создаст новые ViewHolder’ы, а для другой заново использует уже имеющиеся. Причём неизвестно в каком порядке.
- Теперь если мы прокрутим вьюху обратно вверх, часть из наших 10 ViewHolder’ов будет уничтожена, вместо них будут созданы новые. Оставшаяся часть будет использована заново и совершенно не обязательно для тех же позиций.
- Отменяем ActionMode. SelectionHelper должен раскидать слушателям уведомления о сменившемся выделении на элементах с указанием ViewHolder’а для каждого элемента, но он уже не владеет актуальными данными, все Holder’ы поменялись!
В результате это приведёт к тому, что часть элементов останется отображаться с выделением.
И здесь становится очевидным ещё один важный момент — нельзя сохранять строгие ссылки (strong reference) на ViewHolder’ы! Они могут быть удалены из RecyclerView в зависимости от фазы Луны и желания левой пятки Ларри Пейджа. В этом случае, если мы будем хранить строгие ссылки на них, случится утечка памяти. Поэтому для хранения ссылок в ViewHolderWrapper и WeakHolderTracker используются только WeakReference.
4. Также важно не забыть в onBindViewHolder(ViewHolder, int) визуально отобразить выделение если оно есть (если нет — не забыть убрать!). Вы же помните, что для не выделенного элемента может быть использован ViewHolder, ранее использовавшийся для не выделенного и наоборот?
У меня это реализовано следующим образом:
4.1. SelectableRecyclerViewAdapter.onBindViewHolder(ViewHolder, int)
4.2. layout-файл для элемента
CheckableAutofitHeightFrameLayout добавляет к FrameLayout всего 2 вещи: во-первых, он всегда квадратный (смотри onMeasure(int, int)) и, во-вторых, добавляет к DrawableStates (те самые, которые используются в xml) состояние state_checked. В результате, для отображения выделения у такого layout’а можно использовать StateListDrawable на вроде этого:и все детали отображения уползают в xml-ки, в Java только нужно установить соответствующие состояния.
5. Передать событие onSelectableChanged(boolean) в Activity и запустить ActionMode:
Как вы видите, при запуске ActionMode, она регистрирует себя как SelectionObserver. Таким образом, можно обновлять количество выделенных элементов в заголовке. Не забудьте вызвать unregisterSelectionObserver(SelectionObserver) при закрытии!
4. Заключение + Бонус
Кажется, с выделением разобрались. Весь исходный код также можно посмотреть на GitHub.
В заключение вкратце приведу ещё несколько фишек для работы с RecyclerView, которые вы можете найти в примере.
1. Если не нужно выделять элементы, а нужно просто обрабатывать нажатия, вместо ViewHolderMultiSelectionWrapper оборачивайте элементы в ViewHolderClickWrapper методом wrapClickable(ViewHolder). Сам адаптер в таком случае будет выглядеть примерно так:
Виджет подбирает ширину столбцов в зависимости от параметра columnWidth. Важный момент: если доступная ширина 330 пикселей, а мы передадим желаемую ширину 100, в итоге в таблице будет 3 столбца по 110 пикселей и элементы будут этой ширины. Именно поэтому я также сделал CheckableAutofitHeightFrameLayout автоматически изменяющим свою высоту в зависимости от ширины.
3. Для добавления отступов между элементами можно выставить paddingTop/Left у RecyclerView и marginRight/Bottom у элементов, однако это выглядит как костыль. Рекомендуемым способом является добавление ItemDecoration к RecyclerView. В примере можно найти несколько. Для добавления отступов к обычному GridLayoutManager (под «обычным» я имею ввиду GridLayoutManager со стандартным SpanSizeLookup, в нём каждый элемент занимает 1 span) можно использовать
Источник
RecyclerView.ItemDecoration: используем по максимуму
Привет, дорогой читатель Хабра. Меня зовут Олег Жило, последние 4 года я Android-разработчик в Surf. За это время я поучаствовал в разного рода крутых проектах, но и с легаси-кодом поработать довелось.
У этих проектов есть как минимум одна общая деталь: везде при разработке приложения использовался список с элементами. Например, список контактов телефонной книги или список настроек вашего профиля.
В наших проектах для списков используется RecyclerView. Я не буду рассказывать, как писать Adapter для RecyclerView или как правильно обновлять данные в списке. В своей статье расскажу о другом важном и часто игнорируемом компоненте — RecyclerView.ItemDecoration, покажу как его применить при вёрстке списка и на что он способен.
Кроме данных в списке в RecyclerView есть ещё важные элементы декора, например, разделители ячеек, полосы прокрутки. И вот тут нам поможет RecyclerView.ItemDecoration отрисовать весь декор и не плодить лишние View в вёрстке ячеек и экрана.
ItemDecoration представляет из себя абстрактный класс с 3-мя методами:
Метод для отрисовки декора до отрисовки ViewHolder
Метод для отрисовки декора после отрисовки ViewHolder
Метод для выставления отступов у ViewHolder при заполнении RecyclerView
По сигнатуре методов onDraw* видно, что для отрисовки декора используется 3 основных компонента.
- Canvas — для отрисовки необходимого декора
- RecyclerView — для доступа к параметрам самого RecyclerVIew
- RecyclerView.State — содержит информацию о состоянии RecyclerView
Подключение к RecyclerView
Для подключения экземпляра ItemDecoration к RecyclerView есть два метода:
Все подключенные экземпляры RecyclerView.ItemDecoration добавляются в один список и отрисовываются сразу все.
Также RecyclerView имеет дополнительные методы для манимуляции с ItemDecoration.
Удаление ItemDecoration по индексу
Удаление экземпляра ItemDecoration
Получить ItemDecoration по индексу
Получить текущее количество подключенных ItemDecoration в RecyclerView
Перерисовать текущий список ItemDecoration
В SDK уже есть наследники RecyclerView.ItemDecoration, например, DeviderItemDecoration. Он позволяет отрисовать разделители для ячеек.
Работает очень просто, необходимо использовать drawable и DeviderItemDecoration отрисует его в качестве разделителя ячеек.
И подключим DividerItemDeoration к RecyclerView:
Идеально подходит для простых случаев.
Под «капотом» DeviderItemDecoration всё элементарно:
На каждый вызов onDraw(. ) циклом проходим по всем текущим View в RecyclerView и отрисовываем переданный drawable.
Но экран может содержать и более сложные элементы вёрстки, чем список одинаковых элементов. На экране могут присутствовать:
а. Несколько видов ячеек;
b. Несколько видов дивайдеров;
c. Ячейки могут иметь закругленные края;
d. Ячейки могут иметь разный отступ по вертикали и горизонтали в зависимости от каких-то условий;
e. Всё вышеперечисленное сразу.
Давайте рассмотрим пункт e. Поставим себе сложную задачу и рассмотрим её решение.
- На экране есть 3 вида уникальных ячеек, назовём их a, b и с.
- Все ячейки имеют отступ в 16dp по горизонтали.
- Ячейка b имеет ещё отступ в 8dp по вертикали.
- Ячейка a имеет закруглённые края сверху, если это первая ячейка в группе и снизу, если это последняя ячейка в группе.
- Между ячейками с отрисовываются дивайдеры, НО после последней ячейки в группе дивайдера быть не должно.
- На фоне ячейки c рисуется картинка с эффектом параллакса.
Должно в итоге получиться так:
Рассмотрим варианты решения:
Заполнение списка ячейками разного типа.
Можно написать свой Adapter, а можно использовать любимую библиотеку.
Я буду использовать EasyAdapter.
Выставление отступов ячейкам.
Тут есть три способа:
- Проставить paddingStart и paddingEnd для RecyclerView.
Данное решение не подойдёт, если не у всех ячеек отступ одинаковый. - Проставить layout_marginStart и layout_marginEnd у ячейки.
Придётся всем ячейкам в списке проставлять одни и те же отступы. - Написать реализацию ItemDecoration и переопределить метод getItemOffsets.
Уже лучше, решение получится более универсальное и переиспользуемое.
Закругление углов у групп ячеек.
Решение кажется очевидным: хочется сразу добавить какой-нибудь enum
- Модель данных в списке усложняется.
- Для таких манипуляций придётся заранее просчитывать какой enum проставлять каждой ячейке.
- После удаления/добавления элемента в список придётся это пересчитывать заново.
- ItemDecoration. Понять какая это ячейка в группе и правильно отрисовать фон можно в методе onDraw* ItemDecoration’a.
Рисование дивайдеров.
Рисование дивайдеров внутри ячейки — плохая практика, так как в итоге получится усложненная вёрстка, на сложных экранах начнутся проблемы с динамическим показом дивайдеров. И поэтому ItemDecoration снова выигрывает. Готовый DeviderItemDecoration из sdk нам не подойдёт, так как отрисовывает дивайдеры после каждой ячейки, и это никак не решается из коробки. Надо писать свою реализацию.
Паралакс на фоне ячейки.
На ум может прийти идея проставить RecyclerView OnScrollListener и использовать какую-нибудь кастомную View для отрисовки картинки. Но и здесь нас снова выручит ItemDecoration, так как он имеет доступ к Canvas Recycler’а и ко всем нужным параметрам.
Итого, нам необходимо написать как минимум 4 реализации ItemDecoration. Очень хорошо, что все пункты можем свести к работе только с ItemDecoration и не трогать вёрстку и бизнес логику фичи. Плюс, все реализации ItemDecoration получится переиспользовать, если у нас есть похожие кейсы в приложении.
Однако последние несколько лет у нас в проектах всё чаще появлялись сложные списки и приходилось писать каждый раз набор ItemDecoration под нужды проекта. Было необходимо более универсальное и гибкое решение, чтобы была возможность переиспользовать на других проектах.
Каких целей хотелось добиться:
- Писать как можно меньше наследников ItemDecoration.
- Отделить логику отрисовки на Canvas и выставления отступов.
- Иметь преимущества работы с методами onDraw и onDrawOver.
- Сделать более гибкие в настройке декораторы (например, отрисовка дивайдеров по условию, а не всех ячеек).
- Сделать решение без привязки к Дивайдерам, ведь ItemDecoration способен на большее, чем рисование горизонтальных и вертикальных линий.
- Этим можно легко пользоваться, смотря на сэмпл проект.
В итоге у нас получилась библиотека RecyclerView decorator.
Библиотека имеет простой Builder интерфейс, отдельные интерфейсы для работы с Canvas и отступами, также возможность работать с методами onDraw и onDrawOver. Реализация ItemDecoration всего одна.
Давайте вернёмся к нашей задаче и посмотрим, как её решить с помощью библиотеки.
Builder нашего декоратора выглядит просто:
- .underlay(. ) — нужен для отрисовки под ViewHolder.
- .overlay(. ) — нужен для отрисовки над ViewHolder.
- .offset(. ) — используется для выставления отступа ViewHolder.
Для отрисовки декора и выставления отступов используется 3 интерфейса.
- RecyclerViewDecor — отрисовывает декор на RecyclerView.
- ViewHolderDecor — отрисовывает декор на RecyclerView, но даёт доступ к ViewHolder.
- OffsetDecor — используется для выставления отступов.
Но это не всё. ViewHolderDecor и OffsetDecor можно привязать к конкретному ViewHolder с помощью viewType, что позволяет комбинировать несколько видов декоров на одном списке и даже ячейке. Если viewType не передавать, то ViewHolderDecor и OffsetDecor будут применяться ко всем ViewHolder в RecyclerView. RecyclerViewDecor такой возможности не имеет, так как рассчитан на работу с RecyclerView в общем, а не с ViewHolder’ами. Плюс один и тот же экземпляр ViewHolderDecor/RecyclerViewDecor можно передавать как в overlay(. ) так underlay(. ).
Приступим к написанию кода
В библиотеке EasyAdapter для создания ViewHolder используются ItemController’ы. Если коротко, они отвечают за создание и идентификацию ViewHolder. Для нашего примера хватит одного контроллера, который может отображать разные ViewHolder. Главное, чтобы viewType был уникальный для каждой вёрстки ячейки. Выглядит это следующим образом:
Для выставления отступов нам нужен наследник OffsetDecor:
Для отрисовки закруглённых углов у ViewHolder нужен наследник ViewHolderDecor. Тут нам понадобится OutlineProvider, чтобы press-state тоже обрезался по краям.
Для рисования дивайдеров напишем ещё одного наследника ViewHolderDecor:
Для настройки нашего дивадера будем использовать класс Gap.kt:
Он поможет настроить цвет, высоту, горизонтальные отступы и правила отрисовки дивайдера
Остался последний наследник ViewHolderDecor. Для рисования картинки эффектом параллакса.
Соберём теперь всё вместе.
Инициализируем RecyclerView, добавим ему наш декоратор и контроллеры:
На этом всё. Декор нашего списка готов.
У нас получилось написать набор декораторов, которые можно легко переиспользовать и гибко настраивать.
Посмотрим как ещё можно применить декораторы.
PageIndicator для горизонтального RecyclerView
ChatActivityView.kt |
TimeLineActivity.kt |
Заключение
Несмотря на простоту интерфейса ItemDecoration, он позволяет делать сложные вещи со списком без изменения вёрстки. Надеюсь мне удалось показать, что это достаточно мощный инструмент и он достоин вашего внимания. А наша библиотека поможет вам декорировать списки проще.
Всем большое спасибо за внимание, буду рад вашим комментариям.
UPD: 06.08.2020 добавлен пример для Sticky header
Источник