Разработка под Android TV с применением нативных компонентов из Leanback: карточки контента
Продолжаем разбираться с Leanback. Сегодня создадим карточки контента, которые можно использовать, например, в списке элементов.
vlada_maestro / shutterstock
Игорь Дубровин
эксперт
об авторе
Android-разработчик в аутсорс-продакшне FINCH. Программирует на Kotlin и Java. Любит кодить и отдыхать на пляже.
* Мнение автора может не совпадать с мнением редакции
В предыдущей статье я рассказал о том, как создавать главный экран приложения со списком элементов. Тогда каждый элемент отрисовывался при помощи своей уникальной карточки. В этот раз я расскажу о том, как создаются эти карточки и какие инструменты работы доступны «из коробки».
Leanback Cards
Допустим, мы хотим создать раздел приложения, где карточки будут выглядеть следующим образом:
В Leanback уже есть готовая карточка, которую можно использовать в своем приложении, — это ImageCardView. Эта карточка — подкласс BaseCardView, которая достаточно гибко настраивается. Рассмотрим, из чего она состоит:
- ImageView — основная картинка.
- ViewGroup (infoArea) — контейнер для дополнительных элементов, таких как:
- ImageView — иконка;
- TextView — заголовок;
- TextView — описание.
Попробуем создать нашу карточку. Для начала сделаем элемент контента и поместим в него параметры для отрисовки:
Затем создадим Presenter, который будет связывать данные из LeanbackCardItem c ImageCardView. Presenter нужен для отрисовки элементов контента, которые были переданы в адаптер — он связывает каждый элемент контента с UI-отображением, то есть карточкой контента.
Каждый Presenter требует реализовать 3 метода:
- fun onCreateViewHolder (viewGroup: ViewGroup): Presenter.ViewHolder — создание ViewHolder, смысл которого — в переиспользовании уже созданной View. Подобное происходит с ViewHolder адаптера RecyclerView.
- fun onBindViewHolder (viewHolder: Presenter.ViewHolder, item: Any) — во время вызова этого метода необходимо связать данные определенного элемента с его UI-представлением.
- fun onUnbindViewHolder (viewHolder: Presenter.ViewHolder) — отвязывание View от его элемента и очистка ресурсов, связанных с данным элементом.
У созданной ImageCardView установим два флага isFocusable = true и isFocusableInTouchMode = true — это необходимо для того, чтобы наша View получала фокус от контроллера управления.
С помощью метода setMainImageDimensions (int width, int height) можно установить размер основной картинки. Также для упрощения работы, связанной с загрузкой изображений, мы создали таргет для Glide ImageCardViewTarget, который умеет работать с ImageCardView.
Добавим код создания элементов и инициализации адаптеров во фрагмент:
Посмотрим на результат:
Близко, но нет. Карточки еще нужно доработать: иконка пока находится с правой стороны, а infoArea расположена на картинке и имеет селектор у выделенной карточки.
Управлять компонентами ImageCardView можно путем расширения стиля Widget.Leanback.ImageCardViewStyle и установки свойства lbImageCardViewType с одним из следующих поддерживаемых значений: Title, Content, IconOnRight, IconOnLeft, ImageOnly или их комбинацией.
Создадим стиль ImageCardViewStyle и укажем для свойства lbImageCardViewType следующие значения IconOnLeft|Title|Content. Комбинация этих значений позволит нам разместить иконку с левой стороны от описания:
Применить этот стиль к ImageCardView можно через основную тему приложения, установив ее в качестве значения imageCardViewStyle. В данном случае этот стиль будет применен ко всем карточкам приложения, но для большей гибкости создадим отдельную тему:
Затем применим тему к конкретной ImageCardView при помощи ContextThemeWrapper. Такой подход позволяет создавать различные стили под определенный тип карточек приложения. ImageCardView (ContextThemeWrapper (context, R.style.ImageCardTheme))
Запустим приложение и посмотрим, что получилось:
Бинго! Иконка расположена с левой стороны, а заголовок и описание отчетливо видны. Теперь сделаем селектор и градиентный бэкграунд для infoArea. Для начала создадим градиентную область gradient_background.xml:
Затем создадим селектор info_area_selector.xml:
Зададим селектор как бэкграунд infoArea нашей карточки. Это можно сделать через стиль imageCardViewStyle, установив его в качестве значения свойства infoAreaBackground. Стиль карточки теперь выглядит следующим образом:
Теперь осталось разместить infoArea поверх основной картинки.
У BaseCardView есть такие параметры, как cardType и infoVisibility, они также позволяют управлять элементами карточки. cardType управляет расположением областей и может принимать одно из следующих значений:
- CARD_TYPE_MAIN_ONLY — видимой будет только основная область (у ImageCardView это основная картинка, т.е. это значение эквивалентно ImageOnly);
- CARD_TYPE_INFO_OVER — информационная область находится поверх основной (infoArea будет размещена поверх основной картинки);
- CARD_TYPE_INFO_UNDER — информационная область находится под основной (infoArea будет размещена под основной картинкой);
- CARD_TYPE_INFO_UNDER_WITH_EXTRA — поддерживает третью дополнительную область, которая будет расположена под основной областью, но над информационной. Этой областью может быть view, которая добавлена в карточку с помощью метода addView (view: View) (относительно ImageCardView дополнительная область будет расположена между основной картинкой и infoArea).
infoVisibility управляет видимостью информационной области (у ImageCardView это infoArea) и может принимать одно из следующих значений:
- CARD_REGION_VISIBLE_ALWAYS;
- CARD_REGION_VISIBLE_ACTIVATED;
- CARD_REGION_VISIBLE_SELECTED.
Для того чтобы разместить infoArea поверх основной картинки, установим для нашей ImageCardView cardType со значением CARD_TYPE_INFO_OVER:
Посмотрим на результат. Получилось то, что и планировалось.
С помощью стандартных инструментов Leanback можно создавать много разных вариантов карточек контента ImageCardView, но все же варианты «из коробки» ограничены. Иногда нужны совсем нестандартные варианты.
Создание собственных карточек контента
Реализуем раздел меню для нашего приложения, который должен выглядеть следующим образом:
Для начала создадим элемент меню, который мы хотим отрисовать. Он содержит заголовок и иконку:
Попробуем его реализовать при помощи ImageCardView. Создадим презентер для связывания элементов меню в ImageCardView:
Добавим код создания элементов меню и инициализации адаптеров в наш фрагмент:
Запустим и посмотрим, что получилось:
Неплохо, но далеко от идеала. Иконки растягиваются на всю область основного изображения, а изменение ее размеров приводит к изменению размеров самой карточки. Для решения этой задачи нам необходимо создать собственную карточку для отрисовки элементов меню. Назовем ее MenuView.
Для начала создадим ее layout, view_menu.xml:
Затем создадим MenuView и заинфлейтим в нее наш layout:
И, наконец, перепишем презентер. Теперь он будет связывать данные из элемента меню в MenuView:
Похоже, но при нажатии на карточку отсутствует ripple-эффект. Создадим ripple drawable:
И установим его как foreground MenuView:
Теперь при нажатии на карточку можно увидеть ripple-эффект:
Появилась другая проблема. RippleDrawable обрабатывает состояние focused и накладывает на view полупрозрачный бэкграунд. Чтобы этот эффект было лучше видно, изменим цвет карточки на темный.
Как видно, выделенная карточка становится светлее.Для решения этой проблемы отфильтруем состояние focused у MenuView, переопределив следующий метод:
Мы избавились от искажения цвета карточки в выделенном состоянии и сохранили ripple.
Таким образом можно создавать карточки контента, которые нельзя сделать при помощи ImageCardView. Но это еще не все: я бы хотел улучшить получившуюся карточку и сделать у нее поддержку функционала, который предоставляет базовая реализация карточек BaseCardView. Давайте добавим на нашу карточку небольшое описание, которое будет появляться при переходе в состояние selected. Для этого создадим еще один layout:
Этот layout похож на предыдущий, за исключением того что здесь добавили дополнительный TextView для вывода описания и новый атрибут lb: layout_viewType. Это атрибут BaseCardView, с помощью которого он находит элементы, которыми умеет управлять. У тега существует 3 значения: main — основная область карточки, info — информационная область и extra — дополнительная область.
Создадим новую view, которая будет наследником BaseCardView. Так мы добавим базовые функции карточек:
Теперь наследуем тему для view. Установим для свойства cardType значение infoOver — это означает, что информационная область (TextView с описанием) будет находиться поверх основной. А для свойства infoVisibility установим значение selected — это означает, что информационная область будет видна только в момент, когда карточка находится в состоянии selected.
Так мы получили кастомную карточку, поддерживающую базовые фичи карточек из Leanback.
Вывод
Leanback предоставляет достаточно гибко настраиваемый вариант карточки, а именно ImageCardView. При необходимости можно достаточно просто создать свою карточку контента, а для того чтобы она имела базовый функционал карточек, ее родителем должна выступать BaseCardView.
Если вам понадобится проект, который создавался в статье, то его можно скачать на GitHub.