Membuat browser katalog

Membangun aplikasi dengan lebih baik menggunakan Compose
Membuat UI yang menarik dengan kode minimal menggunakan Jetpack Compose untuk Android TV OS.

Aplikasi media yang berjalan di TV perlu memungkinkan pengguna menjelajahi penawaran kontennya, membuat memilih, dan mulai memutar konten. Pengalaman penjelajahan konten harus sederhana dan intuitif serta menyenangkan dan menarik secara visual.

Panduan ini membahas cara menggunakan class yang disediakan oleh library androidx.Lean untuk mengimplementasikan antarmuka pengguna bagi penjelajahan musik atau video dari katalog media aplikasi Anda.

Catatan: Contoh penerapan yang ditunjukkan di sini menggunakan BrowseSupportFragment bukan BrowseFragment yang tidak digunakan lagi . BrowseSupportFragment memperluas AndroidX Class Fragment, membantu memastikan perilaku yang konsisten di seluruh perangkat dan versi Android.

Layar utama aplikasi

Gambar 1. Fragmen jelajah aplikasi contoh Leanback menampilkan data katalog video.

Membuat tata letak penelusuran media

BrowseSupportFragment di toolkit UI Leanback memungkinkan Anda membuat tata letak utama untuk menelusuri kategori dan baris item media dengan minimum kode. Contoh berikut menunjukkan cara membuat tata letak yang berisi Objek 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>

Aktivitas utama aplikasi mengatur tampilan ini, seperti yang ditampilkan dalam contoh berikut:

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

Metode BrowseSupportFragment mengisi tampilan dengan data video dan elemen UI serta menyetel parameter tata letak seperti ikon dan judul serta apakah header kategori diaktifkan atau tidak.

Untuk mengetahui informasi selengkapnya tentang cara menyiapkan elemen UI, lihat artikel Menyetel UI elemen. Untuk informasi selengkapnya tentang menyembunyikan header, lihat Bagian Sembunyikan atau nonaktifkan header.

Subclass aplikasi yang mengimplementasikan BrowseSupportFragment juga menyiapkan pemroses peristiwa untuk tindakan pengguna pada elemen UI dan mempersiapkan pengelola latar belakang, seperti yang ditunjukkan dalam contoh berikut:

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

Menyetel elemen UI

Pada contoh sebelumnya, metode pribadi setupUIElements() memanggil beberapa BrowseSupportFragment metode untuk menentukan gaya browser katalog media:

  • setBadgeDrawable() menempatkan sumber daya dapat digambar yang ditetapkan di sudut kanan atas fragmen jelajah, yang ditunjukkan pada gambar 1 dan 2. Metode ini menggantikan string judul dengan resource drawable, jika setTitle() juga dipanggil. Resource drawable harus berukuran 52 dp tinggi.
  • setTitle() mengatur string judul di sudut kanan atas fragmen jelajah, kecuali setBadgeDrawable() dipanggil.
  • setHeadersState() dan setHeadersTransitionOnBackEnabled() menyembunyikan atau menonaktifkan header. Lihat bagian Menyembunyikan atau menonaktifkan header untuk informasi selengkapnya.
  • setBrandColor() mengatur warna latar belakang untuk elemen UI dalam fragmen jelajah, khususnya header warna latar belakang bagian, dengan nilai warna yang ditentukan.
  • setSearchAffordanceColor() menyetel warna ikon penelusuran dengan nilai warna yang ditentukan. Ikon penelusuran ditampilkan di sudut kiri atas fragmen jelajah, seperti yang ditunjukkan pada gambar 1 dan 2.

Menyesuaikan tampilan header

Fragmen jelajah yang ditunjukkan dalam gambar 1 menampilkan nama kategori video, yang merupakan header baris dalam {i>database<i} video, dalam tampilan teks. Anda juga dapat menyesuaikan untuk menyertakan tampilan tambahan dalam tata letak yang lebih kompleks. Bagian berikut menunjukkan cara menyertakan tampilan gambar yang menampilkan ikon di samping nama kategori, seperti yang ditunjukkan pada gambar 2.

Layar utama aplikasi

Gambar 2. Header baris dalam fragmen jelajah dengan ikon dan label teks.

Tata letak untuk header baris ditentukan sebagai berikut:

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

Gunakan Presenter dan terapkan metode metode abstrak untuk membuat, mengikat, dan melepas ikatan holder tampilan. Hal berikut yang menunjukkan cara mengikat view holder dengan dua tampilan, ImageView dan 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
    }
}

{i>Header<i} Anda harus dapat difokuskan sehingga D-pad dapat digunakan untuk scroll pada bagian tersebut. Ada dua cara untuk mengelola ini:

  • Setel tampilan agar dapat difokuskan di 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
        // ...
    }
    
  • Menyetel tata letak agar dapat difokuskan:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Terakhir, dalam implementasi BrowseSupportFragment yang menampilkan browser katalog, gunakan library setHeaderPresenterSelector() untuk mengatur presenter bagi tajuk baris, seperti yang ditunjukkan pada contoh berikut.

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

Untuk contoh lengkapnya, lihat Aplikasi contoh Leanback kami.

Menyembunyikan atau menonaktifkan header

Terkadang Anda tidak ingin {i>header <i}baris muncul, misalnya saat tidak ada cukup kategori untuk memerlukan daftar yang dapat di-scroll. Panggil BrowseSupportFragment.setHeadersState() selama onActivityCreated() fragmen metode untuk menyembunyikan atau menonaktifkan {i>header <i}baris. setHeadersState() menyetel status awal header dalam fragmen jelajah, dengan salah satu hal berikut sebagai parameter:

  • HEADERS_ENABLED: ketika aktivitas fragmen jelajah dibuat, header diaktifkan dan ditampilkan oleh secara default. Header akan muncul seperti yang ditunjukkan pada gambar 1 dan 2 di halaman ini.
  • HEADERS_HIDDEN: ketika aktivitas fragmen jelajah dibuat, header akan diaktifkan dan disembunyikan secara default. Bagian header layar akan diciutkan, seperti yang ditunjukkan di gambar di Menyediakan tampilan kartu. Tujuan pengguna dapat memilih bagian header yang diciutkan untuk meluaskannya.
  • HEADERS_DISABLED: ketika aktivitas fragmen jelajah dibuat, header dinonaktifkan secara {i>default<i} dan tidak pernah ditampilkan.

Jika HEADERS_ENABLED atau HEADERS_HIDDEN disetel, Anda dapat memanggil setHeadersTransitionOnBackEnabled() untuk mendukung pemindahan kembali ke header baris dari item konten yang dipilih dalam baris. Hal ini diaktifkan oleh secara default jika metode tersebut tidak dipanggil. Untuk menangani gerakan mundur sendiri, teruskan false ke setHeadersTransitionOnBackEnabled() dan mengimplementasikan penanganan data sebelumnya.

Menampilkan daftar media

BrowseSupportFragment memungkinkan Anda menentukan dan menampilkan kategori konten media dan item media yang dapat dijelajahi dari katalog media menggunakan adaptor dan {i>presenter<i}. Adaptor memungkinkan Anda menghubungkan ke sumber data lokal atau online yang berisi informasi katalog media Anda. Adaptor menggunakan {i>presenter<i} untuk membuat tampilan dan mengikat data ke tampilan tersebut untuk menampilkan item di layar.

Kode contoh berikut menunjukkan implementasi Presenter untuk menampilkan data 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
    }
}

Setelah membuat class presenter untuk item media, Anda bisa membangun adaptor dan memasangnya ke BrowseSupportFragment untuk menampilkan item tersebut di layar untuk dijelajahi pengguna. Contoh berikut kode ini menunjukkan cara membuat adaptor untuk menampilkan kategori dan item di kategori tersebut menggunakan class StringPresenter yang ditampilkan di contoh kode sebelumnya:

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

Contoh berikut ini menampilkan implementasi statik adaptor. Aplikasi penjelajahan media biasa menggunakan data dari {i>database<i} atau layanan web {i>online<i}. Untuk contoh aplikasi penjelajahan yang menggunakan data yang diambil dari web, lihat Aplikasi contoh Leanback kami.

Memperbarui latar belakang

Untuk menambahkan daya tarik visual ke aplikasi penjelajahan media di TV, Anda bisa memperbarui latar belakang gambar saat pengguna menjelajahi konten. Teknik ini dapat lebih meningkatkan interaksi dengan aplikasi Anda sinematik dan menyenangkan.

Toolkit UI Leanback menyediakan BackgroundManager untuk mengubah latar belakang aktivitas aplikasi TV Anda. Contoh berikut menunjukkan cara buat metode sederhana untuk memperbarui latar belakang dalam aktivitas aplikasi TV Anda:

Kotlin

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

Java

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

Banyak aplikasi penjelajahan media memperbarui latar belakang secara otomatis saat pengguna menavigasi melalui listingan media. Untuk melakukannya, Anda dapat menyiapkan pemroses pilihan agar secara otomatis memperbarui latar belakang berdasarkan pilihan pengguna saat ini. Contoh berikut menunjukkan bagaimana untuk menyiapkan class OnItemViewSelectedListener agar tangkap peristiwa pemilihan dan perbarui latar belakang:

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

Catatan: Implementasi sebelumnya adalah contoh sederhana untuk tujuan ilustrasi. Saat membuat fungsi ini di aplikasi Anda sendiri, jalankan tindakan update latar belakang di thread terpisah untuk performa yang lebih baik. Selain itu, jika Anda berencana memperbarui latar belakang sebagai respons terhadap pengguna yang menggulir item, menambahkan waktu untuk menunda pembaruan gambar latar hingga pengguna puas dengan suatu item. Teknik ini menghindari pembaruan gambar latar yang berlebihan.