באפליקציית מדיה שפועלת בטלוויזיה, המשתמשים צריכים להיות מסוגלים לעיין בתוכן המוצע, לבחור תוכן ולהתחיל להפעיל אותו. חוויית העיון בתוכן צריכה להיות פשוטה ואינטואיטיבית, וגם מושכת ומרתקת מבחינה ויזואלית.
במדריך הזה מוסבר איך להשתמש במחלקות שסופקו על ידי ספריית androidx.leanback שהוצאה משימוש, כדי להטמיע ממשק משתמש לגלישה במוזיקה או בסרטונים מקטלוג המדיה של האפליקציה.
הערה: בדוגמה להטמעה שמוצגת כאן נעשה שימוש ב-BrowseSupportFragment
במקום במחלקה BrowseFragment
שהוצאה משימוש. BrowseSupportFragment
מרחיב את המחלקה AndroidX
Fragment
, ועוזר להבטיח התנהגות עקבית במכשירים ובגרסאות Android שונות.

איור 1. בקטע של הדפדפן באפליקציית הדוגמה Leanback מוצגים נתונים של קטלוג סרטונים.
יצירת פריסה של דפדוף במדיה
המחלקות BrowseSupportFragment
ב-Leanback UI toolkit
מאפשרות ליצור פריסה ראשית לעיון בקטגוריות ובשורות של פריטי מדיה עם
מינימום קוד. בדוגמה הבאה מוצג אופן היצירה של פריסה שמכילה אובייקט 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
מאכלסות את התצוגה בנתוני הסרטון וברכיבי ממשק המשתמש, ומגדירות פרמטרים של פריסה כמו הסמל והכותרת, וגם אם כותרות הקטגוריות מופעלות.
מחלקת המשנה של האפליקציה שמטמיעה את השיטות גם מגדירה מאזינים לאירועים של פעולות משתמש ברכיבי ממשק המשתמש ומכינה את מנהל הרקע, כפי שמוצג בדוגמה הבאה:BrowseSupportFragment
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()); } ...
הגדרת רכיבים בממשק המשתמש
בדוגמה הקודמת, השיטה הפרטית setupUIElements()
קוראת לכמה
BrowseSupportFragment
שיטות כדי לעצב את דפדפן קטלוג המדיה:
-
setBadgeDrawable()
מציב את משאב ה-drawable שצוין בפינה השמאלית העליונה של קטע הגלישה, כמו שמוצג באיורים 1 ו-2. השיטה הזו מחליפה את מחרוזת הכותרת במשאב הניתן לציור, אם גםsetTitle()
נקרא. משאב ה-drawable צריך להיות בגובה 52dp. -
setTitle()
מגדיר את מחרוזת הכותרת בפינה השמאלית העליונה של קטע הגלישה, אלא אם מתבצעת קריאה ל-setBadgeDrawable()
. -
setHeadersState()
ו-setHeadersTransitionOnBackEnabled()
מסתירים או משביתים את הכותרות. מידע נוסף זמין בקטע הסתרה או השבתה של כותרות. setBrandColor()
מגדיר את צבע הרקע של רכיבי ממשק המשתמש בקטע של הדפדפן, במיוחד את צבע הרקע של קטע הכותרת, עם ערך הצבע שצוין.setSearchAffordanceColor()
מגדיר את הצבע של סמל החיפוש עם ערך הצבע שצוין. סמל החיפוש מופיע בפינה הימנית העליונה של קטע הגלישה, כמו שמוצג באיורים 1 ו-2.
התאמה אישית של תצוגות הכותרת
בקטע הגלישה שמוצג באיור 1 מוצגים שמות הקטגוריות של הסרטונים, שהם כותרות השורות במסדי הנתונים של הסרטונים, בתצוגות טקסט. אפשר גם להתאים אישית את הכותרת כדי לכלול תצוגות נוספות בפריסה מורכבת יותר. בקטעים הבאים מוסבר איך לכלול תצוגת תמונה שמציגה סמל לצד שם הקטגוריה, כמו שמוצג באיור 2.

איור 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
ומטמיעים את השיטות המופשטות כדי ליצור את מחזיק התצוגה, לקשור אותו ולבטל את הקשר שלו. בדוגמה הבאה אפשר לראות איך לקשר את ViewHolder עם שני תצוגות, 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 } }
הכותרות צריכות להיות ניתנות למיקוד כדי שאפשר יהיה להשתמש בלחצני החיצים כדי לגלול ביניהן. יש שתי דרכים לנהל את זה:
- כדי להגדיר את התצוגה כך שאפשר יהיה להתמקד בה ב-
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()
של הפריט. השיטה setHeadersState()
מגדירה את המצב הראשוני של הכותרות בקטע של הדפדפן, בהינתן אחד מהקבועים הבאים כפרמטר:
HEADERS_ENABLED
: כשפעילות הגלישה של קטע נוצרת, הכותרות מופעלות ומוצגות כברירת מחדל. הכותרות יופיעו כמו באיורים 1 ו-2 בדף הזה.HEADERS_HIDDEN
: כשיוצרים את פעילות הגלישה, הכותרות מופעלות ומוסתרות כברירת מחדל. הקטע של הכותרת במסך מכווץ, כמו שמוצג בדמות במאמר הצגת כרטיס. המשתמש יכול ללחוץ על כותרת הקטע המכווץ כדי להרחיב אותו.HEADERS_DISABLED
: כשפעילות של מקטע גלישה נוצרת, הכותרות מושבתות כברירת מחדל והן אף פעם לא מוצגות.
אם אחת מהאפשרויות HEADERS_ENABLED
או HEADERS_HIDDEN
מוגדרת, אפשר להתקשר אל setHeadersTransitionOnBackEnabled()
כדי לתמוך בחזרה לכותרת השורה מפריט תוכן שנבחר בשורה. ההגדרה הזו מופעלת כברירת מחדל אם לא קוראים לשיטה. כדי לטפל בעצמכם בתנועה אחורה,
מעבירים את false
אל setHeadersTransitionOnBackEnabled()
ומטמיעים את הטיפול שלכם במחסנית החזרה.
הצגת רשימות של מדיה
המחלקות 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 } }
אחרי שיוצרים מחלקה של פריטים להצגה עבור פריטי המדיה, אפשר ליצור מתאם ולצרף אותו ל-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 .
עדכון הרקע
כדי להוסיף עניין ויזואלי לאפליקציה לעיון במדיה בטלוויזיה, אפשר לעדכן את תמונת הרקע בזמן שהמשתמשים מעיינים בתוכן. הטכניקה הזו יכולה להפוך את האינטראקציה עם האפליקציה שלכם לקולנועית ומהנה יותר.
ערכת הכלים לבניית ממשק המשתמש של 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
כדי לתעד אירועי בחירה ולעדכן את הרקע:
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(); } } }; }
הערה: ההטמעה הקודמת היא דוגמה פשוטה למטרות המחשה. כשיוצרים את הפונקציה הזו באפליקציה שלכם, מריצים את פעולת העדכון ברקע בשרשור נפרד כדי לשפר את הביצועים. בנוסף, אם אתם מתכננים לעדכן את הרקע בתגובה לגלילה של המשתמשים בין הפריטים, כדאי להוסיף זמן להשהיית עדכון תמונת הרקע עד שהמשתמש יתמקד בפריט מסוים. הטכניקה הזו מונעת עדכונים מוגזמים של תמונות הרקע.