Медиаприложение, работающее на телевизоре, должно позволять пользователям просматривать предлагаемый контент, делать выбор и запускать воспроизведение. Процесс просмотра контента должен быть простым и интуитивно понятным, а также визуально привлекательным и увлекательным.
В этом руководстве обсуждается, как использовать классы, предоставляемые устаревшей библиотекой androidx.leanback, для реализации пользовательского интерфейса для просмотра музыки или видео из каталога мультимедиа вашего приложения.
Примечание: В представленном здесь примере реализации используется BrowseSupportFragment
вместо устаревшего класса BrowseFragment
. BrowseSupportFragment
расширяет класс AndroidX Fragment
, обеспечивая единообразное поведение на разных устройствах и версиях Android.

Рисунок 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.

Рисунок 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
для изменения фона активности вашего приложения TV. В следующем примере показано, как создать простой метод для обновления фона в активности вашего приложения TV:
Котлин
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(); } } }; }
Примечание: Предыдущая реализация — это простой пример, представленный для иллюстрации. При создании этой функции в вашем приложении выполняйте обновление фона в отдельном потоке для повышения производительности. Кроме того, если вы планируете обновлять фон в ответ на прокрутку элементов пользователем, добавьте время задержки обновления фонового изображения, пока пользователь не остановится на элементе. Этот метод позволяет избежать чрезмерного обновления фонового изображения.