يجب أن يسمح تطبيق الوسائط الذي يتم تشغيله على التلفزيون للمستخدمين بتصفُّح المحتوى الذي يوفّره واختيار المحتوى وبدء تشغيله. ويجب أن تكون تجربة تصفح المحتوى بسيطة وسهلة الاستخدام وممتعة مرئيًا.
يناقش هذا الدليل كيفية استخدام الصفوف التي تقدمها مكتبة Leanback androidx لتنفيذ واجهة مستخدم لتصفح الموسيقى أو الفيديوهات من كتالوج الوسائط في تطبيقك.
ملاحظة: يستخدِم مثال التنفيذ الموضّح هنا 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) } ...
جافا
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() } ...
جافا
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 } }
جافا
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 // ... }
جافا
@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() } })
جافا
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
للحصول على مثال كامل، يمكنك الاطّلاع على نموذج تطبيق Android Leanback في مستودع GitHub الخاص بـ Android TV.
إخفاء العناوين أو إيقافها
أحيانًا لا تريد أن تظهر عناوين الصفوف، مثلما هو الحال عندما لا تكون هناك فئات
كافية لطلب قائمة قابلة للتمرير. عليك استدعاء الطريقة 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 } }
جافا
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 }
جافا
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); }
يوضح هذا المثال تنفيذًا ثابتًا للمحوّلات. ويستخدم التطبيق العادي لتصفّح الوسائط بيانات من قاعدة بيانات أو خدمة ويب على الإنترنت. للحصول على مثال على تطبيق تصفح يستخدم البيانات التي تم استردادها من الويب، اطّلع على نموذج تطبيق Android Leanback في مستودع GitHub الخاص بـ Android TV.
تعديل الخلفية
لإضافة اهتمام مرئي إلى تطبيق لتصفّح الوسائط على التلفزيون، يمكنك تعديل صورة الخلفية أثناء تصفّح المستخدمين للمحتوى. ويمكن أن يجعل هذا الأسلوب التفاعل مع تطبيقك أكثر سينمائية وممتعًا.
توفر مكتبة دعم Leanback صف BackgroundManager
لتغيير خلفية النشاط على تطبيق التلفزيون. يوضّح المثال التالي كيفية إنشاء طريقة بسيطة لتحديث الخلفية ضمن النشاط على تطبيقات التلفزيون:
لغة Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
جافا
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() } }
جافا
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(); } } }; }
ملاحظة: عملية التنفيذ السابقة هي مثال بسيط لأغراض التوضيح. عند إنشاء هذه الدالة في تطبيقك، شغِّل إجراء التحديث في الخلفية في سلسلة محادثات منفصلة للحصول على أداء أفضل. وإذا كنت تريد تعديل الخلفية استجابةً لتنقُّل المستخدمين بين العناصر، عليك إضافة وقتًا لتأجيل عملية تعديل صورة الخلفية إلى أن يستقرّ المستخدم على العنصر. يتجنّب هذا الأسلوب إجراء التعديلات المفرطة لصور الخلفية.