Android N 及更早版本中的推荐

在与电视互动时,用户通常希望在观看内容之前进行最少的输入。对于许多电视用户来说,理想的情况是:坐下来,打开电视,然后观看。让用户以最少的步骤获得他们喜欢的內容通常是他们首选的方式。

注意:仅使用此处描述的 API 来对在 Android 7.1(API 级别 25)及更低版本中运行的应用进行推荐。要为在 Android 8.0(API 级别 26)及更高版本中运行的应用提供推荐,您的应用必须使用推荐频道

Android 框架通过在主屏幕上提供推荐行来协助最小输入交互。内容推荐在设备首次使用后显示为主屏幕的第一行。提供来自您应用内容目录的推荐可以帮助用户返回您的应用。

图 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 对象,这些对象使用特定的模板并标记有特定的类别。

设置值

要设置推荐卡片的 UI 元素值,您可以创建一个遵循如下所述的构建器模式的构建器类。首先,设置推荐卡片元素的值。

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 类的实现必须在安装它的电视设备启动后运行。为此,请在应用清单中注册此类,并使用一个意图过滤器来侦听设备启动过程的完成。以下示例代码演示了如何在清单中添加此配置

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