يجب أن يتيح تطبيق الوسائط الذي يعمل على التلفزيون للمستخدمين تصفّح عروض المحتوى واختيار المحتوى وبدء تشغيله. يجب أن تكون تجربة تصفّح المحتوى بسيطة وسهلة الاستخدام وجذابة بصريًا وتفاعلية.
يوضّح هذا الدليل كيفية استخدام الفئات التي توفّرها مكتبة 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
تُعدّ أيضًا أدوات معالجة الأحداث لإجراءات المستخدم على عناصر واجهة المستخدم وتجهّز
مدير الخلفية، كما هو موضّح في المثال التالي:
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 وحدة بكسل مستقلة عن الكثافة. - يضبط
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 } }
يجب أن تكون العناوين قابلة للتركيز عليها حتى يمكن استخدام لوحة التحكّم الاتجاهية للتنقّل بينها. هناك طريقتان لإدارة ذلك:
- اضبط طريقة العرض لتكون قابلة للتركيز في
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
لعرض بيانات السلسلة:
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(); } } }; }
ملاحظة: التنفيذ السابق هو مثال بسيط لأغراض التوضيح. عند إنشاء هذه الدالة في تطبيقك، شغِّل إجراء التعديل في الخلفية في سلسلة محادثات منفصلة لتحسين الأداء. بالإضافة إلى ذلك، إذا كنت تخطّط لتعديل الخلفية استجابةً لتنقّل المستخدمين بين العناصر، أضِف وقتًا لتأخير تعديل صورة الخلفية إلى أن يستقر المستخدم على أحد العناصر. تتجنّب هذه الطريقة إجراء عدد كبير من التعديلات على صورة الخلفية.