Медиа-приложение, работающее на телевизоре, должно позволять пользователям просматривать предложения контента, делать выбор и начинать воспроизведение контента. Опыт просмотра контента должен быть простым и интуитивно понятным, а также визуально приятным и привлекательным.
В этом руководстве обсуждается, как использовать классы, предоставляемые библиотекой androidx.leanback , для реализации пользовательского интерфейса для просмотра музыки или видео из медиа-каталога вашего приложения.
Примечание. Показанный здесь пример реализации использует BrowseSupportFragment
а не устаревший класс BrowseFragment
. BrowseSupportFragment
расширяет класс AndroidX Fragment
, помогая обеспечить согласованное поведение на разных устройствах и версиях Android.
![Главный экран приложения](https://developer.android.com/static/images/tv/app-browse.png?authuser=19&hl=ru)
Рис. 1. Фрагмент просмотра примера приложения Leanback отображает данные каталога видео.
Создание макета просмотра мультимедиа
Класс BrowseSupportFragment
в наборе инструментов Leanback UI позволяет создать основной макет для просмотра категорий и строк элементов мультимедиа с минимальным использованием кода. В следующем примере показано, как создать макет, содержащий объект BrowseSupportFragment
:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_frame" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.android.tvleanback.ui.MainFragment" android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
Основное действие приложения устанавливает это представление, как показано в следующем примере:
Котлин
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } ...
Ява
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } ...
Методы BrowseSupportFragment
заполняют представление видеоданными и элементами пользовательского интерфейса и задают параметры макета, такие как значок и заголовок, а также определяют, включены ли заголовки категорий.
Подкласс приложения, реализующий методы BrowseSupportFragment
, также настраивает прослушиватели событий для действий пользователя над элементами пользовательского интерфейса и подготавливает фоновый менеджер, как показано в следующем примере:
Котлин
class MainFragment : BrowseSupportFragment(), LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadVideoData() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareBackgroundManager() setupUIElements() setupEventListeners() } ... private fun prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(activity).apply { attach(activity?.window) } defaultBackground = resources.getDrawable(R.drawable.default_background) metrics = DisplayMetrics() activity?.windowManager?.defaultDisplay?.getMetrics(metrics) } private fun setupUIElements() { badgeDrawable = resources.getDrawable(R.drawable.videos_by_google_banner) // Badge, when set, takes precedent over title title = getString(R.string.browse_title) headersState = BrowseSupportFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true // Set header background color brandColor = ContextCompat.getColor(requireContext(), R.color.fastlane_background) // Set search icon color searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque) } private fun loadVideoData() { VideoProvider.setContext(activity) videosUrl = getString(R.string.catalog_url) loaderManager.initLoader(0, null, this) } private fun setupEventListeners() { setOnSearchClickedListener { Intent(activity, SearchActivity::class.java).also { intent -> startActivity(intent) } } onItemViewClickedListener = ItemViewClickedListener() onItemViewSelectedListener = ItemViewSelectedListener() } ...
Ява
public class MainFragment extends BrowseSupportFragment implements LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { } ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadVideoData(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); prepareBackgroundManager(); setupUIElements(); setupEventListeners(); } ... private void prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); defaultBackground = getResources() .getDrawable(R.drawable.default_background); metrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); } private void setupUIElements() { setBadgeDrawable(getActivity().getResources() .getDrawable(R.drawable.videos_by_google_banner)); // Badge, when set, takes precedent over title setTitle(getString(R.string.browse_title)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(true); // Set header background color setBrandColor(ContextCompat.getColor(requireContext(), R.color.fastlane_background)); // Set search icon color setSearchAffordanceColor(ContextCompat.getColor(requireContext(), R.color.search_opaque)); } private void loadVideoData() { VideoProvider.setContext(getActivity()); videosUrl = getString(R.string.catalog_url); getLoaderManager().initLoader(0, null, this); } private void setupEventListeners() { setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } }); setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); } ...
Установить элементы пользовательского интерфейса
В предыдущем примере закрытый метод setupUIElements()
вызывает несколько методов BrowseSupportFragment
для стилизации браузера каталога мультимедиа:
-
setBadgeDrawable()
помещает указанный рисуемый ресурс в правый верхний угол фрагмента просмотра, как показано на рисунках 1 и 2. Этот метод заменяет строку заголовка доступным для рисования ресурсом, если также вызываетсяsetTitle()
. Вытягиваемый ресурс должен иметь высоту 52 dp. -
setTitle()
устанавливает строку заголовка в правом верхнем углу фрагмента просмотра, если не вызываетсяsetBadgeDrawable()
. -
setHeadersState()
иsetHeadersTransitionOnBackEnabled()
скрывают или отключают заголовки. Дополнительную информацию см. в разделе «Скрыть или отключить заголовки» . -
setBrandColor()
устанавливает цвет фона для элементов пользовательского интерфейса во фрагменте просмотра, в частности цвет фона раздела заголовка, с указанным значением цвета. -
setSearchAffordanceColor()
устанавливает цвет значка поиска с указанным значением цвета. Значок поиска появляется в левом верхнем углу фрагмента просмотра, как показано на рисунках 1 и 2.
Настройте вид заголовка
Фрагмент просмотра, показанный на рисунке 1, отображает имена категорий видео, которые являются заголовками строк в базе данных видео, в текстовых представлениях. Вы также можете настроить заголовок, включив дополнительные представления в более сложный макет. В следующих разделах показано, как включить представление изображения, в котором рядом с именем категории отображается значок, как показано на рис. 2.
![Главный экран приложения](https://developer.android.com/static/images/tv/custom-head.png?authuser=19&hl=ru)
Рисунок 2. Заголовки строк во фрагменте просмотра со значком и текстовой меткой.
Макет заголовка строки определяется следующим образом:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/header_icon" android:layout_width="32dp" android:layout_height="32dp" /> <TextView android:id="@+id/header_label" android:layout_marginTop="6dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
Используйте Presenter
и реализуйте абстрактные методы для создания, привязки и отмены привязки держателя представления. В следующем примере показано, как связать держатель представления с двумя представлениями: ImageView
и TextView
.
Котлин
class IconHeaderItemPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder { val view = LayoutInflater.from(viewGroup.context).run { inflate(R.layout.icon_header_item, null) } return Presenter.ViewHolder(view) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.findViewById<ImageView>(R.id.header_icon).apply { rootView.resources.getDrawable(R.drawable.ic_action_video, null).also { icon -> setImageDrawable(icon) } } rootView.findViewById<TextView>(R.id.header_label).apply { text = headerItem.name } } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no-op } }
Ява
public class IconHeaderItemPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.icon_header_item, null); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; ImageView iconView = (ImageView) rootView.findViewById(R.id.header_icon); Drawable icon = rootView.getResources().getDrawable(R.drawable.ic_action_video, null); iconView.setImageDrawable(icon); TextView label = (TextView) rootView.findViewById(R.id.header_label); label.setText(headerItem.getName()); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { // no-op } }
Ваши заголовки должны быть фокусируемыми, чтобы можно было использовать D-pad для их прокрутки. Есть два способа справиться с этим:
- Установите фокус на фокус в
onBindViewHolder()
:Котлин
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.focusable = View.FOCUSABLE // ... }
Ява
@Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; rootView.setFocusable(View.FOCUSABLE) // Allows the D-Pad to navigate to this header item // ... }
- Настройте макет так, чтобы он был фокусируемым:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Наконец, в реализации BrowseSupportFragment
, которая отображает браузер каталога, используйте метод setHeaderPresenterSelector()
чтобы установить презентатор для заголовка строки, как показано в следующем примере.
Котлин
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
Ява
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
Полный пример см. в примере приложения Leanback .
Скрыть или отключить заголовки
Иногда вы не хотите, чтобы заголовки строк отображались, например, когда категорий недостаточно, чтобы требовался прокручиваемый список. Вызовите метод BrowseSupportFragment.setHeadersState()
во время метода onActivityCreated()
фрагмента, чтобы скрыть или отключить заголовки строк. Метод setHeadersState()
устанавливает начальное состояние заголовков во фрагменте просмотра, используя в качестве параметра одну из следующих констант:
-
HEADERS_ENABLED
: при создании активности фрагмента просмотра заголовки включаются и отображаются по умолчанию. Заголовки выглядят так, как показано на рисунках 1 и 2 на этой странице. -
HEADERS_HIDDEN
: при создании активности фрагмента просмотра заголовки включаются и скрываются по умолчанию. Раздел заголовка экрана свернут, как показано на рисунке в разделе Предоставление представления карточки . Пользователь может выбрать свернутый раздел заголовка, чтобы развернуть его. -
HEADERS_DISABLED
: при создании активности фрагмента просмотра заголовки по умолчанию отключены и никогда не отображаются.
Если установлен HEADERS_ENABLED
или HEADERS_HIDDEN
, вы можете вызвать setHeadersTransitionOnBackEnabled()
для поддержки возврата к заголовку строки из выбранного элемента содержимого в строке. Это включено по умолчанию, если вы не вызываете метод. Чтобы самостоятельно обрабатывать обратное движение, передайте false
в функцию setHeadersTransitionOnBackEnabled()
и реализуйте собственную обработку обратного стека.
Отображение списков мультимедиа
Класс BrowseSupportFragment
позволяет определять и отображать доступные для просмотра категории медиаконтента и элементы мультимедиа из каталога мультимедиа с помощью адаптеров и презентаторов. Адаптеры позволяют подключаться к локальным или онлайн-источникам данных, содержащим информацию вашего медиа-каталога. Адаптеры используют презентаторы для создания представлений и привязки данных к этим представлениям для отображения элемента на экране.
В следующем примере кода показана реализация Presenter
для отображения строковых данных:
Котлин
private const val TAG = "StringPresenter" class StringPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder { val textView = TextView(parent.context).apply { isFocusable = true isFocusableInTouchMode = true background = parent.resources.getDrawable(R.drawable.text_bg) } return Presenter.ViewHolder(textView) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) { (viewHolder.view as TextView).text = item.toString() } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no op } }
Ява
public class StringPresenter extends Presenter { private static final String TAG = "StringPresenter"; public ViewHolder onCreateViewHolder(ViewGroup parent) { TextView textView = new TextView(parent.getContext()); textView.setFocusable(true); textView.setFocusableInTouchMode(true); textView.setBackground( parent.getResources().getDrawable(R.drawable.text_bg)); return new ViewHolder(textView); } public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText(item.toString()); } public void onUnbindViewHolder(ViewHolder viewHolder) { // no op } }
После создания класса презентатора для элементов мультимедиа вы можете создать адаптер и присоединить его к BrowseSupportFragment
, чтобы отображать эти элементы на экране для просмотра пользователем. В следующем примере кода показано, как создать адаптер для отображения категорий и элементов в этих категориях с помощью класса StringPresenter
, показанного в предыдущем примере кода:
Котлин
private const val NUM_ROWS = 4 ... private lateinit var rowsAdapter: ArrayObjectAdapter override fun onCreate(savedInstanceState: Bundle?) { ... buildRowsAdapter() } private fun buildRowsAdapter() { rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in 0 until NUM_ROWS) { val listRowAdapter = ArrayObjectAdapter(StringPresenter()).apply { add("Media Item 1") add("Media Item 2") add("Media Item 3") } HeaderItem(i.toLong(), "Category $i").also { header -> rowsAdapter.add(ListRow(header, listRowAdapter)) } } browseSupportFragment.adapter = rowsAdapter }
Ява
private ArrayObjectAdapter rowsAdapter; private static final int NUM_ROWS = 4; @Override protected void onCreate(Bundle savedInstanceState) { ... buildRowsAdapter(); } private void buildRowsAdapter() { rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); for (int i = 0; i < NUM_ROWS; ++i) { ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( new StringPresenter()); listRowAdapter.add("Media Item 1"); listRowAdapter.add("Media Item 2"); listRowAdapter.add("Media Item 3"); HeaderItem header = new HeaderItem(i, "Category " + i); rowsAdapter.add(new ListRow(header, listRowAdapter)); } browseSupportFragment.setAdapter(rowsAdapter); }
В этом примере показана статическая реализация адаптеров. Типичное приложение для просмотра мультимедиа использует данные из онлайн-базы данных или веб-службы. Пример приложения для просмотра, использующего данные, полученные из Интернета, см. в примере приложения Leanback .
Обновите фон
Чтобы добавить визуальный интерес к приложению для просмотра мультимедиа на телевизоре, вы можете обновлять фоновое изображение, когда пользователи просматривают контент. Этот метод может сделать взаимодействие с вашим приложением более кинематографичным и приятным.
Набор инструментов Leanback UI предоставляет класс BackgroundManager
для изменения фона активности вашего ТВ-приложения. В следующем примере показано, как создать простой метод для обновления фона в активности вашего ТВ-приложения:
Котлин
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Ява
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
Многие приложения для просмотра мультимедиа автоматически обновляют фон, когда пользователь перемещается по спискам мультимедиа. Для этого вы можете настроить прослушиватель выбора для автоматического обновления фона на основе текущего выбора пользователя. В следующем примере показано, как настроить класс OnItemViewSelectedListener
для перехвата событий выбора и обновления фона:
Котлин
protected fun clearBackground() { BackgroundManager.getInstance(this).drawable = defaultBackground } protected fun getDefaultItemViewSelectedListener(): OnItemViewSelectedListener = OnItemViewSelectedListener { _, item, _, _ -> if (item is Movie) { item.getBackdropDrawable().also { background -> updateBackground(background) } } else { clearBackground() } }
Ява
protected void clearBackground() { BackgroundManager.getInstance(this).setDrawable(defaultBackground); } protected OnItemViewSelectedListener getDefaultItemViewSelectedListener() { return new OnItemViewSelectedListener() { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie ) { Drawable background = ((Movie)item).getBackdropDrawable(); updateBackground(background); } else { clearBackground(); } } }; }
Примечание. Предыдущая реализация представляет собой простой пример для иллюстрации. При создании этой функции в своем приложении запустите действие фонового обновления в отдельном потоке для повышения производительности. Кроме того, если вы планируете обновлять фон в ответ на прокрутку элементов пользователями, добавьте время для задержки обновления фонового изображения до тех пор, пока пользователь не остановится на элементе. Этот метод позволяет избежать чрезмерного обновления фонового изображения.