إنشاء متصفِّح كتالوج

الإنشاء بشكل أفضل باستخدام Compose
يمكنك إنشاء واجهات مستخدم رائعة باستخدام أقل عدد ممكن من الرموز باستخدام Jetpack Compose لنظام التشغيل 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 تعمل على إعداد أدوات معالجة الأحداث أيضًا لإجراءات المستخدم على عناصر واجهة المستخدم وتستعد مدير الخلفية، كما هو موضح في المثال التالي:

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
    }
}

يجب أن تكون الرؤوس قابلة للتركيز حتى يمكن استخدام لوحة التحكّم وقم بالتمرير عبرها. هناك طريقتان لإدارة ذلك:

  • ضبط العرض ليكون قابلاً للتركيز في 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();
            }
        }
    };
}

ملاحظة: عملية التنفيذ السابقة هي مثال بسيط لأغراض توضيح. عند إنشاء هذه الدالة في تطبيقك، شغِّل إجراء تحديث في الخلفية في سلسلة محادثات منفصلة لتحقيق أداء أفضل. أيضًا، إذا تخطط لتحديث الخلفية استجابةً للمستخدمين الذين يتنقلون بين العناصر وإضافة وقت لتأجيل تحديث صورة الخلفية إلى أن يستقر المستخدم على أحد العناصر. يتجنب هذا الأسلوب إجراء تحديثات زائدة عن الحد لصورة الخلفية.