แอปสื่อที่ทำงานบนทีวีจำเป็นต้องให้ผู้ใช้เรียกดูเนื้อหาที่มีให้บริการ เลือกแล้วเริ่มเล่นเนื้อหา ประสบการณ์การเรียกดูเนื้อหา ต้องเรียบง่ายและเข้าใจง่าย รวมถึงสวยงามและดึงดูดสายตา
คำแนะนำนี้จะกล่าวถึงวิธีใช้ชั้นเรียนที่ไลบรารี androidx.leanback เพื่อใช้อินเทอร์เฟซผู้ใช้สำหรับการเรียกดูเพลงหรือวิดีโอจากแคตตาล็อกสื่อของแอป
หมายเหตุ: ตัวอย่างการใช้งานที่แสดงที่นี่ใช้
BrowseSupportFragment
แทนที่จะเป็น BrowseFragment
ที่เลิกใช้งานแล้ว
BrowseSupportFragment
ขยายเวลาสำหรับ AndroidX
Fragment
ชั้นเรียน
เพื่อช่วยให้เกิดลักษณะการทำงานที่สอดคล้องกันในอุปกรณ์และเวอร์ชัน Android
![หน้าจอหลักของแอป](https://developer.android.com/static/images/tv/app-browse.png?hl=th)
รูปที่ 1 ส่วนการเรียกดูของแอปตัวอย่าง Leanback จะแสดงข้อมูลแคตตาล็อกวิดีโอ
สร้างเลย์เอาต์การเรียกดูสื่อ
BrowseSupportFragment
ในชุดเครื่องมือ Leanback UI
ช่วยให้คุณสามารถสร้างเค้าโครงหลักสำหรับการเรียกดูหมวดหมู่และแถวของรายการสื่อที่มี
ขั้นต่ำของโค้ด ตัวอย่างต่อไปนี้จะแสดงวิธีสร้างเลย์เอาต์ที่มีองค์ประกอบ
ออบเจ็กต์ 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
จะตั้งค่า Listener เหตุการณ์สำหรับการดำเนินการของผู้ใช้ในองค์ประกอบ 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()
วางทรัพยากรที่ถอนได้ที่ระบุไว้ไว้ที่มุมขวาบนของส่วนการเรียกดู แสดงในรูปที่ 1 และ 2 วิธีนี้จะแทนที่สตริงชื่อด้วยพารามิเตอร์ ทรัพยากรที่ถอนออกได้ หากมีการเรียกsetTitle()
ด้วย ทรัพยากรที่ถอนออกได้ต้องมีขนาด 52 dp สูงsetTitle()
ตั้งค่าสตริงชื่อที่มุมขวาบนของส่วนการเรียกดู นอกจาก มีการเรียกsetBadgeDrawable()
setHeadersState()
และsetHeadersTransitionOnBackEnabled()
ซ่อนหรือปิดใช้ส่วนหัว ดูส่วนซ่อนหรือปิดใช้ส่วนหัวสำหรับข้อมูลเพิ่มเติมsetBrandColor()
จะกำหนดสีพื้นหลังสำหรับองค์ประกอบ UI ในส่วนการเรียกดู โดยเฉพาะส่วนหัว สีพื้นหลังของส่วนด้วยค่าสีที่ระบุsetSearchAffordanceColor()
กำหนดสีของไอคอนการค้นหาด้วยค่าสีที่ระบุ ไอคอนค้นหา จะปรากฏขึ้นที่มุมซ้ายบนของส่วนการเรียกดูดังที่แสดงในรูปที่ 1 และ 2
ปรับแต่งมุมมองส่วนหัว
ส่วนการเรียกดูที่แสดงในรูปที่ 1 จะแสดงชื่อหมวดหมู่วิดีโอ ซึ่งเป็นส่วนหัวของแถวในฐานข้อมูลวิดีโอในมุมมองข้อความ คุณยังสามารถปรับแต่ง เพื่อรวมมุมมองเพิ่มเติมในเลย์เอาต์ที่ซับซ้อนมากขึ้น ส่วนต่อไปนี้จะแสดงวิธีการ มีมุมมองรูปภาพที่แสดงไอคอนข้างชื่อหมวดหมู่ ดังที่แสดงในรูปที่ 2
![หน้าจอหลักของแอป](https://developer.android.com/static/images/tv/custom-head.png?hl=th)
รูปที่ 2 ส่วนหัวของแถวในส่วนการเรียกดูที่มีทั้งไอคอน และป้ายกำกับข้อความ
เลย์เอาต์สำหรับส่วนหัวของแถวจะได้รับการกำหนดดังต่อไปนี้
<?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
และใช้
วิธีการเชิงนามธรรมในการสร้าง เชื่อมโยง และยกเลิกการเชื่อมโยงผู้ถือมุมมอง ดังต่อไปนี้
ตัวอย่างแสดงวิธีเชื่อมโยงผู้ถือการดูที่มี 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 ที่ใช้เวลาเพียง 2 นาที
ซ่อนหรือปิดใช้ส่วนหัว
บางครั้งคุณไม่ต้องการให้ส่วนหัวของแถวปรากฏ เช่น เมื่อมีจำนวนไม่เพียงพอ
หมวดหมู่ที่ต้องการรายการที่เลื่อนได้ โทรหา BrowseSupportFragment.setHeadersState()
ในระหว่าง onActivityCreated()
ของส่วนย่อย
ในการซ่อนหรือปิดใช้ส่วนหัวของแถว setHeadersState()
เมธอดจะกำหนดสถานะเริ่มต้นของส่วนหัวในส่วนย่อยการเรียกดู โดยเป็นอย่างใดอย่างหนึ่งต่อไปนี้
ค่าคงที่ที่เป็นพารามิเตอร์
HEADERS_ENABLED
: เมื่อสร้างกิจกรรมส่วนย่อยของการเรียกดู ส่วนหัวจะถูกเปิดใช้งานและแสดงโดย "ค่าเริ่มต้น" ส่วนหัวจะปรากฏในรูปที่ 1 และ 2 ในหน้านี้HEADERS_HIDDEN
: เมื่อมีการสร้างกิจกรรมส่วนย่อยของการเรียกดู ส่วนหัวจะเปิดใช้และซ่อนโดยค่าเริ่มต้น ส่วนหัวของหน้าจอยุบดังที่แสดงใน รูปในส่วนแสดงมุมมองการ์ด ผู้ใช้สามารถเลือกส่วนหัวที่ยุบเพื่อขยายได้HEADERS_DISABLED
: เมื่อมีการสร้างกิจกรรมส่วนย่อยของการเรียกดู ส่วนหัวจะถูกปิดใช้งานโดยค่าเริ่มต้นและ ไม่แสดง
หากมีการตั้งค่า HEADERS_ENABLED
หรือ HEADERS_HIDDEN
ไว้ คุณสามารถโทรหา
setHeadersTransitionOnBackEnabled()
เพื่อรองรับการย้ายกลับไปที่ส่วนหัวของแถวจากรายการเนื้อหาที่เลือกในแถว เปิดใช้โดย
เป็นค่าเริ่มต้นถ้าคุณไม่ได้เรียกใช้เมธอด ในการจับการเคลื่อนไหวด้านหลังด้วยตนเอง
ผ่าน false
ไปยัง setHeadersTransitionOnBackEnabled()
และใช้งานการจัดการ Back Stack ของคุณเอง
แสดงรายการสื่อ
BrowseSupportFragment
ให้คุณ
กำหนดและแสดงหมวดหมู่เนื้อหาสื่อที่เรียกดูได้และรายการสื่อจาก
แคตตาล็อกสื่อโดยใช้อะแดปเตอร์และผู้นำเสนอ อะแดปเตอร์ช่วยให้คุณเชื่อมต่อได้
ลงในแหล่งข้อมูลท้องถิ่นหรือออนไลน์ที่มีข้อมูลแคตตาล็อกสื่อของคุณ
อะแดปเตอร์จะใช้ตัวนำเสนอในการสร้างมุมมองและเชื่อมโยงข้อมูลกับมุมมองเหล่านั้น
การแสดงรายการบนหน้าจอ
โค้ดตัวอย่างต่อไปนี้แสดงการใช้งาน 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 } }
เมื่อคุณสร้างชั้นเรียนผู้นำเสนอสำหรับรายการสื่อแล้ว คุณสามารถสร้าง
อะแดปเตอร์และต่อเข้ากับ 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 ที่ใช้เวลาเพียง 2 นาที
อัปเดตพื้นหลัง
คุณอัปเดตพื้นหลังเพื่อเพิ่มความน่าสนใจของภาพลงในแอปการเรียกดูสื่อบนทีวีได้ ขณะผู้ใช้เรียกดูเนื้อหาต่างๆ เทคนิคนี้สามารถช่วยให้เกิดการโต้ตอบกับแอปมากขึ้น เหมือนในภาพยนตร์และเพลิดเพลิน
ชุดเครื่องมือ Leanback UI มีBackgroundManager
สำหรับการเปลี่ยนพื้นหลังของกิจกรรมบนแอปทีวี ตัวอย่างต่อไปนี้จะแสดงวิธีการ
สร้างวิธีง่ายๆ ในการอัปเดตพื้นหลังภายในกิจกรรมบนแอป TV ดังนี้
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
แอปการเรียกดูสื่อจำนวนมากจะอัปเดตพื้นหลังโดยอัตโนมัติเมื่อผู้ใช้ไปยังส่วนต่างๆ
ผ่านรายการสื่อ คุณสามารถตั้งค่า Listener ที่เลือกโดยอัตโนมัติ
อัปเดตพื้นหลังตามการเลือกปัจจุบันของผู้ใช้ ตัวอย่างต่อไปนี้จะแสดงวิธีการ
เพื่อตั้งค่าชั้นเรียน OnItemViewSelectedListener
ให้
ตรวจจับกิจกรรมการเลือกและอัปเดตพื้นหลัง
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(); } } }; }
หมายเหตุ: ตัวอย่างการติดตั้งใช้งานก่อนหน้านี้คือตัวอย่างง่ายๆ เพื่อวัตถุประสงค์ต่อไปนี้ ภาพ เมื่อสร้างฟังก์ชันนี้ในแอปของคุณเอง ให้เรียกใช้ การดำเนินการอัปเดตในเบื้องหลังในชุดข้อความแยกต่างหากเพื่อประสิทธิภาพที่ดีขึ้น นอกจากนี้ หากคุณ วางแผนอัปเดตพื้นหลังเพื่อตอบสนองต่อผู้ใช้ที่เลื่อนดูรายการต่างๆ เพิ่ม ระยะเวลาเลื่อนการอัปเดตภาพพื้นหลังจนกว่าผู้ใช้จะตัดสินใจดำเนินการใดๆ เทคนิคนี้ช่วยเลี่ยง การอัปเดตภาพพื้นหลังมากเกินไป