Tạo trình duyệt danh mục

Tạo dựng ứng dụng hiệu quả hơn với Compose
Tạo giao diện người dùng đẹp mắt với lượng mã tối thiểu bằng Jetpack Compose cho hệ điều hành Android TV.

Ứng dụng đa phương tiện chạy trên TV cần cho phép người dùng duyệt qua nội dung mà ứng dụng đó cung cấp, tạo chọn và bắt đầu phát nội dung. Trải nghiệm duyệt xem nội dung phải đơn giản và trực quan, cũng như hình ảnh bắt mắt và hấp dẫn.

Hướng dẫn này thảo luận cách sử dụng các lớp do thư viện androidx.leanback cung cấp để triển khai giao diện người dùng để duyệt qua nhạc hoặc video từ danh mục nội dung đa phương tiện của ứng dụng.

Lưu ý: Ví dụ triển khai hiển thị ở đây sử dụng BrowseSupportFragment thay vì BrowseFragment không dùng nữa . BrowseSupportFragment mở rộng AndroidX Lớp Fragment, giúp đảm bảo hành vi nhất quán trên các thiết bị và phiên bản Android.

Màn hình chính của ứng dụng

Hình 1. Mảnh duyệt qua của ứng dụng mẫu Leanback hiển thị dữ liệu danh mục video.

Tạo bố cục duyệt qua nội dung nghe nhìn

BrowseSupportFragment trong bộ công cụ Leanback UI cho phép bạn tạo bố cục chính để duyệt qua các danh mục và hàng mục nội dung đa phương tiện bằng số lượng mã tối thiểu. Ví dụ sau đây cho thấy cách tạo bố cục chứa Đối tượng 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>

Hoạt động chính của ứng dụng thiết lập thành phần hiển thị này như trong ví dụ sau:

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

Các phương thức BrowseSupportFragment sẽ điền sẵn vào khung hiển thị bằng dữ liệu video và phần tử trên giao diện người dùng, đồng thời đặt các tham số bố cục như biểu tượng và tiêu đề và liệu tiêu đề danh mục có được bật hay không.

Để biết thêm thông tin về cách thiết lập các phần tử trên giao diện người dùng, hãy xem bài viết Thiết lập giao diện người dùng phần tử. Để biết thêm thông tin về cách ẩn tiêu đề, hãy xem Ẩn hoặc vô hiệu hoá tiêu đề.

Lớp con của ứng dụng triển khai BrowseSupportFragment cũng thiết lập trình nghe sự kiện cho hành động của người dùng trên các phần tử trên giao diện người dùng và chuẩn bị trình quản lý nền, như trong ví dụ sau:

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

Thiết lập thành phần trên giao diện người dùng

Trong mẫu trước, phương thức riêng tư setupUIElements() gọi nhiều BrowseSupportFragment để tạo kiểu cho trình duyệt danh mục nội dung đa phương tiện:

  • setBadgeDrawable() đặt tài nguyên có thể vẽ đã chỉ định ở góc trên bên phải của mảnh duyệt qua, như được thể hiện trong hình 1 và 2. Phương thức này thay thế chuỗi tiêu đề bằng tài nguyên có thể vẽ nếu setTitle() cũng được gọi. Tài nguyên có thể vẽ phải có kích thước là 52 dp cao.
  • setTitle() đặt chuỗi tiêu đề ở góc trên bên phải của mảnh duyệt qua, trừ khi setBadgeDrawable() sẽ được gọi.
  • setHeadersState()setHeadersTransitionOnBackEnabled() ẩn hoặc vô hiệu hoá các tiêu đề. Xem phần Ẩn hoặc vô hiệu hoá tiêu đề để biết thêm thông tin.
  • setBrandColor() đặt màu nền cho phần tử giao diện người dùng trong phân đoạn duyệt qua, cụ thể là tiêu đề màu nền của phần, với giá trị màu được chỉ định.
  • setSearchAffordanceColor() đặt màu của biểu tượng tìm kiếm bằng giá trị màu được chỉ định. Biểu tượng tìm kiếm xuất hiện ở góc trên bên trái của mảnh duyệt qua, như được thể hiện trong hình 1 và 2.

Tuỳ chỉnh khung hiển thị tiêu đề

Mảnh duyệt qua trong hình 1 hiển thị tên danh mục video, là các tiêu đề hàng trong cơ sở dữ liệu video, ở chế độ xem văn bản. Bạn cũng có thể tuỳ chỉnh để bao gồm các chế độ xem bổ sung trong một bố cục phức tạp hơn. Các phần sau đây trình bày cách bao gồm chế độ xem hình ảnh hiển thị biểu tượng bên cạnh tên danh mục, như được minh hoạ trong hình 2.

Màn hình chính của ứng dụng

Hình 2. Các tiêu đề hàng trong mảnh duyệt qua có cả biểu tượng và một nhãn văn bản.

Bố cục cho tiêu đề hàng được xác định như sau:

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

Sử dụng Presenter và triển khai các phương thức trừu tượng để tạo, liên kết và huỷ liên kết phần tử giữ khung hiển thị. Nội dung sau đây cho thấy cách liên kết trình xem chủ sở hữu với hai chế độ xem, một 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
    }
}

Tiêu đề của bạn phải có thể làm tâm điểm thì mới có thể dùng D-pad để cuộn qua chúng. Có hai cách để quản lý:

  • Đặt chế độ xem của bạn thành có thể làm tâm điểm trong 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
        // ...
    }
    
  • Đặt bố cục có thể làm tâm điểm:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Cuối cùng, trong quá trình triển khai BrowseSupportFragment, hiển thị khi duyệt xem danh mục, hãy sử dụng setHeaderPresenterSelector() để thiết lập người trình bày cho tiêu đề hàng, như trong ví dụ sau.

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

Để biết ví dụ đầy đủ, hãy xem Ứng dụng mẫu Leanback của Google.

Ẩn hoặc tắt tiêu đề

Đôi khi, bạn không muốn tiêu đề hàng xuất hiện, chẳng hạn như khi không có đủ các danh mục cần một danh sách có thể cuộn được. Gọi BrowseSupportFragment.setHeadersState() trong quá trình onActivityCreated() của mảnh để ẩn hoặc vô hiệu hoá các tiêu đề hàng. setHeadersState() phương thức đặt trạng thái ban đầu của các tiêu đề trong mảnh duyệt qua, với một trong các giá trị sau hằng số dưới dạng tham số:

  • HEADERS_ENABLED: khi hoạt động mảnh duyệt qua được tạo, tiêu đề sẽ được bật và hiển thị bởi mặc định. Các tiêu đề xuất hiện như được minh hoạ trong hình 1 và 2 trên trang này.
  • HEADERS_HIDDEN: khi hoạt động mảnh duyệt qua được tạo, tiêu đề sẽ được bật và ẩn theo mặc định. Phần tiêu đề của màn hình được thu gọn, như minh hoạ trong một hình trong mục Cung cấp chế độ xem thẻ. Chiến lược phát hành đĩa đơn người dùng có thể chọn phần tiêu đề đã thu gọn để mở rộng tiêu đề đó.
  • HEADERS_DISABLED: khi hoạt động mảnh duyệt qua được tạo, tiêu đề sẽ bị tắt theo mặc định và không bao giờ được hiển thị.

Nếu đã đặt HEADERS_ENABLED hoặc HEADERS_HIDDEN, bạn có thể gọi setHeadersTransitionOnBackEnabled() để hỗ trợ di chuyển trở lại tiêu đề hàng từ mục nội dung đã chọn trong hàng. Tính năng này được bật bởi mặc định nếu bạn không gọi phương thức này. Cách tự xử lý chuyển động quay lại: truyền false đến setHeadersTransitionOnBackEnabled() và triển khai cách xử lý ngăn xếp lui của riêng bạn.

Hiển thị danh sách nội dung nghe nhìn

BrowseSupportFragment giúp bạn xác định và hiển thị danh mục nội dung đa phương tiện có thể xem và các mục nội dung đa phương tiện danh mục nội dung nghe nhìn sử dụng bộ chuyển đổi và người trình bày. Bộ chuyển đổi giúp bạn kết nối đến các nguồn dữ liệu địa phương hoặc trực tuyến có chứa thông tin danh mục nội dung nghe nhìn của bạn. Bộ chuyển đổi sử dụng trình trình bày để tạo các chế độ xem và liên kết dữ liệu với các chế độ xem đó để hiển thị một mục trên màn hình.

Mã ví dụ sau đây cho thấy cách triển khai Presenter để hiển thị dữ liệu chuỗi:

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

Sau khi xây dựng lớp trình bày cho các mục nội dung đa phương tiện, bạn có thể một bộ chuyển đổi rồi đính kèm vào BrowseSupportFragment để hiển thị các mục đó trên màn hình nhằm phục vụ người dùng duyệt web. Ví dụ sau đây mã trình bày cách tạo bộ chuyển đổi để hiển thị các danh mục và mục trong các danh mục đó bằng cách sử dụng lớp StringPresenter hiển thị trong ví dụ về mã trước:

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

Ví dụ này cho thấy cách triển khai tĩnh của các bộ chuyển đổi. Một ứng dụng duyệt nội dung nghe nhìn thông thường sử dụng dữ liệu từ cơ sở dữ liệu trực tuyến hoặc dịch vụ web. Ví dụ về một ứng dụng duyệt web sử dụng dữ liệu được truy xuất từ web, hãy xem Ứng dụng mẫu Leanback của Google.

Cập nhật nền

Để thêm sự quan tâm vào hình ảnh cho một ứng dụng duyệt nội dung nghe nhìn trên TV, bạn có thể cập nhật chế độ nền khi người dùng duyệt qua nội dung. Kỹ thuật này có thể giúp người dùng tương tác với ứng dụng của bạn nhiều hơn hiệu ứng điện ảnh và thú vị.

Bộ công cụ Leanback UI cung cấp BackgroundManager để thay đổi nền của hoạt động trong ứng dụng TV. Ví dụ sau đây trình bày cách tạo một phương thức đơn giản để cập nhật nền trong hoạt động ứng dụng trên TV:

Kotlin

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

Java

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

Nhiều ứng dụng duyệt nội dung nghe nhìn tự động cập nhật ở chế độ nền khi người dùng di chuyển thông qua danh sách nội dung nghe nhìn. Để thực hiện việc này, bạn có thể thiết lập trình nghe lựa chọn để tự động cập nhật nền dựa trên lựa chọn hiện tại của người dùng. Ví dụ sau đây minh hoạ cách để thiết lập một lớp OnItemViewSelectedListener nắm bắt các sự kiện lựa chọn và cập nhật nền:

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

Lưu ý: Cách triển khai trước là một ví dụ đơn giản nhằm mục đích hình minh hoạ. Khi tạo hàm này trong ứng dụng của riêng bạn, hãy chạy hàm hành động cập nhật ở chế độ nền trong một chuỗi riêng biệt để đạt được hiệu suất tốt hơn. Ngoài ra, nếu bạn hãy lên kế hoạch cập nhật nền để phản hồi khi người dùng cuộn qua các mục, hãy thêm một khoảng thời gian trì hoãn cập nhật hình nền cho đến khi người dùng giải quyết xong một mục. Kỹ thuật này tránh được cập nhật hình nền quá nhiều lần.