Tworzenie przeglądarki katalogu

Aplikacja do multimediów działająca na telewizorze musi umożliwiać użytkownikom przeglądanie oferty treści, wybieranie treści i rozpoczynanie odtwarzania. Przeglądanie treści musi być proste i intuicyjne, a jednocześnie przyjemne dla oka i wciągające.

W tym przewodniku omawiamy, jak korzystać z klas dostępnych w bibliotece Leanback na Androidax do implementowania interfejsu użytkownika do przeglądania muzyki i filmów z katalogu multimediów aplikacji.

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

Ekran główny aplikacji

Rysunek 1. Fragment przeglądania przykładowej aplikacji Stackdriver wyświetla dane katalogu filmów.

Utwórz układ przeglądania multimediów

Klasa BrowseSupportFragment w bibliotece SKAdNetwork pozwala utworzyć główny układ do przeglądania kategorii i wierszy elementów multimedialnych przy minimalnej uwzględnieniu 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>

Ten widok jest ustawiany przez główną aktywność aplikacji, 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. Określają też parametry układu, takie jak ikona i tytuł, oraz informację o tym, 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 na temat ukrywania nagłówków znajdziesz w sekcji Ukrywanie i wyłączanie nagłówków.

Podklasa aplikacji, która implementuje metody BrowseSupportFragment, konfiguruje też detektory zdarzeń dla działań użytkownika związanych z elementami interfejsu i przygotowuje menedżera działającego w tle, jak 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ądania, tak jak to pokazano na rysunkach 1 i 2. Ta metoda zastępuje ciąg tytułu zasobem rysowalnym, jeśli jest też wywoływany setTitle(). Zasób rysowalny musi mieć wysokość 52 dp.
  • setTitle() ustawia ciąg tytułu w prawym górnym rogu fragmentu przeglądania, chyba że element setBadgeDrawable() zostanie wywołany.
  • 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 zwłaszcza kolor tła sekcji nagłówka, z określoną wartością koloru.
  • setSearchAffordanceColor() ustawia kolor ikony wyszukiwania o określonej wartości. Ikona wyszukiwania pojawi się w lewym górnym rogu fragmentu przeglądania, jak widać na rysunkach 1 i 2.

Dostosowywanie widoków nagłówka

Fragment przeglądania widoczny na ilustracji 1 zawiera nazwy kategorii filmów, które są nagłówkami wierszy w bazie danych filmów w widokach tekstowych. Możesz też dostosować nagłówek tak, aby zawierał dodatkowe widoki w bardziej złożonym układzie. W poniższych sekcjach opisano, jak dołączyć widok obrazu z ikoną obok nazwy kategorii, tak jak pokazano na ilustracji 2.

Ekran główny aplikacji

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

Układ nagłówka wiersza jest zdefiniowany w następujący 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 abstrakcyjne metody, aby utworzyć, powiązać i usunąć właściciela widoku. Z przykładu poniżej dowiesz się, jak powiązać osobę przeglądającą z 2 widokami danych: 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
    }
}

Aby można było przewijać nagłówki za pomocą pada kierunkowego, należy je zaznaczyć. Możesz to zrobić na 2 sposoby:

  • Ustaw widok, 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
        // ...
    }
    
  • Ustaw układ, który można 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ę katalogową, użyj metody setHeaderPresenterSelector(), aby ustawić prezentację dla nagłówka wiersza, tak jak 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 funkcji TalkBack.

Ukryj lub wyłącz nagłówki

Czasami nie chcesz, by nagłówki wierszy się wyświetlały, na przykład gdy nie ma wystarczającej liczby kategorii, aby wymagać przewijanej listy. Wywołaj metodę BrowseSupportFragment.setHeadersState() w metodzie onActivityCreated() danego fragmentu, aby ukryć lub wyłączyć nagłówki wierszy. Metoda setHeadersState() ustawia początkowy stan nagłówków we fragmencie przeglądania, biorąc pod uwagę jedną z tych stałych jako parametr:

  • HEADERS_ENABLED: po utworzeniu aktywności związanej z fragmentem przeglądania nagłówki są domyślnie włączone i wyświetlane. Nagłówki są wyświetlane w sposób pokazany na rysunkach 1 i 2 na tej stronie.
  • HEADERS_HIDDEN: po utworzeniu aktywności związanej z fragmentem przeglądania nagłówki są domyślnie włączone i ukryte. Sekcja nagłówka ekranu jest zwinięta, tak jak na ilustrzeniu w sekcji Udostępnianie widoku kart. Użytkownik może rozwinąć sekcję nagłówka, klikając ją.
  • HEADERS_DISABLED: po utworzeniu aktywności związanej z fragmentem przeglądania nagłówki są domyślnie wyłączone i nigdy nie są wyświetlane.

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

Wyświetl listy multimediów

Klasa BrowseSupportFragment umożliwia definiowanie i wyświetlanie kategorii treści multimedialnych oraz elementów multimedialnych z katalogu multimediów, które można przeglądać, za pomocą adapterów i prezenterów. Adaptery umożliwiają podłączenie lokalnych lub internetowych źródeł danych, które zawierają informacje z katalogu multimediów. Adaptery używają prezenterów do tworzenia widoków i wiązać z nimi dane na potrzeby wyświetlania elementu na ekranie.

Ten przykładowy kod pokazuje implementację parametru 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 prowadzącego na potrzeby elementów multimedialnych możesz utworzyć adapter i podłączyć go do interfejsu BrowseSupportFragment, aby wyświetlić te elementy na ekranie i przejrzeć je użytkownik. Poniższy przykładowy kod pokazuje, jak utworzyć adapter do wyświetlania kategorii i elementów z tych kategorii przy użyciu 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 lub usługi internetowej. Przykład aplikacji do przeglądania, która używa danych pobranych z sieci, znajdziesz w przykładowej aplikacji funkcji TalkBack.

Aktualizowanie tła

Aby uatrakcyjnić wygląd aplikacji do przeglądania multimediów na telewizorze, możesz zaktualizować obraz tła podczas przeglądania treści. Ta technika może sprawić, że korzystanie z aplikacji będzie bardziej filmowe i przyjemniejsze.

Biblioteka obsługi funkcji TalkBack zawiera klasę BackgroundManager służącą do zmiany tła aktywności w aplikacji telewizyjnej. Poniższy przykład pokazuje, jak utworzyć prostą metodę aktualizowania tła w aktywności w 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 porusza się między stronami. Aby to zrobić, możesz skonfigurować detektor wyboru tak, aby automatycznie aktualizował tło na podstawie bieżącego wyboru użytkownika. Poniższy przykład pokazuje, jak skonfigurować klasę OnItemViewSelectedListener, aby rejestrować 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 tylko charakter poglądowy. Jeśli tworzysz tę funkcję we własnej aplikacji, uruchom działanie aktualizacji w tle w oddzielnym wątku, aby zwiększyć wydajność. Jeśli planujesz aktualizować tło, gdy użytkownicy przewijają elementy, dodaj czas, by opóźnić aktualizację obrazu tła do momentu, aż użytkownik skupi się na produkcie. Ta metoda pozwala uniknąć nadmiernej liczby aktualizacji obrazu tła.