Разработка под Android TV с применением нативных компонентов из Leanback
Подробный разбор возможностей BrowseFragment для разработки под Android TV — на примере приложения онлайн-кинотеатра.
Иллюстрация: Merry Mary для Skillbox Media
В интернете, помимо официальной документации от Google и нескольких HelloWorld-статей на русском языке, мало информации о работе с библиотекой Leanback. Поэтому я решил рассказать об опыте нашей компании.
Мы рассмотрим возможности BrowseFragment, который, по задумке Google, должен быть основным экраном приложения. В нашем случае это было приложение для онлайн-кинотеатра крупного медиахолдинга.
Опишу инструменты, с помощью которых мы собрали что-то подобное:
В статье я расскажу про:
- BrowseFragment и его составные элементы: TitleView, HeadersFragment & RowsFragment.
- Настройку контейнера TitleView, создание кастомных вариантов.
- Базовую настройку HeadersFragment и RowsFragment.
- Кастомизацию HeadersFragment — изменение внешнего вида всех заголовков.
- Кастомизацию RowsFragment: работу с GridListRow, создание сеток, превращение RowsFragment в отдельный экран, комбинирование элементов разного типа в одной строке.
BrowseFragment и его друзья
BrowseFragment — это фрагмент, предназначенный для создания экрана со списками элементов и заголовками. Его структура выглядит следующим образом:
Рассмотрим каждый из элементов.
TitleView
TitleView — это контейнер с элементами. Он нужен для брендирования приложения TextView и ImageView, а также для добавления кнопки поиска SearchOrbView из коробки. Чтобы кнопка поиска была видимой, ей необходимо установить слушателя. Это можно сделать, вызвав setOnSearchClickedListener {//ваш код} у фрагмента.
Настроить цветовую схему кнопки можно несколькими способами:
- Установив searchAffordanceColor = context.getColorRes (R.color.search_opaque)
При таком подходе изменится только цвет круга. - Установив searchAffordanceColors = SearchOrbView.Colors (context.getColorRes (R.color.search_opaque),
context.getColorRes (R.color.search_opaque_bright),
context.getColorRes (R.color.search_opaque_icon)
)
SearchOrbView.Colors имеет перегруженный конструктор, который позволяет более гибко настроить кнопку поиска.
- Colors (@ColorInt int color) — установит цвет круга.
- Colors (@ColorInt int color, @ColorInt int brightColor) — установит цвет круга и цвет анимации круга.
- Colors (@ColorInt int color, @ColorInt int brightColor, @ColorInt int iconColor) — установит цвет круга, цвет анимации круга и цвет иконки.
Под анимацией круга понимается эффект мерцания, вот такой:
В TitleView можно установить текстовый тайтл title = "Finch" или логотип:
badgeDrawable = ContextCompat.getDrawable (context, R.drawable.app_icon_your_company).
Причём установить можно только один из этих элементов, и наибольший приоритет всегда у логотипа.
Элементы настраиваются гибко — достаточно придерживаться рекомендаций Google. При необходимости можно создать кастомный TitleView. Например, если вы хотите, чтобы вместо кнопки поиска было текстовое поле, то это можно сделать в несколько шагов:
1. Создать layout для нового TitleView:
2. Создать View, которая реализует TitleViewAdapter.Provider, и создать сам TitleViewAdapter:
3. Создать ещё один layout, в котором будет всего один элемент — CustomTitleView:
4. Затем создать style для активити:
И установить её как тему нашей активити в манифесте:
Вот и всёмолчанию обрабатывает onBackPressed(). Если, у нас теперь есть кастомная TitleView.
HeadersFragment & RowsFragment
BrowseSupportFragment — это фрагмент, предназначенный для отрисовки элементов своего адаптера (ObjectAdapter) в виде строк вертикального списка. Визуально этот фрагмент делится на две части, на HeadersFragment — список заголовков в левой части экрана и RowsFragment — контейнер для контента в правой части экрана. Эти фрагменты работают в связке, и BrowseFragment делегирует им отрисовку элементов своего адаптера.
Все элементы, передаваемые в адаптер BrowseFragment, должны быть подклассами Row, так как этот объект несёт информацию о заголовке и контенте для отображения.
HeadersFragment предназначен для отрисовки и взаимодействия с HeaderItem, которые являются частью Row, переданного в адаптер BrowseFragment.
RowsFragment предназначен для отрисовки и взаимодействия с элементами своего ObjectAdapter. Это не адаптер BrowseFragment, и он не является частью Row, переданного в BrowseFragment. В него передаются объекты, описывающие контент.
Продемонстрирую сказанное на примере. Создадим Presenter, который будет передавать текст в TextView:
Затем создадим все необходимые адаптеры, заполним их данными и передадим BrowseFragment для отображения данных:
Каждому адаптеру передадим Presenter, который будет рисовать элементы:
- Для BrowseFragment здесь использована стандартная реализация ListRowPresenter. Этот Presenter умеет работать с ListRow и визуализирует ListRow, используя HorizontalGridView, помещённый в ListRowView.
- Для адаптера RowsFragment мы использовали свою реализацию TextPresenter(), так как передаём в него элементы типа String и TextPresenter(). Он умеет их отображать.
Теперь к каждому заголовку в левой половине экрана прикрепляется список в правой половине экрана:
Рассмотрим подробнее HeadersFragment.
В базовой реализации HeadersFragment сразу виден пользователю. Это поведение можно изменить с помощью метода setHeadersState(int). К нему нужно обратиться во время вызова onActivityCreated() и передать одно из состояний:
- HEADERS_ENABLED — фрагмент виден пользователю.
- HEADERS_HIDDEN — фрагмент свёрнут.
- HEADERS_DISABLED — фрагмент полностью скрыт с экрана.
BrowseFragment по умолчанию обрабатывает onBackPressed (). Если HeadersFragment свёрнут, то по нажатии пользователем кнопки «Назад» на пульте или джойстике он переходит в активное состояние и становится виден. Такое поведение отключается передачей параметра false в метод setHeadersTransitionOnBackEnabled ().
Чтобы управлять состоянием HeadersFragment вручную, можно использовать метод startHeadersTransition (boolean). В зависимости от входного параметра фрагмент заголовков либо будет показан (при передаче true), либо свёрнут (при передаче false).
Если есть необходимость прослушивать начало и конец анимации перехода HeadersFragment из свёрнутого состояния в активное и наоборот, то можно установить слушателя:
setBrowseTransitionListener(BrowseSupportFragment.BrowseTransitionListener).
Существует ещё несколько реализаций Row, которые оказывают влияние только на HeadersFragment:
- DividerRow — разделитель между заголовками.
- SectionRow — подзаголовок.
Изменим код следующим образом:
Что получилось: между элементами HeadersFragment появились разделители, а у последнего элемента — подзаголовок.
Кастомизация HeadersFragment
Допустим, мы захотели изменить внешний вид заголовков и добавить иконки. Для этого необходимо создать элемент заголовка с уникальным Presenter, а также создать PresenterSelector — он выполняет роль Presenter для каждого элемента списка.
Начнём с элемента заголовка:
Мы расширили стандартный HeaderItem, добавив возможность устанавливать id‑иконки из ресурсов. Сделаем вёрстку для нового заголовка:
Создадим свой презентер, который умеет отрисовывать IconHeaderItem:
Установим новый PresenterSelector для заголовков. Для этого у BrowseFragment есть метод setHeaderPresenterSelector:
Теперь смотрим на заголовок Row. Если он IconHeaderItem, то возвращаем Presenter, который умеет с ним работать — в данном случае IconRowHeaderPresenter. Заменим заголовки на новые:
Получаем результат:
Таким же образом можно кастомизировать и подзаголовки в SectionRow или разделители DividerRow.
Подробнее о RowsFragment
Кастомный ListRow
Допустим, нам нужно отобразить в правой части экрана большое количество элементов. По стандарту все они будут отображены в одну строку:
Но нам это не подходит — мы хотим отобразить элементы в виде сетки. Сделать это можно с помощью кастомного Row.
Создадим свой GridListRow, который будет отображать данные в виде сетки, а не списка:
Затем расширим возможности ListRowPresenter и научим его работать с GridListRow:
Теперь установим GridListRow в качестве Presenter адаптера BrowseFragment:
Теперь используем GridListAdapter в RowsFragment:
Получился следующий список:
Кастомный RowsFragment
BrowseFragment позволяет заменить стандартную реализацию RowsFragment на кастомную при помощи FragmentFactory. Эта фабрика отвечает за создание фрагмента для текущего элемента.
Сделаем так, чтобы RowsFragment всегда показывал внутренние заголовки. Для начала создадим свою реализацию RowsFragment для работы со списками:
Здесь мы переопределили метод setExpand, чтобы всегда передавать true в качестве аргумента родительского метода. Теперь заголовки всегда будут показываться.
Далее реализуем фабрику FragmentFactory, которая будет возвращать ListRowFragment:
И установим её в качестве фабрики, возвращающей фрагмент для GridListRow:
Теперь заголовки RowsFragment будут отображаться всегда.
RowsFragment как отдельный экран
Выше были рассмотрены варианты с ListRow, где каждому элементу заголовка из левой части ставился в соответствие список элементов в правой части экрана. Теперь рассмотрим ситуацию, когда каждому заголовку из левой части ставится в соответствие не список, а отдельная страница в правой части экрана.
Реализовать такое поведение можно через PageRow совместно с FragmentFactory. Для начала создадим фрагмент, который будет представлять собой отдельную страницу:
У RowsFragment, так же как и у BrowseFragment, есть общий внешний и внутренний адаптеры для каждого списка элементов. При создании PageRowsFragment мы передаём ему id текущего элемента заголовка BrowseFragment и устанавливаем в качестве заголовка списка.
Создадим FragmentFactory, которая будет возвращать PageRowFragment:
Установим его в качестве фабрики, возвращающей фрагменты для PageRow:
Теперь каждый элемент заголовка BrowseFragment имеет свой собственный RowsFragment, переключение между которыми происходит при выборе нового заголовка:
Элементы разного типа в одной строке
Иногда в один ListRow нужно добавить объекты разного типа. Для этого под элемент каждого типа необходимо создать свой Presenter, который будет возвращать PresenterSelector. В Leanback уже есть готовая реализация PresenterSelector, которая возвращает Presenter для каждого элемента списка по типу его класса — ClassPresenterSelector.
Создадим два класса TextItem и ImageItem. В первый будем передавать текст для элемента, во второй — id изображения из ресурсов:
Перепишем TextPresenter таким образом, чтобы он умел работать с элементами TextItem. В rowsAdapter будем добавлять не String, а TextItem:
Добавим ImagePresenter, работающий с ImageItem:
ImagePresenter загружает изображение из ресурсов, показывает его и помещает в ImageView. Создадим ClassPresenterSelector и укажем, какие данные отображает каждый Presenter:
Затем передадим этот презентер в адаптер RowsFragment:
и добавим несколько элементов типа ImageItem:
Теперь в одном списке могут присутствовать элементы разных типов:
Заключение
Мы рассмотрели возможности BrowseFragment, который является частью библиотеки Leanback. Но Leanback не ограничивается этим шаблоном. Она настолько богатая, что её элементов хватит для создания полноценного приложения Android TV с учётом всех гайдлайнов.
А здесь можно посмотреть исходники проекта.