Это третья часть цикла статей про разработку адаптеров для 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
        }
    }
)

Если элемент «премиум», он будет занимать две ячейки, в противном случае одну. Результат:

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

  • Отображение загрузки списка и ошибки загрузки списка

  • Обработку «долгих» нажатий

  • Удаление элемента «свайпом»

  • Перемещение элементов

Проект на Гите

Библиотека

Комментарии (2)


  1. Meir19
    20.09.2021 19:24

    Классно, спасибо за статью!


    1. bsod_keks Автор
      21.09.2021 08:54

      пожалуйста:)