Una aplicación multimedia que se ejecute en una TV debe permitir a los usuarios explorar sus ofertas de contenido, hacer una y comenzar a reproducir contenido. La experiencia de navegación de contenido debe ser simple e intuitiva, así como visualmente agradable y atractiva.
En esta guía, se explica cómo usar las clases que proporciona la biblioteca de AndroidX.Leanback para implementar una interfaz de usuario que permita explorar música o videos del catálogo multimedia de tu app.
Nota: En el ejemplo de implementación que se muestra aquí, se usan
BrowseSupportFragment
en lugar de la versión obsoleta de BrowseFragment
. BrowseSupportFragment
extiende AndroidX.
clase Fragment
,
lo que ayuda a garantizar un comportamiento coherente
en todos los dispositivos y versiones de Android.
![Pantalla principal de la app](https://developer.android.com/static/images/tv/app-browse.png?authuser=19&hl=es)
Figura 1: El fragmento de navegación de la app de ejemplo de Leanback muestra datos de un catálogo de videos.
Cómo crear un diseño para navegadores de medios
El BrowseSupportFragment
en el kit de herramientas de la IU de Leanback
te permite crear un diseño principal para la navegación por categorías y filas de elementos multimedia con una
mínimo de código. En el siguiente ejemplo, se muestra cómo crear un diseño que contenga un elemento
Objeto 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>
La actividad principal de la app establece esta vista, tal como se muestra en el siguiente ejemplo:
Kotlin
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } ...
Java
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } ...
Los métodos BrowseSupportFragment
propagan la vista con el
datos de video y elementos de la interfaz de usuario, y configurar parámetros de diseño, como el ícono, el título y
si los encabezados de categoría están habilitados.
La subclase de la aplicación que implementa BrowseSupportFragment
también configuran objetos de escucha de eventos para las acciones del usuario en los elementos de la IU y prepara
el administrador en segundo plano, como se muestra en el siguiente ejemplo:
Kotlin
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() } ...
Java
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()); } ...
Cómo establecer elementos de IU
En el ejemplo anterior, el método privado setupUIElements()
llama a varias
BrowseSupportFragment
métodos para definir el estilo del navegador del catálogo de contenido multimedia:
setBadgeDrawable()
coloca el recurso de elemento de diseño especificado en la esquina superior derecha del fragmento de navegación, como en las figuras 1 y 2. Este método reemplaza la cadena del título por el elemento de diseño, si también se llama asetTitle()
. El recurso de elemento de diseño debe ser de 52 dp. alto.setTitle()
establece la cadena de título en la esquina superior derecha del fragmento de navegación, a menos que Se llama asetBadgeDrawable()
.setHeadersState()
ysetHeadersTransitionOnBackEnabled()
ocultan o inhabilitan los encabezados. Consulta la sección Cómo ocultar o inhabilitar encabezados para obtener más información.setBrandColor()
Establece el color de fondo para los elementos de la IU en el fragmento de navegación, específicamente el encabezado. sección, con el valor de color especificado.setSearchAffordanceColor()
establece el color del ícono de búsqueda con el valor de color especificado. El ícono de búsqueda aparece en la esquina superior izquierda del fragmento de navegación, como se muestra en las figuras 1 y 2.
Cómo personalizar las vistas de encabezados
El fragmento de navegación que se muestra en la figura 1 muestra los nombres de las categorías de videos. que son los encabezados de las filas en la base de datos de videos, en vistas de texto. También puedes personalizar el para incluir vistas adicionales en un diseño más complejo. En las siguientes secciones, se muestra cómo incluir una vista de imagen que muestre un ícono junto al nombre de la categoría, como puede verse en la figura 2.
![Pantalla principal de la app](https://developer.android.com/static/images/tv/custom-head.png?authuser=19&hl=es)
Figura 2: Los encabezados de filas en el fragmento de navegación con un ícono y una etiqueta de texto.
El diseño para el encabezado de fila se define de la siguiente manera:
<?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>
Usa un Presenter
y, luego, implementa
métodos abstractos para crear, vincular y desvincular el contenedor de vistas. Lo siguiente
ejemplo muestra cómo vincular el viewholder con dos vistas, un
ImageView
y TextView
.
Kotlin
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 } }
Java
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 } }
Tus encabezados deben poder enfocarse de manera que el pad direccional pueda usarse para lo siguiente: te desplazas por ellas. Existen dos maneras de administrar esto:
- Establece la vista para que pueda enfocarse en
onBindViewHolder()
:Kotlin
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.focusable = View.FOCUSABLE // ... }
Java
@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 // ... }
- Establecer el diseño para que pueda enfocarse:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Por último, en la implementación de BrowseSupportFragment
, que muestra el elemento
navegador de catálogos, usa el setHeaderPresenterSelector()
para establecer el presentador como encabezado de fila, como se muestra en el siguiente ejemplo.
Kotlin
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
Java
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
Para ver un ejemplo completo, consulta la App de ejemplo de Leanback de Google Cloud.
Cómo ocultar o inhabilitar encabezados
A veces no quieres que aparezcan los encabezados de fila, por ejemplo, cuando no hay suficientes
categorías requieren una lista desplazable. Llama al BrowseSupportFragment.setHeadersState()
durante el método onActivityCreated()
del fragmento
para ocultar o inhabilitar los encabezados de fila. El setHeadersState()
establece el estado inicial de los encabezados en el fragmento de navegación, según una de las siguientes opciones:
constantes como un parámetro:
HEADERS_ENABLED
: Cuando se crea la actividad del fragmento de navegación, los encabezados están habilitados y los muestran de forma predeterminada. Los encabezados aparecen como se muestra en las figuras 1 y 2 de esta página.HEADERS_HIDDEN
: Cuando se crea la actividad del fragmento de navegación, los encabezados están habilitados y ocultos de forma predeterminada. La sección de encabezado de la pantalla está contraída, como se muestra en una figura en Proporciona una vista de tarjetas. El usuario puede seleccionar la sección contraída del encabezado para expandirla.HEADERS_DISABLED
: Cuando se crea la actividad del fragmento de navegación, los encabezados están inhabilitados de forma predeterminada y se nunca se muestra.
Si se establece HEADERS_ENABLED
o HEADERS_HIDDEN
, puedes llamar a
setHeadersTransitionOnBackEnabled()
para admitir el regreso al encabezado de la fila desde un elemento de contenido seleccionado en la fila. Esto lo habilita
la configuración predeterminada si no llamas al método. Para controlar el movimiento de espalda,
pasar false
a setHeadersTransitionOnBackEnabled()
e implementar
tu propio manejo de la pila de actividades.
Cómo mostrar listas de medios
El BrowseSupportFragment
te permite
definir y mostrar elementos multimedia y categorías de contenido multimedia explorables desde
un catálogo de medios con adaptadores y presentadores. Los adaptadores te permiten conectarte
a fuentes de datos locales o en línea que contengan la información de tu catálogo de medios.
Los adaptadores usan presentadores para crear vistas y vincular datos a esas vistas para
mostrar un elemento en pantalla.
En el siguiente código de ejemplo, se muestra la implementación de un Presenter
para mostrar datos de una cadena:
Kotlin
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 } }
Java
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 } }
Una vez que hayas creado una clase de presentador para tus elementos multimedia, puedes crear
un adaptador y conéctalo al BrowseSupportFragment
para mostrar esos elementos en la pantalla para que el usuario navegue. El siguiente ejemplo
código demuestra cómo construir un adaptador para mostrar categorías y elementos
en esas categorías con la clase StringPresenter
que se muestra en el
ejemplo de código anterior:
Kotlin
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 }
Java
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); }
Este ejemplo muestra la implementación estática de los adaptadores. Una aplicación típica de navegación de medios usa datos de una base de datos en línea o de un servicio web. Veamos un ejemplo de una aplicación de navegación que usa datos recuperados de la Web, consulta el App de ejemplo de Leanback de Google Cloud.
Cómo actualizar el fondo
Para agregar interés visual a una app de navegación de contenido multimedia en la TV, puedes actualizar el fondo mientras los usuarios navegan por el contenido. Esta técnica puede hacer que la interacción con tu app sea más cinematográfico y agradable.
El kit de herramientas de la IU de Leanback proporciona un BackgroundManager
.
para cambiar el fondo de la actividad de tu aplicación para TV. En el siguiente ejemplo, se muestra cómo
Crea un método simple para actualizar el fondo de la actividad de tu app para TV:
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
Muchas apps de navegación de contenido multimedia actualizan automáticamente el fondo a medida que el usuario navega.
a través de listas de medios. Para ello, puedes configurar un objeto de escucha de selección para que automáticamente
actualizar el fondo en función de la selección actual del usuario. En el siguiente ejemplo, se muestra cómo
para configurar una clase OnItemViewSelectedListener
detectar eventos de selección y actualizar el fondo:
Kotlin
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() } }
Java
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(); } } }; }
Nota: La implementación anterior es un ejemplo simple para los fines de ilustración. Cuando crees esta función en tu propia app, ejecuta el actualización del fondo en un subproceso independiente para mejorar el rendimiento Además, si planeas actualizar el fondo en respuesta a los usuarios que se desplazan por los elementos, agregar un tiempo para retrasar la actualización de una imagen de fondo hasta que el usuario se detenga en un elemento. Con esta técnica, se evita actualizaciones excesivas de imágenes de fondo