الاقتراحات في Android N والإصدارات الأقدم

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

ملاحظة: استخدِم واجهات برمجة التطبيقات الموضّحة هنا لتقديم الاقتراحات في التطبيقات التي تعمل على إصدارات Android حتى الإصدار Android 7.1 (مستوى واجهة برمجة التطبيقات 25) ضِمنًا فقط. لتقديم اقتراحات للتطبيقات التي تعمل على الإصدار Android 8.0 (مستوى واجهة برمجة التطبيقات 26) والإصدارات الأحدث، يجب أن يستخدم تطبيقك قنوات الاقتراحات.

يساعد إطار عمل Android في التفاعل بأقل قدر من المعلومات من خلال توفير صف الاقتراحات على الشاشة الرئيسية. تظهر اقتراحات المحتوى كالصف الأول من الشاشة الرئيسية للتلفزيون بعد أول استخدام للجهاز. يمكن أن يساعد تقديم الاقتراحات من كتالوج محتوى تطبيقك في إعادة المستخدمين إلى تطبيقك.

مثال على صف الاقتراحات على شاشة Android TV الرئيسية
الشكل 1. مثال على صف الاقتراحات

يعلّمك هذا الدليل كيفية إنشاء الاقتراحات وتقديمها إلى إطار عمل Android ليتمكّن المستخدمون من اكتشاف محتوى تطبيقك والاستمتاع به. يمكنك أيضًا الاطّلاع على نموذج التنفيذ في تطبيق Leanback النموذجي .

أفضل الممارسات المتعلّقة بالاقتراحات

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

أنواع الاقتراحات

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

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

لمزيد من المعلومات حول كيفية تصميم بطاقات الاقتراحات لتقديم أفضل تجربة للمستخدم، اطّلِع على مقالة صف الاقتراحات في مواصفات تصميم Android TV.

إعادة تحميل المحتوى المقترح

عند إعادة تحميل الاقتراحات، لا تقم بإزالتها وإعادة نشرها فقط، لأنّ ذلك يؤدي إلى ظهور الاقتراحات في نهاية صف الاقتراحات. بعد تشغيل عنصر محتوى، مثل فيلم، أزِله من الاقتراحات.

تخصيص الاقتراحات

يمكنك تخصيص بطاقات الاقتراحات لنقل معلومات العلامة التجارية من خلال ضبط عناصر واجهة المستخدم، مثل الصورة الأمامية والخلفية للبطاقة ولونها ورمز التطبيق وعنوانه وعنوانه الفرعي. لمزيد من المعلومات، اطّلِع على مقالة صف الاقتراحات في مواصفات تصميم Android TV.

تجميع الاقتراحات

يمكنك اختياريًا تجميع الاقتراحات استنادًا إلى مصدر الاقتراح. على سبيل المثال، قد يقدّم تطبيقك مجموعتَين من الاقتراحات: اقتراحات للمحتوى الذي اشترك فيه المستخدم، واقتراحات للمحتوى الجديد الرائج الذي قد لا يكون المستخدم على علم به.

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

استخدِم NotificationCompat.Builder.setGroup() لضبط سلسلة مفتاح المجموعة الخاصة بالاقتراح. على سبيل المثال، لوضع علامة على اقتراح يشير إلى أنّه ينتمي إلى مجموعة تحتوي على محتوى جديد رائج، يمكنك استدعاء setGroup("trending").

إنشاء خدمة الاقتراحات

يتم إنشاء اقتراحات المحتوى من خلال المعالجة في الخلفية. لكي يساهم تطبيقك في الاقتراحات، عليك إنشاء خدمة تضيف بشكل دوري قوائم من كتالوج تطبيقك إلى قائمة الاقتراحات في النظام.

يوضّح مثال الرمز البرمجي التالي كيفية توسيع نطاق IntentService لـ إنشاء خدمة اقتراحات لتطبيقك:

Kotlin

class UpdateRecommendationsService : IntentService("RecommendationService") {
    override protected fun onHandleIntent(intent: Intent) {
        Log.d(TAG, "Updating recommendation cards")
        val recommendations = VideoProvider.getMovieList()
        if (recommendations == null) return

        var count = 0

        try {
            val builder = RecommendationBuilder()
                    .setContext(applicationContext)
                    .setSmallIcon(R.drawable.videos_by_google_icon)

            for (entry in recommendations.entrySet()) {
                for (movie in entry.getValue()) {
                    Log.d(TAG, "Recommendation - " + movie.getTitle())

                    builder.setBackground(movie.getCardImageUrl())
                            .setId(count + 1)
                            .setPriority(MAX_RECOMMENDATIONS - count)
                            .setTitle(movie.getTitle())
                            .setDescription(getString(R.string.popular_header))
                            .setImage(movie.getCardImageUrl())
                            .setIntent(buildPendingIntent(movie))
                            .build()
                    if (++count >= MAX_RECOMMENDATIONS) {
                        break
                    }
                }
                if (++count >= MAX_RECOMMENDATIONS) {
                    break
                }
            }
        } catch (e: IOException) {
            Log.e(TAG, "Unable to update recommendation", e)
        }
    }

    private fun buildPendingIntent(movie: Movie): PendingIntent {
        val detailsIntent = Intent(this, DetailsActivity::class.java)
        detailsIntent.putExtra("Movie", movie)

        val stackBuilder = TaskStackBuilder.create(this)
        stackBuilder.addParentStack(DetailsActivity::class.java)
        stackBuilder.addNextIntent(detailsIntent)

        // Ensure a unique PendingIntents, otherwise all
        // recommendations end up with the same PendingIntent
        detailsIntent.setAction(movie.getId().toString())

        val intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
        return intent
    }

    companion object {
        private val TAG = "UpdateRecommendationsService"
        private val MAX_RECOMMENDATIONS = 3
    }
}

Java

public class UpdateRecommendationsService extends IntentService {
    private static final String TAG = "UpdateRecommendationsService";
    private static final int MAX_RECOMMENDATIONS = 3;

    public UpdateRecommendationsService() {
        super("RecommendationService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "Updating recommendation cards");
        HashMap<String, List<Movie>> recommendations = VideoProvider.getMovieList();
        if (recommendations == null) return;

        int count = 0;

        try {
            RecommendationBuilder builder = new RecommendationBuilder()
                    .setContext(getApplicationContext())
                    .setSmallIcon(R.drawable.videos_by_google_icon);

            for (Map.Entry<String, List<Movie>> entry : recommendations.entrySet()) {
                for (Movie movie : entry.getValue()) {
                    Log.d(TAG, "Recommendation - " + movie.getTitle());

                    builder.setBackground(movie.getCardImageUrl())
                            .setId(count + 1)
                            .setPriority(MAX_RECOMMENDATIONS - count)
                            .setTitle(movie.getTitle())
                            .setDescription(getString(R.string.popular_header))
                            .setImage(movie.getCardImageUrl())
                            .setIntent(buildPendingIntent(movie))
                            .build();

                    if (++count >= MAX_RECOMMENDATIONS) {
                        break;
                    }
                }
                if (++count >= MAX_RECOMMENDATIONS) {
                    break;
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "Unable to update recommendation", e);
        }
    }

    private PendingIntent buildPendingIntent(Movie movie) {
        Intent detailsIntent = new Intent(this, DetailsActivity.class);
        detailsIntent.putExtra("Movie", movie);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack(DetailsActivity.class);
        stackBuilder.addNextIntent(detailsIntent);
        // Ensure a unique PendingIntents, otherwise all
        // recommendations end up with the same PendingIntent
        detailsIntent.setAction(Long.toString(movie.getId()));

        PendingIntent intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        return intent;
    }
}

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

<manifest ... >
  <application ... >
    ...

    <service
            android:name="com.example.android.tvleanback.UpdateRecommendationsService"
            android:enabled="true" />
  </application>
</manifest>

إنشاء الاقتراحات

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

ضبط القيم

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

Kotlin

class RecommendationBuilder {
    ...

    fun setTitle(title: String): RecommendationBuilder {
        this.title = title
        return this
    }

    fun setDescription(description: String): RecommendationBuilder {
        this.description = description
        return this
    }

    fun setImage(uri: String): RecommendationBuilder {
        imageUri = uri
        return this
    }

    fun setBackground(uri: String): RecommendationBuilder {
        backgroundUri = uri
        return this
    }

...

Java

public class RecommendationBuilder {
    ...

    public RecommendationBuilder setTitle(String title) {
            this.title = title;
            return this;
        }

        public RecommendationBuilder setDescription(String description) {
            this.description = description;
            return this;
        }

        public RecommendationBuilder setImage(String uri) {
            imageUri = uri;
            return this;
        }

        public RecommendationBuilder setBackground(String uri) {
            backgroundUri = uri;
            return this;
        }
...

إنشاء الإشعار

بعد ضبط القيم، عليك إنشاء الإشعار، وتعيين القيم من فئة المنشئ للإشعار، واستدعاء NotificationCompat.Builder.build().

احرِص أيضًا على استدعاء setLocalOnly() حتى لا يظهر إشعار NotificationCompat.BigPictureStyle على الأجهزة الأخرى.

يوضّح مثال الرمز البرمجي التالي كيفية إنشاء اقتراح.

Kotlin

class RecommendationBuilder {
    ...

    @Throws(IOException::class)
    fun build(): Notification {
        ...

        val notification = NotificationCompat.BigPictureStyle(
        NotificationCompat.Builder(context)
                .setContentTitle(title)
                .setContentText(description)
                .setPriority(priority)
                .setLocalOnly(true)
                .setOngoing(true)
                .setColor(context.resources.getColor(R.color.fastlane_background))
                .setCategory(Notification.CATEGORY_RECOMMENDATION)
                .setLargeIcon(image)
                .setSmallIcon(smallIcon)
                .setContentIntent(intent)
                .setExtras(extras))
                .build()

        return notification
    }
}

Java

public class RecommendationBuilder {
    ...

    public Notification build() throws IOException {
        ...

        Notification notification = new NotificationCompat.BigPictureStyle(
                new NotificationCompat.Builder(context)
                        .setContentTitle(title)
                        .setContentText(description)
                        .setPriority(priority)
                        .setLocalOnly(true)
                        .setOngoing(true)
                        .setColor(context.getResources().getColor(R.color.fastlane_background))
                        .setCategory(Notification.CATEGORY_RECOMMENDATION)
                        .setLargeIcon(image)
                        .setSmallIcon(smallIcon)
                        .setContentIntent(intent)
                        .setExtras(extras))
                .build();

        return notification;
    }
}

تشغيل خدمة الاقتراحات

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

Kotlin

class BootupActivity : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "BootupActivity initiated")
        if (intent.action.endsWith(Intent.ACTION_BOOT_COMPLETED)) {
            scheduleRecommendationUpdate(context)
        }
    }

    private fun scheduleRecommendationUpdate(context: Context) {
        Log.d(TAG, "Scheduling recommendations update")
        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val recommendationIntent = Intent(context, UpdateRecommendationsService::class.java)
        val alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0)
        alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                INITIAL_DELAY,
                AlarmManager.INTERVAL_HALF_HOUR,
                alarmIntent
        )
    }

    companion object {
        private val TAG = "BootupActivity"
        private val INITIAL_DELAY:Long = 5000
    }
}

Java

public class BootupActivity extends BroadcastReceiver {
    private static final String TAG = "BootupActivity";

    private static final long INITIAL_DELAY = 5000;

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "BootupActivity initiated");
        if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
            scheduleRecommendationUpdate(context);
        }
    }

    private void scheduleRecommendationUpdate(Context context) {
        Log.d(TAG, "Scheduling recommendations update");

        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        Intent recommendationIntent = new Intent(context, UpdateRecommendationsService.class);
        PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0);

        alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                INITIAL_DELAY,
                AlarmManager.INTERVAL_HALF_HOUR,
                alarmIntent);
    }
}

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

<manifest ... >
  <application ... >
    <receiver android:name="com.example.android.tvleanback.BootupActivity"
              android:enabled="true"
              android:exported="false">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    </receiver>
  </application>
</manifest>

ملاحظة مهمة: يتطلّب تلقّي إشعار اكتمال عملية التشغيل أن يطلب تطبيقك إذن RECEIVE_BOOT_COMPLETED. لمزيد من المعلومات، اطّلِع على ACTION_BOOT_COMPLETED.

في طريقة onHandleIntent() لفئة خدمة الاقتراحات، عليك نشر الاقتراح إلى المدير على النحو التالي:

Kotlin

val notification = notificationBuilder.build()
notificationManager.notify(id, notification)

Java

Notification notification = notificationBuilder.build();
notificationManager.notify(id, notification);