Um app de música executado em uma TV precisa permitir que os usuários procurem ofertas de conteúdo, façam uma seleção e comecem a reproduzir conteúdo. A experiência de navegação de conteúdo precisa ser simples e intuitiva, além de visualmente agradável e envolvente.
Este guia discute como usar as classes fornecidas pela biblioteca Leanback androidx para implementar uma interface do usuário para a navegação de músicas ou vídeos a partir do catálogo de mídia do app.
Observação:o exemplo de implementação mostrado aqui usa
BrowseSupportFragment
em vez da classe descontinuada BrowseFragment
. A BrowseSupportFragment
estende a classe Fragment
do AndroidX,
ajudando a garantir um comportamento consistente em todos os dispositivos e versões do Android.
![Tela principal do app](https://developer.android.com/static/images/tv/app-browse.png?authuser=5&hl=pt)
Figura 1. O fragmento de navegação do app de amostra Leanback mostra dados do catálogo de vídeo.
Criar um layout de navegação em mídia
A classe BrowseSupportFragment
da biblioteca Leanback
permite criar um layout principal para navegar em categorias e linhas de itens de mídia com um
mínimo de código. O exemplo a seguir mostra como criar um layout que contenha um 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>
A principal atividade do aplicativo define essa exibição, como mostrado no exemplo a seguir:
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); } ...
Os métodos BrowseSupportFragment
preenchem a visualização com
dados de vídeo e elementos da interface, além de definir parâmetros de layout, como ícone e título, e
se os cabeçalhos de categorias estão ativados.
A subclasse do aplicativo que implementa os métodos BrowseSupportFragment
também configura listeners de eventos para ações do usuário nos elementos da interface e prepara
o gerenciador em segundo plano, conforme mostrado no exemplo abaixo:
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()); } ...
Definir elementos da IU
No exemplo anterior, o método particular setupUIElements()
chama vários métodos
BrowseSupportFragment
para estilizar o navegador de catálogo de mídia:
setBadgeDrawable()
posiciona o recurso drawable especificado no canto superior direito do fragmento de navegação, como mostrado nas Figuras 1 e 2. Esse método substituirá a string do título pelo recurso drawable sesetTitle()
também for chamado. O recurso drawable precisa ter 52 dp de altura.setTitle()
define a string do título no canto superior direito do fragmento de navegação, a menos quesetBadgeDrawable()
seja chamado.setHeadersState()
esetHeadersTransitionOnBackEnabled()
ocultam ou desativam os cabeçalhos. Consulte a seção Ocultar ou desativar cabeçalhos para mais informações.setBrandColor()
define a cor de fundo para elementos da IU no fragmento de navegação, especificamente a cor de fundo da seção do cabeçalho, com o valor da cor especificado.setSearchAffordanceColor()
define a cor do ícone de pesquisa com o valor da cor especificado. O ícone de pesquisa aparece no canto superior esquerdo do fragmento de navegação, conforme mostrado nas figuras 1 e 2.
Personalizar as visualizações de cabeçalho
O fragmento de navegação mostrado na Figura 1 mostra os nomes das categorias de vídeo, que são os cabeçalhos de linha no banco de dados de vídeos, em visualizações de texto. Você também pode personalizar o cabeçalho para incluir outras visualizações em um layout mais complexo. As seções abaixo mostram como incluir uma visualização de imagem que mostre um ícone ao lado do nome da categoria, como mostrado na Figura 2.
![Tela principal do app](https://developer.android.com/static/images/tv/custom-head.png?authuser=5&hl=pt)
Figura 2. Os cabeçalhos de linha no fragmento de navegação com um ícone e um rótulo de texto.
O layout para o cabeçalho da linha é definido da seguinte forma:
<?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>
Use um Presenter
e implemente os
métodos abstratos para criar, vincular e desvincular o armazenador de visualização. O exemplo
abaixo mostra como vincular o armazenador de visualização com duas visualizações, uma
ImageView
e uma 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 } }
Os cabeçalhos precisam ser focalizáveis para que o botão direcional possa ser usado para rolar por eles. Há duas maneiras de gerenciar isso:
- Configure a visualização para ser focalizável em
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 // ... }
- Defina o layout para ser focalizável:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
Por fim, na implementação de BrowseSupportFragment
que exibe o
navegador de catálogo, use o método setHeaderPresenterSelector()
para definir o apresentador para o cabeçalho da linha, como mostrado no exemplo a seguir.
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 um exemplo completo, consulte o app de amostra Leanback .
Ocultar ou desativar cabeçalhos
Às vezes, você não quer que os cabeçalhos de linha apareçam, por exemplo, quando não há categorias
suficientes para exigir uma lista rolável. Chame o método BrowseSupportFragment.setHeadersState()
durante o método onActivityCreated()
do fragmento para ocultar ou desativar os cabeçalhos de linha. O método setHeadersState()
define o estado inicial dos cabeçalhos no fragmento de navegação, dada uma das constantes
abaixo como um parâmetro:
HEADERS_ENABLED
: quando a atividade de fragmento de navegação é criada, os cabeçalhos são ativados e mostrados por padrão. Os cabeçalhos aparecem como mostrado nas figuras 1 e 2 desta página.HEADERS_HIDDEN
: quando a atividade de fragmento de navegação é criada, os cabeçalhos são ativados e ocultos por padrão. A seção do cabeçalho da tela é recolhida, como mostrado na figura em Oferecer uma visualização de card. O usuário pode selecionar a seção do cabeçalho recolhida para expandi-la.HEADERS_DISABLED
: quando a atividade de fragmento de navegação é criada, os cabeçalhos são desativados por padrão e nunca são exibidos.
Se HEADERS_ENABLED
ou HEADERS_HIDDEN
estiverem definidos, você poderá chamar
setHeadersTransitionOnBackEnabled()
para oferecer suporte à movimentação de retorno de um item de conteúdo selecionado na linha. Isso será ativado por padrão se você não chamar o método. Para processar o movimento de retorno por conta própria,
transmita false
para setHeadersTransitionOnBackEnabled()
e implemente seu próprio processamento da backstack.
Exibir listas de mídia
A classe BrowseSupportFragment
permite definir e exibir categorias de conteúdo de mídia navegáveis, além de itens de um catálogo de mídia usando adaptadores e apresentadores. Adaptadores permitem a conexão
a fontes de dados locais ou on-line que contêm as informações do catálogo de mídia.
Os adaptadores usam apresentadores para criar visualizações e vincular dados a elas para
exibir um item na tela.
O código de exemplo abaixo mostra a implementação de um Presenter
para mostrar dados de string:
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 } }
Depois de construir uma classe de apresentador para seus itens de mídia, você pode criar um adaptador e anexá-lo ao BrowseSupportFragment
para exibir esses itens na tela para navegação pelo usuário. O código de exemplo a seguir demonstra como construir um adaptador para exibir categorias e itens nessas categorias usando a classe StringPresenter
mostrada no exemplo 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 exemplo mostra uma implementação estática dos adaptadores. Um aplicativo típico de navegação em mídia usa dados de um banco de dados on-line ou serviço da Web. Para ver um exemplo de aplicativo de navegação que usa dados recuperados da Web, consulte o App de amostra Leanback.
Atualizar a imagem de plano de fundo
Para adicionar interesse visual a um app de navegação de mídia na TV, você pode atualizar a imagem de plano de fundo conforme os usuários navegam pelo conteúdo. Essa técnica pode tornar a interação com o app mais cinemática e agradável.
A Biblioteca de Suporte Leanback oferece uma classe BackgroundManager
para mudar o plano de fundo da atividade do app para TV. O exemplo abaixo mostra como
criar um método simples para atualizar o plano de fundo dentro da atividade do 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); }
Muitos apps de navegação de mídia atualizam automaticamente o segundo plano à medida que o usuário navega por listagens de mídia. Para fazer isso, configure um listener de seleção para atualizar automaticamente o plano de fundo com base na seleção atual do usuário. O exemplo a seguir mostra como
definir uma classe OnItemViewSelectedListener
para
capturar eventos selecionados e atualizar o plano de fundo:
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(); } } }; }
Observação:a implementação anterior é um exemplo simples para fins de ilustração. Ao criar essa função no seu app, execute a ação de atualização em segundo plano em uma linha de execução separada para melhorar o desempenho. Além disso, se você planeja atualizar o plano de fundo em resposta à rolagem dos usuários pelos itens, adicione um tempo para atrasar a atualização da imagem de plano de fundo até que o usuário pare em um item. Essa técnica evita o excesso de atualizações de imagens de plano de fundo.