Tworzenie przeglądarki katalogu

Twórz lepsze aplikacje za pomocą Compose
Twórz atrakcyjne interfejsy użytkownika przy użyciu minimalnej ilości kodu za pomocą Jetpack Compose na Androida TV OS.

Aplikacja do multimediów działająca na telewizorze musi umożliwiać użytkownikom przeglądanie oferty treści, wybieranie ich i odtwarzanie. Przeglądanie treści musi być proste i intuicyjne, a także atrakcyjne wizualnie i angażujące.

W tym przewodniku omawiamy, jak za pomocą klas udostępnianych przez wycofaną bibliotekę androidx.leanback zaimplementować interfejs użytkownika do przeglądania muzyki lub filmów z katalogu multimediów aplikacji.

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

Ekran główny aplikacji

Rysunek 1. Fragment przeglądania w przykładowej aplikacji Leanback wyświetla dane katalogu wideo.

Tworzenie układu przeglądania multimediów

Klasa BrowseSupportFragment w zestawie narzędzi interfejsu Leanback umożliwia tworzenie podstawowego układu 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 pokazano 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 filmu i elementami interfejsu oraz ustawiają parametry układu, takie jak ikona i tytuł, a także to, 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 lub wyłączanie nagłówków.

Podklasa aplikacji, która implementuje metody BrowseSupportFragment, konfiguruje też odbiorniki zdarzeń dla działań użytkownika w elementach interfejsu i przygotowuje menedżera tła, 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 nadać styl przeglądarce katalogu multimediów:

  • setBadgeDrawable() umieszcza określony obiekt rysowalny w prawym górnym rogu fragmentu przeglądania, jak pokazano na rysunkach 1 i 2. Ta metoda zastępuje ciąg tytułu zasobem rysowalnym, jeśli wywoływana jest też funkcja setTitle(). Obiekt rysowalny musi mieć wysokość 52 dp.
  • setTitle() ustawia ciąg znaków tytułu w prawym górnym rogu fragmentu przeglądania, chyba że wywoływana jest funkcja setBadgeDrawable().
  • setHeadersState()setHeadersTransitionOnBackEnabled() ukrywają lub wyłączają nagłówki. Więcej informacji znajdziesz w sekcji Ukrywanie lub wyłączanie nagłówków.
  • setBrandColor() ustawia kolor tła elementów interfejsu w fragmencie przeglądania, w szczególności kolor tła sekcji nagłówka, na podaną wartość koloru.
  • setSearchAffordanceColor() ustawia kolor ikony wyszukiwania na podaną wartość. Ikona wyszukiwania jest widoczna w lewym górnym rogu fragmentu przeglądania, jak pokazano na rysunkach 1 i 2.

Dostosowywanie widoków nagłówka

Fragment przeglądania pokazany na rysunku 1 wyświetla nazwy kategorii filmów, które są nagłówkami wierszy w bazie danych filmów, w widokach tekstowych. Możesz też dostosować nagłówek, aby uwzględnić dodatkowe widoki w bardziej złożonym układzie. W sekcjach poniżej dowiesz się, jak dodać widok obrazu, który wyświetla ikonę obok nazwy kategorii, jak pokazano na rysunku 2.

Ekran główny aplikacji

Rysunek 2. Nagłówki wierszy w fragmencie przeglądania z ikoną i etykietą tekstową.

Układ nagłówka wiersza jest zdefiniowany w ten 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>

Użyj Presenter i zastosuj metody abstrakcyjne, aby utworzyć, powiązać i odłączyć obiekt View Holder. Poniższy przykład pokazuje, jak powiązać element Viewholder z 2 widokami: ImageViewTextView.

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ą być możliwe do zaznaczenia, aby można było przewijać je za pomocą pada kierunkowego. Możesz to zrobić na 2 sposoby:

  • Ustaw widok jako fokusowalny w 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
        // ...
    }
  • Ustaw układ tak, aby można było na nim skupić uwagę:
    <?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ć prezenter nagłówka wiersza, jak pokazano w tym przykładzie.

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.

Ukrywanie i wyłączanie nagłówków

Czasami nie chcesz, aby nagłówki wierszy się wyświetlały, np. gdy nie ma wystarczającej liczby kategorii, aby wymagać listy z możliwością przewijania. Wywołaj metodę BrowseSupportFragment.setHeadersState() w trakcie metody onActivityCreated() fragmentu, aby ukryć lub wyłączyć nagłówki wierszy. Metoda setHeadersState() ustawia stan początkowy nagłówków we fragmencie przeglądania. Jako parametr przyjmuje jedną z tych stałych:

  • HEADERS_ENABLED: gdy tworzony jest fragment przeglądania, nagłówki są domyślnie włączone i wyświetlane. Nagłówki będą wyglądać tak, jak na ilustracjach 1 i 2 na tej stronie.
  • HEADERS_HIDDEN: gdy tworzona jest aktywność fragmentu przeglądania, nagłówki są domyślnie włączone i ukryte. Sekcja nagłówka ekranu jest zwinięta, jak pokazano na rysunku w artykule Wyświetlanie karty. Użytkownik może kliknąć zwiniętą sekcję nagłówka, aby ją rozwinąć.
  • HEADERS_DISABLED: gdy tworzony jest fragment przeglądania, nagłówki są domyślnie wyłączone i nigdy nie są wyświetlane.

Jeśli ustawiono wartość HEADERS_ENABLED lub HEADERS_HIDDEN, możesz wywołać setHeadersTransitionOnBackEnabled() , aby umożliwić powrót do nagłówka wiersza z wybranego elementu treści w wierszu. Ta opcja jest domyślnie włączona, jeśli nie wywołasz metody. Aby samodzielnie obsługiwać ruch wstecz, przekaż false do setHeadersTransitionOnBackEnabled() i zastosuj własną obsługę stosu wstecznego.

Wyświetlanie list multimediów

Klasa BrowseSupportFragment umożliwia definiowanie i wyświetlanie kategorii treści multimedialnych, które można przeglądać, oraz elementów multimedialnych z katalogu multimediów za pomocą adapterów i prezenterów. Adaptery umożliwiają łączenie się z lokalnymi lub internetowymi źródłami danych, które zawierają informacje o katalogu multimediów. Adaptery używają prezenterów do tworzenia widoków i powiązywania z nimi danych w celu wyświetlania elementu na ekranie.

Poniższy przykładowy kod pokazuje implementację Presenter do wyświetlania danych w postaci 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 prezentera dla elementów multimedialnych możesz utworzyć adapter i dołączyć go do BrowseSupportFragment, aby wyświetlać te elementy na ekranie i umożliwiać użytkownikowi ich przeglądanie. Poniższy przykładowy kod pokazuje, jak utworzyć adapter do wyświetlania kategorii i elementów w tych kategoriach 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 korzysta z danych z bazy danych online lub usługi internetowej. Przykład aplikacji do przeglądania, która korzysta z danych pobranych z internetu, znajdziesz w  przykładowej aplikacji Leanback .

Aktualizowanie tła

Aby uatrakcyjnić wizualnie aplikację do przeglądania multimediów na telewizorze, możesz aktualizować obraz tła, gdy użytkownicy przeglądają treści. Ta technika może sprawić, że korzystanie z aplikacji będzie bardziej filmowe i przyjemne.

Zestaw narzędzi interfejsu Leanback udostępnia klasę BackgroundManager do zmiany tła aktywności aplikacji na telewizor. Poniższy przykład pokazuje, jak utworzyć prostą metodę aktualizowania tła w aktywności aplikacji TV:

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 przegląda listy multimediów. Aby to zrobić, możesz skonfigurować odbiornik wyboru, który będzie automatycznie aktualizować tło na podstawie bieżącego wyboru użytkownika. W przykładzie poniżej pokazujemy, 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 to prosty przykład, który służy wyłącznie do celów ilustracyjnych. Podczas tworzenia tej funkcji w swojej aplikacji uruchamiaj działanie aktualizacji w tle w osobnym wątku, aby zwiększyć wydajność. Jeśli planujesz aktualizować tło w odpowiedzi na przewijanie elementów przez użytkowników, dodaj czas opóźnienia aktualizacji obrazu tła, aż użytkownik zatrzyma się na elemencie. Dzięki tej technice unikniesz nadmiernych aktualizacji obrazu tła.