יצירת דפדפן של קטלוג

משפרים את הפיתוח באמצעות 'כתיבה'
בעזרת 'Jetpack פיתוח נייטיב' ל-Android TV תוכלו ליצור ממשקי משתמש יפהפיים עם כמה שפחות קוד.

אפליקציית מדיה שפועלת בטלוויזיה צריכה לאפשר למשתמשים לעיין בהצעות התוכן שלה, ליצור בחירה, ולהתחיל להשמיע את התוכן. חוויית הגלישה בתוכן חייב להיות פשוט ואינטואיטיבי, נעים לעין ומעניין.

במדריך הזה מוסבר איך להשתמש בכיתות שזמינות בספריית androidx.leanback כדי להטמיע ממשק משתמש לדפדוף במוזיקה או בסרטונים מקטלוג המדיה של האפליקציה.

הערה: בדוגמת ההטמעה שמוצגת כאן: BrowseSupportFragment ולא BrowseFragment שהוצא משימוש בכיתה. BrowseSupportFragment מרחיבים את AndroidX כיתה אחת (Fragment), כדי להבטיח התנהגות עקבית במכשירים שונים ובגרסאות Android שונות.

המסך הראשי של האפליקציה

איור 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.

המסך הראשי של האפליקציה

איור 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();
            }
        }
    };
}

הערה: ההטמעה הקודמת היא דוגמה פשוטה למטרות של איור. כדי ליצור את הפונקציה הזו באפליקציה שלכם, צריך להריץ את הפקודה פעולת העדכון ברקע בשרשור נפרד להשגת ביצועים טובים יותר. כמו כן, אם לעדכן את הרקע בתגובה למשתמשים שגוללים בין פריטים, להוסיף פרק זמן לעיכוב עדכון של תמונת הרקע עד שהמשתמש יתבסס על פריט. הטכניקה הזו מונעת יותר מדי עדכונים של תמונות רקע.