แอปสื่อที่ทำงานบนทีวีต้องอนุญาตให้ผู้ใช้เรียกดูเนื้อหาที่นำเสนอ เลือกเนื้อหา และเริ่มเล่นเนื้อหา ประสบการณ์การเรียกดูเนื้อหา ต้องเรียบง่ายและใช้งานง่าย รวมถึงดึงดูดสายตาและน่าสนใจ
คู่มือนี้จะอธิบายวิธีใช้คลาสที่จัดเตรียมไว้โดยไลบรารี androidx.leanback ที่เลิกใช้งานแล้วเพื่อใช้ อินเทอร์เฟซผู้ใช้สำหรับการเรียกดูเพลงหรือวิดีโอจากแคตตาล็อกสื่อของแอป
หมายเหตุ: ตัวอย่างการใช้งานที่แสดงที่นี่ใช้
BrowseSupportFragment
แทนคลาส BrowseFragment
ที่เลิกใช้งานแล้ว BrowseSupportFragment
ขยายคลาส AndroidX
Fragment
ช่วยให้มั่นใจได้ว่าการทำงานจะสอดคล้องกันในอุปกรณ์และ Android เวอร์ชันต่างๆ

รูปที่ 1 Browse Fragment ของแอปตัวอย่าง Leanback จะแสดงข้อมูลแคตตาล็อกวิดีโอ
สร้างเลย์เอาต์การเรียกดูสื่อ
คลาส BrowseSupportFragment
ในชุดเครื่องมือ UI ของ Leanback
ช่วยให้คุณสร้างเลย์เอาต์หลักสำหรับการเรียกดูหมวดหมู่และแถวของรายการสื่อได้ด้วย
โค้ดที่น้อยที่สุด ตัวอย่างต่อไปนี้แสดงวิธีสร้างเลย์เอาต์ที่มีออบเจ็กต์ 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>
กิจกรรมหลักของแอปพลิเคชันจะตั้งค่ามุมมองนี้ ดังที่แสดงในตัวอย่างต่อไปนี้
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); } ...
เมธอด BrowseSupportFragment
จะป้อนข้อมูลวิดีโอและองค์ประกอบ UI ลงในมุมมอง รวมถึงตั้งค่าพารามิเตอร์เลย์เอาต์ เช่น ไอคอนและชื่อ และ
เปิดใช้ส่วนหัวของหมวดหมู่หรือไม่
คลาสย่อยของแอปพลิเคชันที่ใช้BrowseSupportFragment
เมธอดจะตั้งค่าเครื่องมือตรวจหาเหตุการณ์สำหรับการดำเนินการของผู้ใช้ในองค์ประกอบ UI และเตรียม
เครื่องมือจัดการพื้นหลังด้วย ดังที่แสดงในตัวอย่างต่อไปนี้
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()); } ...
ตั้งค่าองค์ประกอบ UI
ในตัวอย่างก่อนหน้า เมธอดส่วนตัว setupUIElements()
เรียกใช้เมธอดหลายรายการ
BrowseSupportFragment
เพื่อจัดรูปแบบเบราว์เซอร์แคตตาล็อกสื่อ
setBadgeDrawable()
จะวางทรัพยากร Drawable ที่ระบุไว้ที่มุมขวาบนของ Fragment การเรียกดู ดังที่ แสดงในรูปที่ 1 และ 2 วิธีนี้จะแทนที่สตริงชื่อด้วย ทรัพยากร Drawable หากมีการเรียกใช้setTitle()
ด้วย ทรัพยากร Drawable ต้องมีความสูง 52 dpsetTitle()
ตั้งค่าสตริงชื่อที่มุมขวาบนของ BrowseFragment เว้นแต่จะมีการเรียกใช้setBadgeDrawable()
setHeadersState()
และsetHeadersTransitionOnBackEnabled()
ซ่อนหรือปิดใช้ส่วนหัว ดูข้อมูลเพิ่มเติมได้ที่ส่วนซ่อนหรือปิดใช้ส่วนหัวsetBrandColor()
ตั้งค่าสีพื้นหลังสำหรับองค์ประกอบ UI ใน Fragment การเรียกดู โดยเฉพาะสีพื้นหลังของส่วน ส่วนหัว ด้วยค่าสีที่ระบุsetSearchAffordanceColor()
ตั้งค่าสีของไอคอนค้นหาด้วยค่าสีที่ระบุ ไอคอนค้นหา จะปรากฏที่มุมซ้ายบนของ Fragment การเรียกดู ดังที่แสดงในรูปที่ 1 และ 2
ปรับแต่งมุมมองส่วนหัว
ส่วนการเรียกดูที่แสดงในรูปที่ 1 จะแสดงชื่อหมวดหมู่วิดีโอ ซึ่งเป็นส่วนหัวของแถวในฐานข้อมูลวิดีโอในมุมมองข้อความ นอกจากนี้ คุณยังปรับแต่ง ส่วนหัวเพื่อรวมมุมมองเพิ่มเติมในเลย์เอาต์ที่ซับซ้อนมากขึ้นได้ด้วย ส่วนต่อไปนี้แสดงวิธี รวมมุมมองรูปภาพที่แสดงไอคอนข้างชื่อหมวดหมู่ ดังที่แสดงในรูปที่ 2

รูปที่ 2 ส่วนหัวของแถวใน Fragment การเรียกดูที่มีทั้งไอคอน และป้ายกำกับข้อความ
เลย์เอาต์สำหรับส่วนหัวของแถวกำหนดไว้ดังนี้
<?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>
ใช้ Presenter
และใช้
เมธอดแบบนามธรรมเพื่อสร้าง ผูก และยกเลิกการผูก View Holder ตัวอย่างต่อไปนี้แสดงวิธีเชื่อมโยง Viewholder กับ 2 มุมมอง ได้แก่ ImageView
และ 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 } }
ส่วนหัวต้องโฟกัสได้เพื่อให้ใช้ D-pad เลื่อนผ่านส่วนหัวได้ คุณจัดการได้ 2 วิธีดังนี้
- ตั้งค่ามุมมองให้โฟกัสได้ใน
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 // ... }
- ตั้งค่าเลย์เอาต์ให้โฟกัสได้
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
สุดท้าย ในBrowseSupportFragment
การติดตั้งใช้งานที่แสดงเบราว์เซอร์แคตตาล็อก
ให้ใช้เมธอด setHeaderPresenterSelector()
เพื่อตั้งค่าผู้แสดงสำหรับส่วนหัวของแถว ดังที่แสดงในตัวอย่างต่อไปนี้
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(); } });
ดูตัวอย่างที่สมบูรณ์ได้ใน แอปตัวอย่าง Leanback
ซ่อนหรือปิดใช้ส่วนหัว
บางครั้งคุณอาจไม่ต้องการให้ส่วนหัวของแถวปรากฏ เช่น เมื่อมีหมวดหมู่ไม่เพียงพอที่จะต้องใช้รายการที่เลื่อนได้ เรียกใช้เมธอด BrowseSupportFragment.setHeadersState()
ในระหว่างเมธอด onActivityCreated()
ของ Fragment เพื่อซ่อนหรือปิดใช้ส่วนหัวของแถว เมธอด setHeadersState()
จะตั้งค่าสถานะเริ่มต้นของส่วนหัวใน Fragment การเรียกดู โดยมีค่าคงที่ต่อไปนี้อย่างใดอย่างหนึ่งเป็นพารามิเตอร์
HEADERS_ENABLED
: เมื่อสร้างกิจกรรมของ Browse Fragment ระบบจะเปิดใช้และแสดงส่วนหัวโดย ค่าเริ่มต้น ส่วนหัวจะปรากฏตามที่แสดงในรูปที่ 1 และ 2 ในหน้านี้HEADERS_HIDDEN
: เมื่อสร้างกิจกรรมของ BrowseFragment ระบบจะเปิดใช้และซ่อนส่วนหัวโดยค่าเริ่มต้น ส่วนหัวของหน้าจอจะยุบลง ดังที่แสดงใน รูปภาพในระบุมุมมองการ์ด ผู้ใช้สามารถเลือกส่วนหัวที่ยุบเพื่อขยายได้HEADERS_DISABLED
: เมื่อสร้างกิจกรรมของ Fragment การเรียกดู ระบบจะปิดใช้ส่วนหัวโดยค่าเริ่มต้นและจะไม่ แสดงเลย
หากตั้งค่า HEADERS_ENABLED
หรือ HEADERS_HIDDEN
คุณจะเรียกใช้
setHeadersTransitionOnBackEnabled()
เพื่อรองรับการย้ายกลับไปที่ส่วนหัวของแถวจากรายการเนื้อหาที่เลือกในแถวได้ ซึ่งจะเปิดใช้โดยค่าเริ่มต้นหากคุณไม่เรียกใช้เมธอด หากต้องการจัดการการย้อนกลับด้วยตนเอง ให้ส่ง false
ไปยัง setHeadersTransitionOnBackEnabled()
และใช้การจัดการ Back Stack ของคุณเอง
แสดงรายการสื่อ
คลาส BrowseSupportFragment
ช่วยให้คุณ
กำหนดและแสดงหมวดหมู่เนื้อหาสื่อที่เรียกดูได้และรายการสื่อจาก
แคตตาล็อกสื่อโดยใช้อะแดปเตอร์และพรีเซนเตอร์ อแดปเตอร์ช่วยให้คุณเชื่อมต่อ
กับแหล่งข้อมูลในเครื่องหรือออนไลน์ที่มีข้อมูลแคตตาล็อกสื่อ
อแดปเตอร์ใช้ Presenter เพื่อสร้างมุมมองและเชื่อมโยงข้อมูลกับมุมมองเหล่านั้นเพื่อ
แสดงรายการบนหน้าจอ
โค้ดตัวอย่างต่อไปนี้แสดงการใช้งาน Presenter
สำหรับแสดงข้อมูลสตริง
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 } }
เมื่อสร้างคลาส Presenter สำหรับรายการสื่อแล้ว คุณจะสร้าง
อแดปเตอร์และแนบไปกับ BrowseSupportFragment
เพื่อแสดงรายการเหล่านั้นบนหน้าจอให้ผู้ใช้เรียกดูได้ ตัวอย่างโค้ดต่อไปนี้
แสดงวิธีสร้างอะแดปเตอร์เพื่อแสดงหมวดหมู่และรายการ
ในหมวดหมู่เหล่านั้นโดยใช้คลาส StringPresenter
ที่แสดงใน
ตัวอย่างโค้ดก่อนหน้า
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); }
ตัวอย่างนี้แสดงการติดตั้งใช้งานแบบคงที่ของอแดปเตอร์ แอปพลิเคชันการเรียกดูสื่อทั่วไป ใช้ข้อมูลจากฐานข้อมูลออนไลน์หรือบริการเว็บ ดูตัวอย่างแอปพลิเคชันการท่องเว็บที่ ใช้ข้อมูลที่ดึงมาจากเว็บได้ที่ แอปตัวอย่าง Leanback
อัปเดตพื้นหลัง
หากต้องการเพิ่มความน่าสนใจให้กับแอปการเรียกดูสื่อบนทีวี คุณสามารถอัปเดตรูปภาพพื้นหลัง ขณะที่ผู้ใช้เรียกดูเนื้อหา เทคนิคนี้จะช่วยให้การโต้ตอบกับแอปของคุณดู น่าสนใจและสนุกสนานยิ่งขึ้น
ชุดเครื่องมือ UI ของ Leanback มีคลาส BackgroundManager
สำหรับการเปลี่ยนพื้นหลังของกิจกรรมในแอปทีวี ตัวอย่างต่อไปนี้แสดงวิธี
สร้างวิธีการง่ายๆ ในการอัปเดตพื้นหลังภายในกิจกรรมแอปทีวี
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
แอปการเรียกดูสื่อจำนวนมากจะอัปเดตพื้นหลังโดยอัตโนมัติเมื่อผู้ใช้ไปยังส่วนต่างๆ
ในรายการสื่อ โดยคุณสามารถตั้งค่าเครื่องมือฟังการเลือกเพื่ออัปเดตพื้นหลังโดยอัตโนมัติ
ตามการเลือกปัจจุบันของผู้ใช้ ตัวอย่างต่อไปนี้แสดงวิธี
ตั้งค่าOnItemViewSelectedListener
class เพื่อ
ตรวจหาเหตุการณ์การเลือกและอัปเดตพื้นหลัง
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(); } } }; }
หมายเหตุ: การติดตั้งใช้งานก่อนหน้านี้เป็นเพียงตัวอย่างง่ายๆ เพื่อประกอบการ อธิบาย เมื่อสร้างฟังก์ชันนี้ในแอปของคุณเอง ให้เรียกใช้ การดำเนินการอัปเดตในเบื้องหลังในเธรดแยกต่างหากเพื่อประสิทธิภาพที่ดีขึ้น นอกจากนี้ หากคุณวางแผนที่จะอัปเดตพื้นหลังเพื่อตอบสนองต่อการเลื่อนดูรายการของผู้ใช้ ให้เพิ่มเวลาเพื่อชะลอการอัปเดตภาพพื้นหลังจนกว่าผู้ใช้จะเลือกรายการ เทคนิคนี้ช่วยหลีกเลี่ยง การอัปเดตรูปภาพพื้นหลังมากเกินไป