android_tv.md 16 KB

Android TV

Содрано отсюда, отсюда и отсюда

Первая статья довольно древняя (2016 год), попробуем реализовать в 2022

Данная лекция познакомит вас с разработкой простого приложения для Android TV.

Так как интерфейс приложений для телефонов и Android TV имеет существенные различия, то мы должны создать интерфейс приложения, подходящий для взаимодействия на TV. Например, нам следует создавать приложения с которыми можно взаимодействовать, используя только клавиши . В реализации такого интерфейса нам может помочь библиотека LeanbackSupport, позволяющая вполне легко создавать UI, который будет удобен при работе с приложениями на Android TV.

Коротко о библиотеке Leanback

Библиотека Leanback представляет собой набор шаблонов экранов с различными функциональными особенностями. Есть экраны для отображения списков, карточек контента, диалогов и т. д. Эти экраны обрабатывают все пользовательские переходы между элементами и анимации, а также имеют довольно обширный функционал для построения простых приложений “из коробки”. Идеология данной библиотеки заключается в том, что все приложения на ее основе должны быть похожи в плане пользования. Не нужно думать, узнает ли пользователь о том что можно прокрутить вниз? Узнает, потому что он уже пользовался сотнями однотипных приложений.

Создание проекта в Android Studio

Запустив Android Studio, необходимо создать новый проект. При создании выбрать платформу TV и указать минимальную версию SDK. Android Studio предложит нам создать "Blank Activity", его и создадим (в оригинальной статье предлагают руками добавлять классы, файлы разметки и фрагменты, но это долго, проще выкинуть лишнее из рабочего проекта)

Можем запустить и увидеть что проект работает и что-то показывает:

Рассмотрим структуру созданного проекта:

Манифест

Менять мы пока ничего не будем, просто ознакомимся с особенностями проекта для TV

  • По-умолчанию уже добавлено разрешение на работу с интернетом

    <uses-permission android:name="android.permission.INTERNET" />
    
  • Заданы требования к целевому устройству

    Может отсутствовать (не требуется) тачскрин:

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />
    

    Приложение должно запускаться только на Android TV. Если вы разрабатываете приложение не только для TV, то вам следует установить значение false:

    <uses-feature
        android:name="android.software.leanback"
        android:required="true" />
    
  • При объявлении Activity мы указываем в intent-filter в теге category, что Activity должна запускаться на Android TV.

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
    
        <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
    </intent-filter>
    

MainActivity

Первым запускается как обычно MainActivity

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 version="1.0" encoding="utf-8"?>
<FrameLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_browse_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:deviceIds="tv"
    tools:ignore="MergeRootFrame" />

По коду мы видим, что активность наследуется от FragmentActivity и при запуске содержимое main_browse_fragment заменяется тем, что сгенерирует класс MainFragment. (Файла разметки main_browse_fragment в проекте нет, похоже он реализован в библиотеке Leanback)

MainFragment

Класс MainFragment наследуется от класса BrowseSupportFragment(), а про его структуру хорошо расписано во второй статье.

BrowseFragment — это фрагмент, предназначенный для создания экрана со списками элементов и заголовками. Его структура выглядит следующим образом:

Рассмотрим каждый из элементов:

  • TitleView - это контейнер с элементами. Он нужен для брендирования приложения (текст и логотип в правом верхнем углу), а также для добавления кнопки поиска. Чтобы кнопка поиска была видимой, ей необходимо установить слушателя. Это можно сделать, вызвав метод setOnSearchClickedListener у фрагмента.

  • HeadersFragment & RowsFragment - HeadersFragment — список заголовков в левой части экрана и RowsFragment — контейнер для контента в правой части экрана. Эти фрагменты работают в связке, и BrowseFragment делегирует им отрисовку элементов своего адаптера.

Таким образом, в методе onActivityCreated класса MainFragment настраиваются вышеперечисленные контейнеры. Можем убедиться в этом закомментировав функции, которые что-то делают и посмотрев на результат:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    Log.i(TAG, "onCreate")
    super.onActivityCreated(savedInstanceState)

    // prepareBackgroundManager()
    // setupUIElements()
    // loadRows()
    // setupEventListeners()
}

Метод prepareBackgroundManager

Инициализирует lateinit свойства класса и пока ничего не показывает

Метод setupUIElements

Устанавливает заголовок в правом верхнем углу и цвет для левой панели

Метод loadRows

Формирует всё содержимое, разберёмся с ним подробнее:

  1. val list = MovieList.list - Получаем список фильмов

    Свойство list класса MovieList возвращает массив объектов Movie, реализацию можно не разбирать, мы в реальном проекте всё-равно этот список будем получать динамически из АПИ.

  2. val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) - Создаётся стандартный адаптер (массива объектов) использующий стандартный класс для вывода элементов в виде "строки".

  3. val cardPresenter = CardPresenter() - создание самописанного (реализующего абстрактный класс) экземпляра представления карточки фильма

  4. Цикл перебора категорий (for (i in 0 until NUM_ROWS))

    Тут количество категорий "прибито гвоздями", в реальном проекте мы будем перебирать список имеющихся категорий

    if (i != 0) {
        Collections.shuffle(list)
    }
    

    Эта конструкция меняет содержимое коллекции в случайном порядке для всех строк, кроме первой. Если этого не сделать, то выведутся одинаковые строки (строки формируются из одного и того же списка фильмов):

    Далее создаётся адаптер для строки, которая будет отображаться в RowFragment (правая часть экрана) и заполняется фильмами из списка

    val listRowAdapter = ArrayObjectAdapter(cardPresenter)
    for (j in 0 until NUM_COLS) {
        listRowAdapter.add(list[j % 5])
    }
    

    В конце цикла создаётся заголовок для категории и заголовок вместе со списком фильмов добавляется в адаптер строк rowsAdapter

    val header = HeaderItem(
        i.toLong(), 
        MovieList.MOVIE_CATEGORY[i])
    rowsAdapter.add(
        ListRow(
            header, 
            listRowAdapter))
    
  5. Добавление строки с настройками

    Строка находится в общем адаптере (вертикальный список), но реализация элементов списка у неё другая:

    // формируем заголовок
    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))
    

    В итоге в основной адаптер добавляется строка с настройками

    rowsAdapter.add(
        ListRow(gridHeader, gridRowAdapter))
    

    И готовый адаптер назначается адаптеру нашего класса (его нет в нашей реализации, он объявлен в родительском классе)

    adapter = rowsAdapter
    

Метод setupEventListeners

Настраивает события, которые будет обрабатывать приложение:

setOnSearchClickedListener {
    Toast.makeText(
            activity!!, 
            "Implement your own in-app search",
            Toast.LENGTH_LONG)
        .show()
}

Назначение этого события включает иконку поиска. Реализации поиска пока никакой нет.

onItemViewClickedListener = ItemViewClickedListener()

onItemViewClickedListener - обработчик события клика по карточке фильма

onItemViewSelectedListener = ItemViewSelectedListener()

onItemViewSelectedListener - обработчик клика по элементу вертикального списка (категории)

Модификация под свои нужды

Загрузочный экран

Не знаю, будет ли это на демо-экзамене, но для демонстрации работы с обычными активностями андроид добавим загрузочный экран:

  1. Добавьте пустую активность (в контекстном меню пакета New -> Activity -> Empty Activity)

  2. В манифесте перенесите тег <intent-filter> из активности .MainActivity в .LaunchActivity (я ещё скопировал атрибут android:screenOrientation="landscape")

  3. В drawable ресурсы добавьте картинку для заставки и в файл разметки добавьте ImageView с этой картинкой

  4. В классе LaunchActivity:

    • поменяйте родителя, вместо AppCompatActivity оставьте просто Activity (AppCompatActivity не реализован в Android TV)

    • Реализуйте таймер и переход на главное окно:

      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)