Ứ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 các nội dung mà ứng dụng đó cung cấp, lựa chọn và bắt đầu phát nội dung. Trải nghiệm duyệt nội dung phải đơn giản và trực quan, cũng như hình ảnh dễ chịu 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 một giao diện người dùng để duyệt xem nhạc hoặc video trong danh mục nội dung đa phương tiện của ứng dụng.
Lưu ý: Ví dụ về cách triển khai hiển thị ở đây sử dụng BrowseSupportFragment
thay vì lớp BrowseFragment
không dùng nữa. BrowseSupportFragment
mở rộng lớp AndroidX
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](https://developer.android.com/static/images/tv/app-browse.png?authuser=5&hl=vi)
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
Lớp BrowseSupportFragment
trong bộ công cụ Leanback UI cho phép bạn tạo một bố cục chính để duyệt qua các danh mục và hàng chứa các mục nội dung đa phương tiện mà chỉ cần có tối thiểu mã. Ví dụ sau đây cho biết 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 dữ liệu video và các phần tử trên giao diện người dùng vào khung hiển thị, đồng thời đặt các tham số bố cục như biểu tượng và tiêu đề, cũng như liệu các tiêu đề danh mục có được bật hay không.
Lớp con của ứng dụng triển khai các phương thức BrowseSupportFragment
cũng thiết lập trình nghe sự kiện cho thao tác của người dùng trên các thành phần giao diện người dùng, đồng thời 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 một số phương thức 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ư minh hoạ 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ếusetTitle()
cũng được gọi. Tài nguyên có thể vẽ phải cao 52 dp.setTitle()
đặt chuỗi tiêu đề ở góc trên bên phải của mảnh duyệt qua, trừ phisetBadgeDrawable()
được gọi.setHeadersState()
vàsetHeadersTransitionOnBackEnabled()
ẩn hoặc tắt 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 các phần tử trên giao diện người dùng trong mảnh duyệt qua, cụ thể là màu nền của phần tiêu đề, 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ư minh hoạ trong hình 1 và 2.
Tuỳ chỉnh khung hiển thị tiêu đề
Mảnh duyệt qua như 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, trong khung hiển thị văn bản. Bạn cũng có thể tuỳ chỉnh tiêu đề để đưa các thành phần hiển thị khác vào một bố cục phức tạp hơn. Các phần sau đây cho biết cách bao gồm khung hiển thị hình ảnh để hiển thị biểu tượng bên cạnh tên danh mục, như trong hình 2.
![Màn hình chính của ứng dụng](https://developer.android.com/static/images/tv/custom-head.png?authuser=5&hl=vi)
Hình 2. Các tiêu đề hàng trong mảnh duyệt qua có cả biểu tượng và 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 trình lưu giữ khung hiển thị. Ví dụ sau cho thấy cách liên kết trình xem chủ sở hữu với 2 khung hiển thị (ImageView
và 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 } }
Tiêu đề của bạn phải đặt tiêu đề được thì bạn có thể dùng D-pad để cuộn qua các tiêu đề đó. 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ị trình duyệt danh mục, hãy sử dụng phương thức setHeaderPresenterSelector()
để đặt trình trình bày cho tiêu đề hàng, như minh hoạ 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(); } });
Để xem ví dụ đầy đủ, hãy xem Ứng dụng mẫu Leanback.
Ẩ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ó đủ danh mục để yêu cầu một danh sách có thể cuộn. Gọi phương thức BrowseSupportFragment.setHeadersState()
trong phương thức onActivityCreated()
của phân đoạn để ẩn hoặc tắt các tiêu đề hàng. Phương thức setHeadersState()
đặt trạng thái ban đầu của các tiêu đề trong mảnh duyệt qua, dựa trên một trong các hằng số sau đây 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ị theo 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 hình minh hoạ trong mục Cung cấp chế độ xem thẻ. Người dùng có thể chọn mục tiêu đề đã thu gọn để mở rộng.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ờ hiển thị.
Nếu đặt HEADERS_ENABLED
hoặc HEADERS_HIDDEN
, bạn có thể gọi hàm setHeadersTransitionOnBackEnabled()
để hỗ trợ việc di chuyển trở lại tiêu đề hàng từ một mục nội dung đã chọn trong hàng. Tính năng này sẽ được bật theo mặc định nếu bạn không gọi phương thức này. Để tự xử lý thao tác di chuyển quay lại, hãy truyền false
đến setHeadersTransitionOnBackEnabled()
và triển khai quy trình 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
Lớp BrowseSupportFragment
cho phép bạn xác định và hiển thị danh mục nội dung đa phương tiện có thể xem cũng như các mục nội dung đa phương tiện từ một danh mục nội dung nghe nhìn bằng cách sử dụng bộ chuyển đổi và trình trình bày. Bộ chuyển đổi cho phép bạn kết nối
với những 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 đa phương tiện của bạn.
Bộ chuyển đổi sử dụng trình trình bày để tạo các thành phần hiển thị và liên kết dữ liệu với các thành phần hiển thị đó để 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 tạo một lớp trình bày cho các mục nội dung đa phương tiện, bạn có thể tạo
bộ chuyển đổi và đính kèm vào BrowseSupportFragment
để hiển thị các mục đó trên màn hình để người dùng duyệt xem. Mã ví dụ sau đây minh hoạ cách tạo một bộ chuyển đổi để hiển thị các danh mục và mặt hàng trong những danh mục đó bằng cách sử dụng lớp StringPresenter
như trong mã ví dụ 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 đa phương tiện thông thường sử dụng dữ liệu từ dịch vụ web hoặc cơ sở dữ liệu trực tuyến. Để biết ví dụ về ứ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ập nhật nền
Để thêm hình ảnh hấp dẫn 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 hình nền khi người dùng duyệt qua nội dung. Kỹ thuật này có thể giúp quá trình tương tác với ứng dụng trở nên đậm chất điện ảnh và thú vị hơn.
Bộ công cụ giao diện người dùng Leanback cung cấp một lớp BackgroundManager
để thay đổi nền của hoạt động trong ứng dụng truyền hình. Ví dụ sau đây cho biết cách tạo một phương thức đơn giản để cập nhật nền trong hoạt động trong ứng dụng dành cho 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 qua các trang thông tin nội dung nghe nhìn. Để thực hiện việc này, bạn có thể thiết lập một 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 cho thấy 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 đây chỉ là một ví dụ đơn giản nhằm mục đích minh hoạ. Khi tạo hàm này trong ứng dụng của riêng bạn, hãy chạy thao tác cập nhật ở chế độ nền trong một luồng riêng để đạt được hiệu suất tốt hơn. Ngoài ra, nếu bạn định 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 thời gian trì hoãn việc 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 giúp tránh việc cập nhật hình nền quá nhiều lần.