یک مرورگر کاتالوگ ایجاد کنید

با Compose بهتر بسازید
با استفاده از Jetpack Compose برای سیستم عامل Android TV، رابط‌های کاربری زیبا با حداقل کد ایجاد کنید.

یک برنامه رسانه ای که روی تلویزیون اجرا می شود باید به کاربران اجازه دهد محتوای ارائه شده آن را مرور کنند، انتخاب کنند و شروع به پخش محتوا کنند. تجربه مرور محتوا باید ساده و شهودی و همچنین از نظر بصری دلپذیر و جذاب باشد.

این راهنما نحوه استفاده از کلاس‌های ارائه‌شده توسط کتابخانه androidx.leanback را برای پیاده‌سازی یک رابط کاربری برای مرور موسیقی یا ویدیوها از کاتالوگ رسانه برنامه‌تان مورد بحث قرار می‌دهد.

توجه: مثال پیاده سازی نشان داده شده در اینجا از BrowseSupportFragment به جای کلاس BrowseFragment منسوخ استفاده می کند. BrowseSupportFragment کلاس AndroidX Fragment را گسترش می‌دهد و به اطمینان از رفتار ثابت در دستگاه‌ها و نسخه‌های Android کمک می‌کند.

صفحه اصلی برنامه

شکل 1. بخش مرور برنامه نمونه 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>

فعالیت اصلی برنامه این نمای را تنظیم می کند، همانطور که در مثال زیر نشان داده شده است:

کاتلین

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 نما را با داده‌های ویدئویی و عناصر رابط کاربری پر می‌کنند و پارامترهای طرح‌بندی مانند نماد و عنوان و فعال بودن سرصفحه‌های دسته را تنظیم می‌کنند.

برای اطلاعات بیشتر در مورد تنظیم عناصر رابط کاربری، به بخش تنظیم عناصر رابط کاربری مراجعه کنید. برای اطلاعات بیشتر در مورد پنهان کردن هدرها، به بخش Hide or disable headers مراجعه کنید.

زیر کلاس برنامه که متدهای BrowseSupportFragment پیاده‌سازی می‌کند، شنونده‌های رویداد را برای اقدامات کاربر روی عناصر UI تنظیم می‌کند و مدیر پس‌زمینه را آماده می‌کند، همانطور که در مثال زیر نشان داده شده است:

کاتلین

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());
    }
...

عناصر UI را تنظیم کنید

در نمونه قبلی، متد خصوصی setupUIElements() چندین متد BrowseSupportFragment را برای استایل دادن به مرورگر کاتالوگ رسانه فراخوانی می کند:

  • setBadgeDrawable() منبع قابل ترسیم مشخص شده را در گوشه سمت راست بالای قطعه مرور قرار می دهد، همانطور که در شکل های 1 و 2 نشان داده شده است. اگر setTitle() نیز فراخوانی شود، این روش رشته عنوان را با منبع drawable جایگزین می کند. منبع قابل کشیدن باید 52 dp ارتفاع داشته باشد.
  • setTitle() رشته عنوان را در گوشه سمت راست بالای قطعه مرور تنظیم می کند، مگر اینکه setBadgeDrawable() فراخوانی شود.
  • setHeadersState() و setHeadersTransitionOnBackEnabled() هدرها را مخفی یا غیرفعال می کنند. برای اطلاعات بیشتر به بخش پنهان کردن یا غیرفعال کردن هدرها مراجعه کنید.
  • setBrandColor() رنگ پس‌زمینه عناصر UI را در قطعه مرور، به‌ویژه رنگ پس‌زمینه بخش هدر، با مقدار رنگ مشخص شده تنظیم می‌کند.
  • 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 استفاده کنید و متدهای انتزاعی را برای ایجاد، پیوند و جداسازی دارنده view اجرا کنید. مثال زیر نشان می دهد که چگونه می توان 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
    }
}

هدرهای شما باید قابل فوکوس باشند تا بتوان از D-pad برای پیمایش در آنها استفاده کرد. دو راه برای مدیریت این موضوع وجود دارد:

  • نمای خود را طوری تنظیم کنید که در 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 : هنگامی که فعالیت قطعه مرور ایجاد می شود، سرصفحه ها به طور پیش فرض فعال و نشان داده می شوند. سرصفحه ها مطابق شکل های 1 و 2 در این صفحه ظاهر می شوند.
  • 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 UI یک کلاس BackgroundManager برای تغییر پس‌زمینه فعالیت برنامه تلویزیون شما ارائه می‌کند. مثال زیر نحوه ایجاد یک روش ساده برای به‌روزرسانی پس‌زمینه در فعالیت برنامه تلویزیونی خود را نشان می‌دهد:

کاتلین

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();
            }
        }
    };
}

توجه: اجرای قبلی یک مثال ساده برای مصورسازی است. هنگام ایجاد این عملکرد در برنامه خود، برای عملکرد بهتر، اقدام به روز رسانی پس زمینه را در یک رشته جداگانه اجرا کنید. همچنین، اگر قصد دارید پس‌زمینه را در پاسخ به کاربرانی که در آیتم‌ها پیمایش می‌کنند، به‌روزرسانی کنید، زمانی را اضافه کنید تا به‌روزرسانی تصویر پس‌زمینه را تا زمانی که کاربر روی یک مورد ثابت کند به تأخیر بیاندازید. این تکنیک از به روز رسانی بیش از حد تصاویر پس زمینه جلوگیری می کند.