אפליקציית מדיה שפועלת בטלוויזיה צריכה לאפשר למשתמשים לעיין בהצעות התוכן שלה, ליצור בחירה, ולהתחיל להשמיע את התוכן. חוויית הגלישה בתוכן חייב להיות פשוט ואינטואיטיבי, נעים לעין ומעניין.
במדריך הזה מוסבר איך להשתמש בכיתות שזמינות בספריית androidx.leanback כדי להטמיע ממשק משתמש לדפדוף במוזיקה או בסרטונים מקטלוג המדיה של האפליקציה.
הערה: בדוגמת ההטמעה שמוצגת כאן:
BrowseSupportFragment
ולא BrowseFragment
שהוצא משימוש
בכיתה. BrowseSupportFragment
מרחיבים את AndroidX
כיתה אחת (Fragment
),
כדי להבטיח התנהגות עקבית במכשירים שונים ובגרסאות Android שונות.
![המסך הראשי של האפליקציה](https://developer.android.com/static/images/tv/app-browse.png?hl=he)
איור 1. קטע העיון של האפליקציה לדוגמה של Leanback מציג נתוני קטלוג סרטונים.
יצירת פריסה של עיון במדיה
BrowseSupportFragment
בערכת הכלים לבניית ממשק משתמש ל-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
מאכלסות את התצוגה עם
נתוני וידאו ורכיבי ממשק משתמש ולהגדיר פרמטרים לפריסה כמו הסמל והכותרת
האם כותרות הקטגוריות מופעלות.
המחלקה המשנית של האפליקציה שבה מטמיעים את BrowseSupportFragment
methods גם מגדירות פונקציות event listener לפעולות משתמשים ברכיבי ממשק המשתמש והכנות
את מנהל הרקע, כמו בדוגמה הבאה:
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()
מציבה את המשאב שניתן להזזה שצוין בפינה השמאלית העליונה של מקטע הדפדוף, מוצגת במספרים 1 ו-2. השיטה הזו מחליפה את מחרוזת הכותרת במחרוזת משאב שניתן להזזה, אם קוראים גםsetTitle()
. המשאב שניתן להזזה חייב להיות בגודל 52 dp גבוה.setTitle()
מגדיר את מחרוזת הכותרת בפינה הימנית העליונה של קטע העיון, אלא אם קוראים לפונקציהsetBadgeDrawable()
.setHeadersState()
וגםsetHeadersTransitionOnBackEnabled()
מסתירים או משביתים את הכותרות. מידע נוסף זמין בקטע הסתרה או השבתה של כותרות.setBrandColor()
מגדירה את צבע הרקע של רכיבי ממשק המשתמש במקטע העיון, באופן ספציפי הכותרת צבע רקע של קטע, עם ערך הצבע שצוין.setSearchAffordanceColor()
מגדיר את הצבע של סמל החיפוש עם ערך הצבע שצוין. סמל החיפוש מופיעה בפינה הימנית העליונה של קטע הגלישה, כפי שמוצג בדמויות 1 ו-2.
התאמה אישית של תצוגות הכותרת
קטע העיון המוצג באיור 1 מציג את שמות קטגוריות הסרטונים. שהן כותרות השורות במסד הנתונים של הסרטונים, בתצוגות טקסט. אפשר גם להתאים אישית כדי לכלול תצוגות נוספות בפריסה מורכבת יותר. בקטעים הבאים נסביר איך לכלול תצוגת תמונה שבה מוצג סמל לצד שם הקטגוריה, כפי שמוצג באיור 2.
![המסך הראשי של האפליקציה](https://developer.android.com/static/images/tv/custom-head.png?hl=he)
איור 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
ומטמיעים את
שיטות מופשטות ליצירה, קישור וביטול הקישור של בעל התצוגה. הבאים
דוגמה שממחישה איך לקשר את בעל הצפייה באמצעות שתי תצוגות,
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) לגלול ביניהם. יש שתי דרכים לנהל את זה:
- הגדרת התצוגה כך שניתן יהיה להתמקד בה באמצעות
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()
כדי לתמוך בחזרה לכותרת השורה מפריט תוכן שנבחר בשורה. זה מופעל על ידי
כברירת מחדל, אם לא קוראים ל-method. כדי לטפל בתנועת הגב בעצמכם,
להעביר את false
אל setHeadersTransitionOnBackEnabled()
ולהטמיע טיפול בערימה חוזרת משלכם.
הצגת רשימות של מדיה
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 הקצר הזה. התשובות שלך יעזרו לנו להשתפר.
עדכון הרקע
כדי להוסיף תחום עניין חזותי לאפליקציית גלישה במדיה בטלוויזיה, אפשר לעדכן את הרקע התמונה כשהמשתמשים מדפדפים בתוכן. השיטה הזו יכולה לשפר את האינטראקציה עם האפליקציה קולנועי ומהנה.
ערכת הכלים לבניית ממשק משתמש ל-Leanback מספקת BackgroundManager
class כדי לשנות את הרקע של הפעילות שלך באפליקציות לטלוויזיה. הדוגמה הבאה מראה איך
אפשר ליצור שיטה פשוטה לעדכון הרקע בפעילות באפליקציה לטלוויזיה:
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(); } } }; }
הערה: ההטמעה הקודמת היא דוגמה פשוטה למטרות של איור. כדי ליצור את הפונקציה הזו באפליקציה שלכם, צריך להריץ את הפקודה פעולת העדכון ברקע בשרשור נפרד להשגת ביצועים טובים יותר. כמו כן, אם לעדכן את הרקע בתגובה למשתמשים שגוללים בין פריטים, להוסיף פרק זמן לעיכוב עדכון של תמונת הרקע עד שהמשתמש יתבסס על פריט. הטכניקה הזו מונעת יותר מדי עדכונים של תמונות רקע.