Это третья часть цикла статей про разработку адаптеров для RecyclerView c BRVAH.
Прошлые части:
В этой части рассмотрю:
Анимацию появления элементов
Использование нескольких layout в одном списке
Один из частых запросов бизнеса в целом и дизайна в частности – добавить анимацию появления элементов в списке. В этой библиотеке предусмотрено пять готовых анимаций
AlphaInAnimation() – появление элемента из невидимости
ScaleInAnimation() – появление элемента с его увеличением
SlideInBottomAnimation() – появление элемента снизу
SlideInLeftAnimation() – появление элемента слева
SlideInRightAnimation() – появление элемента справа
Есть возможность создать свою анимацию унаследовавшись от BaseAnimation
Добавление анимации происходит в одну строку кода
adapter.adapterAnimation = SlideInRightAnimation()
Результат:
Дальше рассмотрю потребность в использовании разных layout для одного списка, это может использоваться для:
Секционирования списка подзаголовками
Выделения элемента, например «премиум» объявления, на доске объявлений
Рассмотрю первую потребность. Layout, для вывода заголовка содержит в себе только TextView, для отображения текста:
Создал новый адаптер. Для секционированного списка, адаптер наследуется от BaseSectionQuickAdapter. При инициализации, основное отличие в том, что в его конструктор необходимо еще передать layout заголовка. Еще значимое отличие, при типизации адаптера, DataClass элемента реализовывает интерфейс SectionEntity, про него расскажу позже. При реализации адаптера необходимо переопределить еще метод convertHeader. В нем происходит заполнение данными layout заголовка.
class SectionAdapter(data: MutableList<NotificationWithSectionsDTO>) : BaseSectionQuickAdapter<NotificationWithSectionsDTO, BaseViewHolder>(R.layout.item_section_header, R.layout.item_notification_with_image, data) {
init {
addChildClickViewIds(R.id.ivState)
}
override fun convert(holder: BaseViewHolder, item: NotificationWithSectionsDTO) {
holder.setGone(R.id.view, holder.layoutPosition == 0)
.setText(R.id.tvDateTime, item.date)
.setText(R.id.tvDsc, item.text)
.setImageResource(
R.id.ivState,
if (item.isRead) R.drawable.ic_delete
else R.drawable.ic_read
)
val imageView = holder.getView<ImageView>(R.id.imageView)
val context = holder.itemView.context
Glide.with(context)
.load(item.imageUrl)
.circleCrop()
.into(imageView)
}
override fun convertHeader(helper: BaseViewHolder, item: NotificationWithSectionsDTO) {
helper.setText(R.id.tvHeader, item.titleText)
}
}
Перейду к DataClassу. Он реализует интерфейс SectionEntity. В нем единственный метод isHeader - определяет является ли элемент заголовком. Опишу новый DataClass
data class NotificationWithSectionsDTO(
val date: String = "",
val text: String = "",
var isRead: Boolean = false,
val imageUrl: String = "",
val titleText: String = "",
) : SectionEntity {
override val isHeader: Boolean
get() = titleText.isNotBlank()
}
Инициализация адаптера не отличается от инициализации в прошлых статьях
Результат:
Секционирование – это частный случай использования списка с различными элементами, ограниченное двумя layout. Рассмотрю более обширный случай использования нескольких элементов.
Для этого, сделаю подобие доски объявлений, с обычными и «премиум» объявлениями.
Начну с DataClass. Для реализации мультиэлементности класс данные реализовывает интерфейс MultiItemEntity. В нем необходимо переопределить один метод itemType: Int, возвращающий тип элемента.
data class MessageDTO(
val text: String,
val price: String,
val image: String,
val mayAgreement: Boolean = false,
val type: Int = typeNotPremium
) : MultiItemEntity {
companion object {
const val typePremium = 0
const val typeNotPremium = 1
}
override val itemType: Int
get() = this.type
}
Создал два типа объявлений «премиум» и «не премиум», itemType возвращает их.
Создал адаптер. В этом случае, адаптер наследуется от BaseMultiItemQuickAdapter. Ему в конструктор не передаются layoutы, они инициализируются методом addItemType. Метод принимает параметры itemType: Int и layoutId: Int. Остальное аналогично BaseQuickAdapter, за исключением разбиения на типы. Для определения типа элемента, в методе convret, использую метод холдера holder.itemViewType. В зависимости от типа элемента – проставляю данные.
class MultiItemAdapter(data: MutableList<MessageDTO>?) : BaseMultiItemQuickAdapter<MessageDTO, BaseViewHolder>(data) {
init {
addItemType(MessageDTO.typeNotPremium, R.layout.item_message)
addItemType(MessageDTO.typePremium, R.layout.item_mesage_premium)
}
override fun convert(holder: BaseViewHolder, item: MessageDTO) {
val imageView = holder.getView<ImageView>(R.id.ivMsg)
val context = holder.itemView.context
Glide.with(context)
.load(item.image)
.into(imageView)
when (holder.itemViewType) {
MessageDTO.typePremium -> {
holder.setText(R.id.tvNamePrem, item.text)
.setText(R.id.tvPrice, item.price)
.setGone(R.id.ivAgreement, !item.mayAgreement)
.setGone(R.id.tvAgreement, !item.mayAgreement)
}
MessageDTO.typeNotPremium -> {
holder.setText(R.id.tvName, item.text)
.setText(R.id.tvPrice, item.price)
.setGone(R.id.ivAgreement, !item.mayAgreement)
.setGone(R.id.tvAgreement, !item.mayAgreement)
}
}
}
}
Результат:
И в завершение рассмотрю, как разместить «премиум» элемент на 2 ячейки. Делается это при инициализации адаптера. Методом setGridSpanSizeLookup адаптера, устанавливаю реализацию интерфейса GridSpanSizeLookup
adapter.setGridSpanSizeLookup(
GridSpanSizeLookup { gridLayoutManager, viewType, position ->
when (viewType) {
MessageDTO.typePremium -> 2
else -> 1
}
}
)
Если элемент «премиум», он будет занимать две ячейки, в противном случае одну. Результат:
В этой части рассмотрел достаточно сложное требование к отображению списков, и легкую его реализацию при помощи библиотеки. В следующих частях рассмотрю:
Отображение загрузки списка и ошибки загрузки списка
Обработку «долгих» нажатий
Удаление элемента «свайпом»
Перемещение элементов
Проект на Гите