Tworzenie przeglądarki katalogu

Ulepszaj dzięki funkcji tworzenia wiadomości
Dzięki Jetpack Compose na system operacyjny Android TV możesz tworzyć atrakcyjne interfejsy użytkownika przy użyciu minimalnej ilości pisania.

Aplikacja do multimediów zainstalowana na telewizorze musi umożliwiać użytkownikom przeglądanie oferty, wybór treści i rozpoczęcie odtwarzania. Przeglądanie treści musi być proste i intuicyjne, a także atrakcyjne wizualnie i angażujące.

Z tego przewodnika dowiesz się, jak korzystać z klas z biblioteki androidx.leanback, aby wdrożyć interfejs użytkownika do przeglądania muzyki lub filmów z katalogu multimediów Twojej aplikacji.

Uwaga: podany tutaj przykład implementacji używa BrowseSupportFragment zamiast wycofanej klasy BrowseFragment. BrowseSupportFragment rozszerza klasę AndroidX Fragment, co pomaga zapewnić spójne działanie aplikacji na różnych urządzeniach i w różnych wersjach Androida.

Ekran główny aplikacji

Rysunek 1. Fragment przeglądania w przykładowej aplikacji Leanback zawiera dane katalogu filmów.

Tworzenie układu przeglądania multimediów

Klasa BrowseSupportFragment w zestawie narzędzi interfejsu Leanback pozwala utworzyć podstawowy układ do przeglądania kategorii i wierszy elementów multimedialnych przy użyciu minimalnej ilości kodu. Poniższy przykład pokazuje, jak utworzyć układ zawierający obiekt 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>

Główne działanie aplikacji ustawia ten widok, jak w tym przykładzie:

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);
    }
...

Metody BrowseSupportFragment wypełniają widok danymi wideo i elementami interfejsu oraz ustawiają parametry układu, takie jak ikona i tytuł, oraz określają, czy nagłówki kategorii są włączone.

Więcej informacji o konfigurowaniu elementów interfejsu znajdziesz w sekcji Ustawianie elementów interfejsu. Więcej informacji o ukrywaniu nagłówków znajdziesz w sekcji Ukrywanie i wyłączanie nagłówków.

Podklasa aplikacji, która implementuje metody BrowseSupportFragment, konfiguruje też detektory zdarzeń działań użytkownika w elementach interfejsu i przygotowuje menedżera w tle, jak pokazano w tym przykładzie:

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());
    }
...

Ustawianie elementów interfejsu

W poprzednim przykładzie metoda prywatna setupUIElements() wywołuje kilka metod BrowseSupportFragment, aby określić styl przeglądarki katalogu multimediów:

  • setBadgeDrawable() umieszcza określony zasób rysowalny w prawym górnym rogu fragmentu Przeglądaj, jak pokazano na rysunkach 1 i 2. Ta metoda zastępuje ciąg tytułu elementem możliwym do rysowania, jeśli wywoływana jest też metoda setTitle(). Zasób rysowalny musi mieć wysokość 52 dp.
  • setTitle() ustawia ciąg tytułu w prawym górnym rogu fragmentu umożliwiającego przeglądanie, chyba że zostanie wywołany setBadgeDrawable().
  • setHeadersState() i setHeadersTransitionOnBackEnabled() ukrywają lub wyłączają nagłówki. Więcej informacji znajdziesz w sekcji Ukrywanie i wyłączanie nagłówków.
  • setBrandColor() ustawia kolor tła elementów interfejsu we fragmencie przeglądania, a konkretnie kolor tła sekcji nagłówka, na określoną wartość koloru.
  • setSearchAffordanceColor() ustawia kolor ikony wyszukiwania za pomocą określonej wartości koloru. W lewym górnym rogu fragmentu przeglądania, tak jak na rysunkach 1 i 2, pojawi się ikona wyszukiwania.

Dostosowywanie widoków nagłówka

Fragment przeglądania widoczny na rysunku 1 wyświetla w widokach tekstu nazwy kategorii filmów, czyli nagłówki wierszy w bazie danych filmów. Możesz też dostosować nagłówek, aby uwzględnić dodatkowe widoki w bardziej złożonym układzie. W poniższych sekcjach dowiesz się, jak uwzględnić widok obrazów z ikoną obok nazwy kategorii, tak jak to pokazano na rysunku 2.

Ekran główny aplikacji

Rysunek 2. Nagłówki wierszy we fragmencie przeglądania zawierające ikonę i etykietę tekstową.

Układ nagłówka wiersza jest zdefiniowany w taki sposób:

<?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>

Za pomocą Presenter i wdróż metody abstrakcyjne, aby utworzyć oraz powiązać i odłączyć element widoku danych. Poniższy przykład pokazuje, jak powiązać właściciela widoku za pomocą 2 widoków: ImageView i 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
    }
}

Nagłówki muszą umożliwiać zaznaczenie, aby można było je przewijać za pomocą pada kierunkowego. Możesz to zrobić na 2 sposoby:

  • Konfigurowanie widoku, który można zaznaczyć w aplikacji 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
        // ...
    }
    
  • Skonfiguruj układ tak, aby można go było zaznaczyć:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Na koniec w implementacji BrowseSupportFragment, która wyświetla przeglądarkę katalogu, użyj metody setHeaderPresenterSelector(), aby ustawić prezentację nagłówka wiersza, jak pokazano w przykładzie poniżej.

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();
    }
});

Pełny przykład znajdziesz w przykładowej aplikacji Leanback.

Ukryj lub wyłącz nagłówki

Czasami nie chcesz, aby nagłówki wierszy się wyświetlały, np. gdy nie ma wystarczającej liczby kategorii, aby wymagać przewijania listy. Wywołaj metodę BrowseSupportFragment.setHeadersState() podczas metody onActivityCreated() we fragmencie, aby ukryć lub wyłączyć nagłówki wierszy. Metoda setHeadersState() ustawia początkowy stan nagłówków we fragmencie przeglądania, przy użyciu jednej z tych stałych jako parametru:

  • HEADERS_ENABLED: po utworzeniu aktywności związanej z fragmentami przeglądania nagłówki są domyślnie włączone i wyświetlane. Nagłówki są widoczne na rysunkach 1 i 2 tej strony.
  • HEADERS_HIDDEN: po utworzeniu aktywności dotyczącej fragmentów przeglądania nagłówki są domyślnie włączone i ukryte. Sekcja nagłówka ekranu jest zwinięta, jak widać na ilustracji w artykule Udostępnianie widoku karty. Użytkownik może kliknąć zwiniętą sekcję nagłówka, by ją rozwinąć.
  • HEADERS_DISABLED: po utworzeniu aktywności związanej z fragmentami przeglądania nagłówki są domyślnie wyłączone i nigdy nie są wyświetlane.

Jeśli ustawiona jest wartość HEADERS_ENABLED lub HEADERS_HIDDEN, możesz wywołać setHeadersTransitionOnBackEnabled(), by umożliwić powrót do nagłówka wiersza z wybranego elementu treści. Ta opcja jest domyślnie włączona, jeśli nie wywołujesz tej metody. Aby samodzielnie wykonać ruch wsteczny, przekaż false do setHeadersTransitionOnBackEnabled() i zaimplementuj własną obsługę stosu.

Wyświetl listy mediów

Klasa BrowseSupportFragment umożliwia definiowanie i wyświetlanie możliwych do przeglądania kategorii treści multimedialnych oraz elementów multimedialnych z katalogu multimediów za pomocą adapterów i prowadzących. Adaptery umożliwiają łączenie się z lokalnymi lub online źródłami danych, które zawierają informacje z katalogu multimediów. Adaptery korzystają z narzędzi prezentujących, aby tworzyć widoki i wiązać z nimi dane w celu wyświetlenia elementu na ekranie.

Poniżej znajduje się przykładowy kod pokazujący implementację Presenter do wyświetlania danych ciągu znaków:

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
    }
}

Po utworzeniu klasy prowadzącej dla swoich elementów multimedialnych możesz utworzyć adapter i podłączyć go do BrowseSupportFragment, aby wyświetlać te elementy na ekranie i przeglądać je przez użytkownika. Ten przykładowy kod pokazuje, jak utworzyć adapter do wyświetlania kategorii i elementów z tych kategorii za pomocą klasy StringPresenter pokazanej w poprzednim przykładzie kodu:

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);
}

Ten przykład pokazuje statyczną implementację adapterów. Typowa aplikacja do przeglądania multimediów używa danych z bazy danych online lub usługi internetowej. Przykładową aplikację do przeglądania, która używa danych pobranych z internetu, znajdziesz w przykładowej aplikacji Leanback.

Zaktualizuj tło

Aby zwiększyć atrakcyjność aplikacji do przeglądania multimediów na telewizorze, możesz zaktualizować obraz tła, gdy użytkownicy przeglądają treści. Dzięki tej technice interakcja z aplikacją może być przyjemniejsza i filmowa.

Zestaw narzędzi interfejsu Leanback zawiera klasę BackgroundManager do zmiany tła aktywności w aplikacji na telewizorze. Ten przykład pokazuje, jak utworzyć prostą metodę aktualizowania tła w aktywności w aplikacji na telewizorze:

Kotlin

protected fun updateBackground(drawable: Drawable) {
    BackgroundManager.getInstance(this).drawable = drawable
}

Java

protected void updateBackground(Drawable drawable) {
    BackgroundManager.getInstance(this).setDrawable(drawable);
}

Wiele aplikacji do przeglądania multimediów automatycznie aktualizuje tło, gdy użytkownik porusza się po liście multimediów. Aby to zrobić, możesz skonfigurować detektor wyboru, który będzie automatycznie aktualizował tło na podstawie bieżącego wyboru użytkownika. Ten przykład pokazuje, jak skonfigurować klasę OnItemViewSelectedListener, aby przechwytywać zdarzenia wyboru i aktualizować tło:

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();
            }
        }
    };
}

Uwaga: poprzednia implementacja ma charakter poglądowy. Podczas tworzenia tej funkcji we własnej aplikacji uruchom działanie aktualizacji w tle w oddzielnym wątku, aby zwiększyć wydajność. Jeśli planujesz aktualizować tło w odpowiedzi na przewijanie elementów przez użytkowników, dodaj czas na opóźnienie aktualizacji obrazu tła, dopóki użytkownik nie zdecyduje się na zakup. Ta metoda pozwala uniknąć nadmiernej aktualizacji obrazu tła.