The Android Developer Challenge is back! Submit your idea before December 2.

Membuat browser katalog

Aplikasi media yang berjalan di TV harus memungkinkan pengguna untuk menjelajahi penawaran kontennya, membuat pilihan, dan mulai memutar konten. Pengalaman penjelajahan konten untuk aplikasi jenis ini harus mudah dan intuitif, serta menyenangkan dan menarik secara visual.

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

Layar utama aplikasi

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

Membuat tata letak jelajah media

Class BrowseFragment dalam library leanback memungkinkan Anda membuat tata letak utama untuk menjelajahi kategori dan baris item media dengan sedikit kode. Contoh berikut menunjukkan cara membuat tata letak yang berisi objek BrowseFragment:

    <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 BrowseFragment mengisi tampilan dengan data video dan elemen UI serta mengatur parameter tata letak seperti ikon, judul, dan apakah header kategori diaktifkan.

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

Kotlin

    class MainFragment : BrowseFragment(),
            LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
        ...
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)

            loadVideoData()

            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 headers background color
            brandColor = resources.getColor(R.color.fastlane_background)
            // set search icon color
            searchAffordanceColor = resources.getColor(R.color.search_opaque)
        }

        private fun loadVideoData() {
            VideoProvider.setContext(activity)
            videosUrl = resources.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 BrowseFragment implements
            LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
    }
    ...

        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            loadVideoData();

            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 headers background color
            setBrandColor(getResources().getColor(R.color.fastlane_background));
            // set search icon color
            setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
        }

        private void loadVideoData() {
            VideoProvider.setContext(getActivity());
            videosUrl = getActivity().getResources().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

Dalam contoh di atas, metode privat setupUIElements() memanggil beberapa metode BrowseFragment untuk mengatur gaya browser katalog media:

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

Menyesuaikan tampilan header

Fragmen jelajah yang ditunjukkan dalam gambar 1 mencantumkan nama kategori video (header baris) di panel kiri. Tampilan teks menampilkan nama kategori ini dari database video. Anda bisa menyesuaikan header untuk menyertakan tampilan tambahan dalam tata letak yang lebih kompleks. Bagian berikut menampilkan cara menyertakan tampilan gambar yang menampilkan ikon di samping nama kategori, seperti dalam 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 implementasikan metode abstrak untuk membuat, mengikat, dan melepaskan view holder. Contoh berikut menunjukkan cara mengikat viewholder 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
        }
    }
    

Header harus bisa difokuskan agar D-pad bisa digunakan untuk men-scroll-nya. Ada dua alternatif untuk hal ini:

  • Menyetel 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 BrowseFragment yang menampilkan browser katalog, gunakan metode setHeaderPresenterSelector() untuk menyetel presenter bagi header baris, seperti yang ditunjukkan dalam 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 Android Leanback di Repositori GitHub Android TV .

Menyembunyikan atau menonaktifkan header

Terkadang, Anda mungkin tidak ingin menampilkan header baris, seperti ketika kategori yang ada tidak mencukupi untuk membuat daftar yang dapat di-scroll. Panggil metode BrowseFragment.setHeadersState() selama metode onActivityCreated() fragmen untuk menyembunyikan atau menonaktifkan header baris. Metode setHeadersState() menyetel status awal header dalam fragmen jelajah yang diberi salah satu konstanta berikut sebagai parameter:

  • HEADERS_ENABLED - Ketika aktivitas fragmen jelajah dibuat, header akan diaktifkan dan ditampilkan secara default. Header muncul seperti yang ditunjukkan dalam gambar 1 dan 2 pada halaman ini.
  • HEADERS_HIDDEN - Saat aktivitas fragmen jelajah dibuat, header akan diaktifkan dan disembunyikan secara default. Bagian header layar akan diciutkan, seperti yang ditunjukkan dalam gambar 1 pada Menyediakan Tampilan Kartu. Pengguna bisa memilih bagian header yang diciutkan untuk membukanya
  • HEADERS_DISABLED - Saat aktivitas fragmen jelajah dibuat, header akan dinonaktifkan secara default 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. Metode ini diaktifkan secara default (jika metode tidak dipanggil), tetapi jika ingin menangani gerakan kembalinya sendiri, Anda harus meneruskan nilai false ke setHeadersTransitionOnBackEnabled() dan mengimplementasikan penanganan back stack sendiri.

Menampilkan daftar media

Class BrowseFragment memungkinkan Anda menentukan dan menampilkan kategori konten media serta item media yang dapat dijelajahi dari katalog media menggunakan adaptor dan presenter. Adaptor memungkinkan Anda menghubungkan ke sumber data lokal atau online yang berisi informasi katalog media Anda. Adaptor menggunakan presenter untuk membuat tampilan dan mengikat data ke tampilan tersebut untuk menampilkan item di layar.

Kode contoh berikut ini 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.getContext().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 dapat membuat adaptor dan melampirkannya ke BrowseFragment untuk menampilkan item tersebut di layar agar dijelajahi pengguna. Contoh kode berikut mendemonstrasikan cara membuat adaptor untuk menampilkan kategori dan item dalam kategori tersebut menggunakan class StringPresenter seperti dalam 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))
            }
        }
        browseFragment.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));
        }

        browseFragment.setAdapter(rowsAdapter);
    }
    

Contoh berikut ini menampilkan implementasi statik adaptor. Aplikasi penjelajahan media biasanya menggunakan data dari database online atau layanan web. Untuk contoh aplikasi penjelajahan yang menggunakan data yang diambil dari web, lihat aplikasi contoh Android Leanback di Repositori GitHub Android TV.

Memperbarui latar belakang

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

Library dukungan Leanback menyediakan class BackgroundManager untuk mengubah latar belakang aktivitas aplikasi TV Anda. Contoh berikut ini menunjukkan cara membuat 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);
    }
    

Saat ini, ada banyak aplikasi penjelajahan media yang memperbarui latar belakangnya secara otomatis ketika pengguna menelusuri daftar media. Untuk melakukannya, Anda bisa membuat pemroses pilihan agar memperbarui latar belakang secara otomatis berdasarkan pilihan pengguna. Contoh berikut menunjukkan cara menyiapkan class OnItemViewSelectedListener untuk merekam peristiwa pilihan dan memperbarui 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 di atas merupakan contoh sederhana yang ditampilkan untuk keperluan ilustrasi. Ketika membuat fungsi ini dalam aplikasi sendiri, Anda harus mempertimbangkan untuk menjalankan tindakan pembaruan latar belakang di thread terpisah agar kinerja menjadi lebih baik. Selain itu, jika Anda berencana memperbarui latar belakang untuk merespons scroll pengguna yang menelusuri item, pertimbangkan untuk menambahkan waktu tunda bagi pembaruan gambar latar belakang hingga pengguna memilih sebuah item. Teknik ini akan menghindari update gambar latar belakang yang berlebihan.