یک برنامه رسانهای که روی تلویزیون اجرا میشود، باید به کاربران اجازه دهد تا محتوای پیشنهادی آن را مرور کنند، انتخاب کنند و پخش محتوا را شروع کنند. تجربه مرور محتوا باید ساده و شهودی و همچنین از نظر بصری دلپذیر و جذاب باشد.
این راهنما نحوهی استفاده از کلاسهای ارائه شده توسط کتابخانهی منسوخشدهی androidx.leanback را برای پیادهسازی یک رابط کاربری جهت مرور موسیقی یا ویدیوها از کاتالوگ رسانهی برنامهی شما مورد بحث قرار میدهد.
نکته: مثال پیادهسازی نشان داده شده در اینجا از BrowseSupportFragment به جای کلاس منسوخ شده BrowseFragment استفاده میکند. BrowseSupportFragment از کلاس AndroidX Fragment ارثبری میکند و به تضمین رفتار سازگار در دستگاهها و نسخههای مختلف اندروید کمک میکند.

شکل ۱. بخش مرور برنامه نمونه Leanback، دادههای کاتالوگ ویدیو را نمایش میدهد.
ایجاد طرح مرور رسانه
کلاس BrowseSupportFragment در جعبه ابزار Leanback UI به شما امکان میدهد با حداقل کد، یک طرحبندی اولیه برای مرور دستهها و ردیفهای آیتمهای رسانهای ایجاد کنید. مثال زیر نحوه ایجاد یک طرحبندی که شامل یک شیء 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>
همانطور که در مثال زیر نشان داده شده است، activity اصلی برنامه، این view را تنظیم میکند:
کاتلین
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 را پیادهسازی میکند، شنوندههای رویداد را نیز برای اقدامات کاربر روی عناصر رابط کاربری تنظیم میکند و مدیر پسزمینه را آماده میکند، همانطور که در مثال زیر نشان داده شده است:
کاتلین
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()منبع ترسیمپذیر مشخصشده را در گوشه بالا سمت راست قطعه مرور قرار میدهد، همانطور که در شکلهای ۱ و ۲ نشان داده شده است. این متد، در صورت فراخوانیsetTitle()، رشته عنوان را با منبع ترسیمپذیر جایگزین میکند. منبع ترسیمپذیر باید ۵۲ dp ارتفاع داشته باشد. -
setTitle()رشته عنوان را در گوشه بالا سمت راست قطعه کد مرورگر قرار میدهد، مگر اینکه تابعsetBadgeDrawable()فراخوانی شود. -
setHeadersState()وsetHeadersTransitionOnBackEnabled()هدرها را مخفی یا غیرفعال میکنند. برای اطلاعات بیشتر به بخش مخفی کردن یا غیرفعال کردن هدرها مراجعه کنید. -
setBrandColor()رنگ پسزمینه عناصر رابط کاربری در بخش مرور، به ویژه رنگ پسزمینه بخش هدر، را با مقدار رنگ مشخص شده تنظیم میکند. -
setSearchAffordanceColor()رنگ آیکون جستجو را با مقدار رنگ مشخص شده تنظیم میکند. آیکون جستجو در گوشه بالا سمت چپ قطعه کد مرورگر، همانطور که در شکلهای ۱ و ۲ نشان داده شده است، ظاهر میشود.
سفارشیسازی نماهای هدر
قطعه مرور نشان داده شده در شکل ۱، نامهای دستهبندی ویدیو، که سربرگهای ردیف در پایگاه داده ویدیو هستند، را در نماهای متنی نمایش میدهد. همچنین میتوانید سربرگ را برای گنجاندن نماهای اضافی در یک طرح پیچیدهتر سفارشی کنید. بخشهای زیر نحوه گنجاندن یک نمای تصویر را نشان میدهند که یک آیکون را در کنار نام دستهبندی نمایش میدهد، همانطور که در شکل ۲ نشان داده شده است.

شکل ۲. سربرگهای ردیف در قطعه مرور با آیکون و برچسب متنی.
طرحبندی برای سربرگ ردیف به صورت زیر تعریف شده است:
<?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 استفاده کنید و متدهای انتزاعی (abstract) را برای ایجاد، اتصال و جدا کردن نگهدارندهی نما (view holder) پیادهسازی کنید. مثال زیر نحوهی اتصال نگهدارندهی نما (viewholder) به دو نما، یک ImageView و یک TextView ، را نشان میدهد.
کاتلین
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() طوری تنظیم کنید که قابلیت فوکوس داشته باشد:کاتلین
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() برای تنظیم ارائهدهنده برای سربرگ ردیف، همانطور که در مثال زیر نشان داده شده است، استفاده کنید.
کاتلین
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
جاوا
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
برای یک مثال کامل، به برنامه نمونه Leanback مراجعه کنید.
پنهان کردن یا غیرفعال کردن هدرها
گاهی اوقات نمیخواهید سرصفحههای ردیف نمایش داده شوند، مانند زمانی که تعداد دستهها برای نمایش یک لیست قابل اسکرول کافی نیست. برای مخفی کردن یا غیرفعال کردن سرصفحههای ردیف، متد BrowseSupportFragment.setHeadersState() را در طول متد onActivityCreated() قطعه فراخوانی کنید. متد setHeadersState() وضعیت اولیه سرصفحهها را در قطعه مرور تنظیم میکند و یکی از ثابتهای زیر را به عنوان پارامتر دریافت میکند:
-
HEADERS_ENABLED: وقتی اکتیویتی قطعه مرور ایجاد میشود، هدرها فعال شده و به طور پیشفرض نمایش داده میشوند. هدرها همانطور که در شکلهای ۱ و ۲ در این صفحه نشان داده شده است، ظاهر میشوند. -
HEADERS_HIDDEN: وقتی اکتیویتی قطعه مرور ایجاد میشود، هدرها به طور پیشفرض فعال و پنهان میشوند. بخش هدر صفحه نمایش، همانطور که در شکل در «ارائه نمای کارت» نشان داده شده است، جمع میشود. کاربر میتواند بخش هدر جمع شده را برای باز کردن آن انتخاب کند. -
HEADERS_DISABLED: وقتی اکتیویتیِ قطعهی مرور ایجاد میشود، هدرها بهطور پیشفرض غیرفعال هستند و هرگز نمایش داده نمیشوند.
اگر هر یک از HEADERS_ENABLED یا HEADERS_HIDDEN تنظیم شده باشد، میتوانید setHeadersTransitionOnBackEnabled() را برای پشتیبانی از حرکت به عقب به سربرگ ردیف از یک آیتم محتوای انتخاب شده در ردیف فراخوانی کنید. اگر این متد را فراخوانی نکنید، این قابلیت به طور پیشفرض فعال است. برای مدیریت حرکت به عقب، مقدار false را به setHeadersTransitionOnBackEnabled() ارسال کنید و مدیریت پشته بازگشت به عقب را خودتان پیادهسازی کنید.
نمایش لیست رسانهها
کلاس BrowseSupportFragment به شما امکان میدهد دستههای محتوای رسانهای قابل مرور و آیتمهای رسانهای را از یک کاتالوگ رسانهای با استفاده از آداپتورها و ارائهدهندهها تعریف و نمایش دهید. آداپتورها به شما امکان میدهند به منابع داده محلی یا آنلاین که حاوی اطلاعات کاتالوگ رسانه شما هستند متصل شوید. آداپتورها از ارائهدهندهها برای ایجاد نماها و اتصال دادهها به آن نماها برای نمایش یک آیتم روی صفحه استفاده میکنند.
کد مثال زیر پیادهسازی یک Presenter را برای نمایش دادههای رشتهای نشان میدهد:
کاتلین
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 که در مثال کد قبلی نشان داده شده است، نشان میدهد:
کاتلین
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); }
این مثال یک پیادهسازی استاتیک از آداپتورها را نشان میدهد. یک برنامهی مرورگر رسانهی معمولی از دادههای یک پایگاه دادهی آنلاین یا سرویس وب استفاده میکند. برای مثالی از یک برنامهی مرورگر که از دادههای بازیابی شده از وب استفاده میکند، به برنامهی نمونهی Leanback مراجعه کنید.
بهروزرسانی پسزمینه
برای افزودن جذابیت بصری به یک برنامهی مرور رسانه در تلویزیون، میتوانید تصویر پسزمینه را همزمان با مرور محتوا توسط کاربران بهروزرسانی کنید. این تکنیک میتواند تعامل با برنامهی شما را سینماییتر و لذتبخشتر کند.
جعبه ابزار رابط کاربری Leanback یک کلاس BackgroundManager برای تغییر پسزمینه activity برنامه تلویزیونی شما ارائه میدهد. مثال زیر نحوه ایجاد یک متد ساده برای بهروزرسانی پسزمینه در activity برنامه تلویزیونی شما را نشان میدهد:
کاتلین
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
جاوا
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
بسیاری از برنامههای مرور رسانه، همزمان با پیمایش کاربر در فهرست رسانهها، پسزمینه را بهطور خودکار بهروزرسانی میکنند. برای انجام این کار، میتوانید یک شنوندهی انتخاب تنظیم کنید تا پسزمینه را بهطور خودکار بر اساس انتخاب فعلی کاربر بهروزرسانی کند. مثال زیر نحوهی تنظیم یک کلاس OnItemViewSelectedListener را برای دریافت رویدادهای انتخاب و بهروزرسانی پسزمینه نشان میدهد:
کاتلین
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(); } } }; }
نکته: پیادهسازی قبلی یک مثال ساده برای اهداف توضیحی است. هنگام ایجاد این تابع در برنامه خود، برای عملکرد بهتر، اکشن بهروزرسانی پسزمینه را در یک نخ جداگانه اجرا کنید. همچنین، اگر قصد دارید پسزمینه را در پاسخ به پیمایش کاربران در بین آیتمها بهروزرسانی کنید، زمانی را برای تأخیر در بهروزرسانی تصویر پسزمینه تا زمانی که کاربر روی یک آیتم متمرکز شود، اضافه کنید. این تکنیک از بهروزرسانیهای بیش از حد تصویر پسزمینه جلوگیری میکند.
