# Android TV >Содрано [отсюда](https://habr.com/ru/post/316260/), [отсюда](https://skillbox.ru/media/code/razrabotka_pod_android_tv/) и [отсюда](https://skillbox.ru/media/code/razrabotka_pod_android_tv_part2/) [Первая](https://habr.com/ru/post/316260/) статья довольно древняя (2016 год), попробуем реализовать в 2022 Данная лекция познакомит вас с разработкой простого приложения для Android TV. Так как интерфейс приложений для телефонов и Android TV имеет существенные различия, то мы должны создать интерфейс приложения, подходящий для взаимодействия на TV. Например, нам следует создавать приложения с которыми можно взаимодействовать, используя только клавиши `—` `↑` `↓` `→` `←`. В реализации такого интерфейса нам может помочь библиотека **LeanbackSupport**, позволяющая вполне легко создавать UI, который будет удобен при работе с приложениями на Android TV. ## Коротко о библиотеке Leanback Библиотека Leanback представляет собой набор шаблонов экранов с различными функциональными особенностями. Есть экраны для отображения списков, карточек контента, диалогов и т. д. Эти экраны обрабатывают все пользовательские переходы между элементами и анимации, а также имеют довольно обширный функционал для построения простых приложений “из коробки”. Идеология данной библиотеки заключается в том, что все приложения на ее основе должны быть похожи в плане пользования. Не нужно думать, узнает ли пользователь о том что можно прокрутить вниз? Узнает, потому что он уже пользовался сотнями однотипных приложений. ## Создание проекта в Android Studio Запустив Android Studio, необходимо создать новый проект. При создании выбрать платформу TV и указать минимальную версию SDK. Android Studio предложит нам создать "Blank Activity", его и создадим (в оригинальной статье предлагают руками добавлять классы, файлы разметки и фрагменты, но это долго, проще выкинуть лишнее из рабочего проекта) ![](../img/tv_01.png) Можем запустить и увидеть что проект работает и что-то показывает: ![](../img/tv_02.png) Рассмотрим структуру созданного проекта: ### Манифест >Менять мы пока ничего не будем, просто ознакомимся с особенностями проекта для TV * По-умолчанию уже добавлено разрешение на работу с интернетом ```xml ``` * Заданы требования к целевому устройству Может отсутствовать (не требуется) тачскрин: ```xml ``` Приложение должно запускаться только на Android TV. Если вы разрабатываете приложение не только для TV, то вам следует установить значение *false*: ```xml ``` * При объявлении Activity мы указываем в *intent-filter* в теге *category*, что Activity должна запускаться на Android TV. ```xml ``` ### MainActivity Первым запускается как обычно **MainActivity** ```kt class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .replace(R.id.main_browse_fragment, MainFragment()) .commitNow() } } } ``` `activity_main.xml`: ```xml ``` По коду мы видим, что активность наследуется от **FragmentActivity** и при запуске содержимое `main_browse_fragment` заменяется тем, что сгенерирует класс **MainFragment**. (Файла разметки `main_browse_fragment` в проекте нет, похоже он реализован в библиотеке **Leanback**) ### MainFragment Класс **MainFragment** наследуется от класса `BrowseSupportFragment()`, а про его структуру хорошо расписано во [второй](https://skillbox.ru/media/code/razrabotka_pod_android_tv/) статье. **BrowseFragment** — это фрагмент, предназначенный для создания экрана со списками элементов и заголовками. Его структура выглядит следующим образом: ![](../img/tv_01.webp) Рассмотрим каждый из элементов: * **TitleView** - это контейнер с элементами. Он нужен для брендирования приложения (текст и логотип в правом верхнем углу), а также для добавления кнопки поиска. Чтобы кнопка поиска была видимой, ей необходимо установить слушателя. Это можно сделать, вызвав метод *setOnSearchClickedListener* у фрагмента. * **HeadersFragment & RowsFragment** - **HeadersFragment** — список заголовков в левой части экрана и **RowsFragment** — контейнер для контента в правой части экрана. Эти фрагменты работают в связке, и **BrowseFragment** делегирует им отрисовку элементов своего адаптера. Таким образом, в методе *onActivityCreated* класса **MainFragment** настраиваются вышеперечисленные контейнеры. Можем убедиться в этом закомментировав функции, которые что-то делают и посмотрев на результат: ```kt override fun onActivityCreated(savedInstanceState: Bundle?) { Log.i(TAG, "onCreate") super.onActivityCreated(savedInstanceState) // prepareBackgroundManager() // setupUIElements() // loadRows() // setupEventListeners() } ``` ![](../img/tv_03.png) #### Метод *prepareBackgroundManager* Инициализирует *lateinit* свойства класса и пока ничего не показывает #### Метод *setupUIElements* Устанавливает заголовок в правом верхнем углу и цвет для левой панели ![](../img/tv_04.png) #### Метод *loadRows* Формирует всё содержимое, разберёмся с ним подробнее: 1. `val list = MovieList.list` - Получаем список фильмов Свойство *list* класса **MovieList** возвращает массив объектов **Movie**, реализацию можно не разбирать, мы в реальном проекте всё-равно этот список будем получать динамически из АПИ. 1. `val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())` - Создаётся **стандартный** адаптер (массива объектов) использующий **стандартный** класс для вывода элементов в виде "строки". 1. `val cardPresenter = CardPresenter()` - создание самописанного (реализующего абстрактный класс) экземпляра представления карточки фильма 1. Цикл перебора категорий (`for (i in 0 until NUM_ROWS)`) >Тут количество категорий "прибито гвоздями", в реальном проекте мы будем перебирать список имеющихся категорий ```kt if (i != 0) { Collections.shuffle(list) } ``` Эта конструкция меняет содержимое коллекции в случайном порядке для всех строк, кроме первой. Если этого не сделать, то выведутся одинаковые строки (строки формируются из одного и того же списка фильмов): ![](../img/tv_05.png) Далее создаётся адаптер для строки, которая будет отображаться в RowFragment (правая часть экрана) и заполняется фильмами из списка ```kt val listRowAdapter = ArrayObjectAdapter(cardPresenter) for (j in 0 until NUM_COLS) { listRowAdapter.add(list[j % 5]) } ``` В конце цикла создаётся заголовок для категории и заголовок вместе со списком фильмов добавляется в адаптер строк *rowsAdapter* ```kt val header = HeaderItem( i.toLong(), MovieList.MOVIE_CATEGORY[i]) rowsAdapter.add( ListRow( header, listRowAdapter)) ``` 1. Добавление строки с настройками ![](../img/tv_06.png) Строка находится в общем адаптере (вертикальный список), но реализация элементов списка у неё другая: ```kt // формируем заголовок val gridHeader = HeaderItem( NUM_ROWS.toLong(), "PREFERENCES") // здесь создаётся представление для элемента настроек val mGridPresenter = GridItemPresenter() // на его основе делается адаптер для строки элементов val gridRowAdapter = ArrayObjectAdapter(mGridPresenter) // и в эту строку добавляются пункты меню настроек gridRowAdapter.add( resources.getString(R.string.grid_view)) gridRowAdapter.add( getString(R.string.error_fragment)) gridRowAdapter.add( resources.getString(R.string.personal_settings)) ``` В итоге в основной адаптер добавляется строка с настройками ```kt rowsAdapter.add( ListRow(gridHeader, gridRowAdapter)) ``` И готовый адаптер назначается адаптеру нашего класса (его нет в нашей реализации, он объявлен в родительском классе) ```kt adapter = rowsAdapter ``` #### Метод *setupEventListeners* Настраивает события, которые будет обрабатывать приложение: ```kt setOnSearchClickedListener { Toast.makeText( activity!!, "Implement your own in-app search", Toast.LENGTH_LONG) .show() } ``` Назначение этого события **включает** иконку поиска. Реализации поиска пока никакой нет. ```kt onItemViewClickedListener = ItemViewClickedListener() ``` *onItemViewClickedListener* - обработчик события клика по карточке фильма ```kt onItemViewSelectedListener = ItemViewSelectedListener() ``` *onItemViewSelectedListener* - обработчик клика по элементу вертикального списка (категории) ## Модификация под свои нужды ### Загрузочный экран Не знаю, будет ли это на демо-экзамене, но для демонстрации работы с обычными активностями андроид добавим загрузочный экран: 1. Добавьте пустую активность (в контекстном меню пакета `New -> Activity -> Empty Activity`) 1. В манифесте перенесите тег `` из активности *.MainActivity* в *.LaunchActivity* (я ещё скопировал атрибут `android:screenOrientation="landscape"`) 1. В `drawable` ресурсы добавьте картинку для заставки и в файл разметки добавьте **ImageView** с этой картинкой 1. В классе **LaunchActivity**: * поменяйте родителя, вместо **AppCompatActivity** оставьте просто **Activity** (**AppCompatActivity** не реализован в Android TV) * Реализуйте таймер и переход на главное окно: ```kt class LaunchActivity : Activity() { lateinit var that: Activity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_launch) that = this Timer().schedule(3000L){ that.runOnUiThread { startActivity(Intent(that, MainActivity::class.java)) } } } } ``` Тут мне лень было писать метод для обработки события таймера, поэтому контекст (*this*) я сохранил в отдельную переменную (внутри лямбда-функции таймера контекст другой) Всё замечательно работает!!! ### Получение списка фильмов и категорий Что-бы не терять время просто так, поместим этот код в загрузочную активность (заодно проверим как тут работает класс **Application**)