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, i rozpocznie się odtwarzanie treści. sposób przeglądania treści, muszą być proste i intuicyjne, a także atrakcyjne wizualnie i angażujące.

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

Uwaga: podany tutaj przykład implementacji wymaga BrowseSupportFragment zamiast wycofanego BrowseFragment zajęcia. BrowseSupportFragment to rozszerzenie AndroidaX Zajęcia: Fragment, co pomaga w zapewnieniu spójnego działania aplikacji na różnych urządzeniach i w różnych wersjach Androida.

Ekran główny aplikacji

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

Tworzenie układu przeglądania multimediów

BrowseSupportFragment w zestawie narzędzi interfejsu Leanback. pozwala utworzyć podstawowy układ do przeglądania kategorii i wierszy elementów multimedialnych za pomocą co najmniej 100% kodu. Poniższy przykład pokazuje, jak utworzyć układ zawierający BrowseSupportFragment obiekt:

<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 funkcją o danych wideo i elementach interfejsu, a także o parametrach układu, takich jak ikona i tytuł, czy nagłówki kategorii są włączone.

Więcej informacji o konfigurowaniu elementów interfejsu znajdziesz w artykule na temat elementów. Więcej informacji o ukrywaniu nagłówków znajdziesz w Ukryj lub wyłącz nagłówki.

Podklasa aplikacji, która implementuje funkcję BrowseSupportFragment konfiguruje detektory zdarzeń dla działań użytkownika w elementach interfejsu z menedżerem tła, 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 BrowseSupportFragment metody stylizacji katalogu multimediów:

  • setBadgeDrawable() umieszcza określony zasób rysowalny w prawym górnym rogu fragmentu, widoczne na rysunkach 1 i 2. Ta metoda zastępuje ciąg tytułu ciągiem zasób rysowalny, jeśli jest również wywoływany setTitle(). Zasób rysowalny musi mieć 52 dp wysoki.
  • setTitle() ustawia ciąg tytułu w prawym górnym rogu fragmentu umożliwiającego przeglądanie, chyba że Funkcja setBadgeDrawable() jest wywoływana.
  • 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 jego nagłówek kolor tła sekcji, określając jego wartość.
  • setSearchAffordanceColor() ustawia kolor ikony wyszukiwania o określonej wartości koloru. Ikona wyszukiwania jest widoczny w lewym górnym rogu fragmentu, co widać na rysunkach 1 i 2.

Dostosowywanie widoków nagłówka

Fragment przeglądania widoczny na ilustracji 1 zawiera nazwy kategorii filmów, czyli nagłówki wierszy w bazie danych wideo w widokach tekstu. Możesz też dostosować w nagłówku, aby uwzględnić dodatkowe widoki w bardziej złożonym układzie. W sekcjach poniżej dowiesz się, jak obejmują widok zdjęć 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 wraz z ikoną i etykieta tekstowa.

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 zaimplementuj abstrakcyjnych metod tworzenia, wiązania i odłączania obiektu widoku danych. Poniżej pokazuje, jak powiązać przeglądającego dwoma widokami: 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 pad kierunkowy mógł być używany do możesz je przewinąć. 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
        // ...
    }
    
  • Konfigurowanie układu z możliwością zaznaczenia:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Natomiast w implementacji BrowseSupportFragment, która wyświetla przeglądarki katalogu, użyj funkcji setHeaderPresenterSelector() , aby ustawić osobę prezentującą nagłówek wiersza, jak pokazano w poniższym 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 tutaj Przykładowa aplikacja Leanback ,

Ukryj lub wyłącz nagłówki

Czasami nie chcesz, aby nagłówki wierszy były wyświetlane, na przykład gdy jest ich za mało. wymaga przewijania listy. Zadzwoń pod numer BrowseSupportFragment.setHeadersState() podczas zdarzenia onActivityCreated() fragmentu ukrywania lub wyłączania nagłówków wierszy. setHeadersState() ustawia początkowy stan nagłówków we fragmencie przeglądania, dla danego argumentu stałe jako parametr:

  • HEADERS_ENABLED: podczas tworzenia aktywności związanej z fragmentami przeglądania, nagłówki są włączone i wyświetlane przez wartość domyślną. Nagłówki są widoczne na rysunkach 1 i 2 tej strony.
  • HEADERS_HIDDEN: po utworzeniu aktywności związanej z fragmentami przeglądania nagłówki są domyślnie włączone i ukryte. Sekcja nagłówka ekranu jest zwinięta, tak jak to widać tutaj: . ilustrację w narzędziu Udostępnianie widoku karty. użytkownik może kliknąć zwiniętą sekcję nagłówka, aby ją rozwinąć.
  • HEADERS_DISABLED: po utworzeniu aktywności związanej z fragmentami przeglądania, nagłówki są domyślnie wyłączone i nigdy nie zostały wyświetlone.

Jeśli jest skonfigurowana HEADERS_ENABLED lub HEADERS_HIDDEN, możesz zadzwonić setHeadersTransitionOnBackEnabled() umożliwia przejście z powrotem do nagłówka wiersza wybranego elementu treści w wierszu. Ta funkcja jest włączona przez . Aby samodzielnie zająć się ruchami pleców, przekaż: false do: setHeadersTransitionOnBackEnabled() i wdrożyć własną obsługę stosu.

Wyświetl listy mediów

BrowseSupportFragment klasa 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ą podłączenie do lokalnych lub online źródeł danych zawierających informacje z katalogu multimediów. Adapterzy korzystają z prezenterów do tworzenia widoków i wiążą z nimi dane w celu: przez wyświetlenie 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 elementów multimedialnych możesz przejściówkę i podłącz ją do urządzenia BrowseSupportFragment aby użytkownik mógł przeglądać te elementy na ekranie. Przykład poniżej pokazuje, jak utworzyć adapter do wyświetlania kategorii i elementów w tych kategoriach za pomocą klasy StringPresenter widocznej w poprzedni przykładowy kod:

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 online z bazy danych lub usługi internetowej. Oto przykład aplikacji do przeglądania, która korzysta z danych pobranych z internetu, zapoznaj się z Przykładowa aplikacja Leanback ,

Zaktualizuj tło

Aby zwiększyć atrakcyjność aplikacji do przeglądania multimediów na telewizorze, możesz zaktualizować tło gdy użytkownicy przeglądają treści. Dzięki tej metodzie możesz zwiększyć liczbę interakcji z aplikacją kinowy i przyjemny.

Zestaw narzędzi interfejsu Leanback zapewnia BackgroundManager klasy umożliwiającej zmianę tła aktywności w aplikacjach na telewizorze. Ten przykład pokazuje, jak Utwórz prostą metodę aktualizowania tła 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 podczas przeglądania treści przez użytkownika za pomocą list mediów. Aby to zrobić, możesz skonfigurować detektor wyboru, który automatycznie zaktualizuje tło na podstawie bieżącego wyboru użytkownika. Ten przykład pokazuje, aby skonfigurować zajęcia OnItemViewSelectedListener, przechwytuj zdarzenia wyboru i aktualizuj 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 pozwala ilustrację. Tworząc tę funkcję we własnej aplikacji, uruchom polecenie w ramach aktualizacji w tle, aby zwiększyć wydajność. Ponadto, jeśli zaplanować zaktualizowanie tła w odpowiedzi na prośby użytkowników przewijających elementy, dodać czas na opóźnienie aktualizacji obrazu tła, dopóki użytkownik nie zdecyduje się na zakup. Ta technika pozwala uniknąć zbyt częstych aktualizacji obrazu tła.