在 AAOS 中实现媒体卡片

媒体卡片是一个独立的 ViewGroup,用于显示媒体元数据(例如标题、专辑封面等),并显示播放控件(例如播放暂停跳过),甚至第三方媒体应用提供的自定义操作。媒体卡片还可以显示媒体内容队列,例如播放列表。

媒体卡

媒体卡

媒体卡

图 1. 媒体卡片示例实现。

如何在 AAOS 中实现媒体卡片?

显示媒体信息的 ViewGroup 会监控 car-media-common数据模型 PlaybackViewModel 中的 LiveData 更新,以填充 ViewGroup。每个 LiveData 更新都对应于已更改的媒体信息的一部分,例如 MediaItemMetadataPlaybackStateWrapperMediaSource

由于这种方法会导致代码重复(每个客户端应用都会在每个 LiveData 上添加 Observer,并且许多类似的 View 都会被分配更新后的数据),因此我们创建了 PlaybackCardController

PlaybackCardController

PlaybackCardController 已添加到 car-media-common 库中,以帮助创建媒体卡片。这是一个公共类,使用 ViewGroup (mView)、PlaybackViewModel (mDataModel)、PlaybackCardViewModel (mViewModel) 和 MediaItemsRepository 实例 (mItemsRepository) 构建而成。

setupController 函数中,系统会使用 mView.findViewById(R.id.xxx) 按 ID 解析 ViewGroup 中的特定视图,并将其分配给受保护的 View 对象。

private void getViewsFromWidget() {
        mTitle
= mView.findViewById(R.id.title);
        mAlbumCover
= mView.findViewById(R.id.album_art);
        mDescription
= mView.findViewById(R.id.album_title);
        mLogo
= mView.findViewById(R.id.content_format);

        mAppIcon
= mView.findViewById(R.id.media_widget_app_icon);
        mAppName
= mView.findViewById(R.id.media_widget_app_name);

         
// ...
}

系统会在受保护的方法中观察 PlaybackViewModel 中的每个 LiveData 更新,并与与收到的数据相关的 View 进行交互。例如,MediaItemMetadata 上的观察器会在 mTitle TextView 上设置标题,并将 MediaItemMetadata.ArtworkRef 传递给专辑封面 ImageBinder mAlbumArtBinder。如果元数据为 null,则视图会隐藏。如有需要,Controller 的子类可以替换此逻辑。

mDataModel.getMetadata().observe(mViewLifecycle, this::updateMetadata);
// ...

/** Update views with {@link MediaItemMetadata} */
protected void updateMetadata(MediaItemMetadata metadata) {
       
if (metadata != null) {
           
String defaultTitle = mView.getContext().getString(
                    R
.string.metadata_default_title);
            updateTextViewAndVisibility
(mTitle, metadata.getTitle(),    defaultTitle);
            updateTextViewAndVisibility
(mSubtitle, metadata.getSubtitle());
            updateMediaLink
(mSubtitleLinker,metadata.getSubtitleLinkMediaId());
            updateTextViewAndVisibility
(mDescription, metadata.getDescription());
            updateMediaLink
(mDescriptionLinker, metadata.getDescriptionLinkMediaId());
            updateMetadataAlbumCoverArtworkRef
(metadata.getArtworkKey());
            updateMetadataLogoWithUri
(metadata);
       
} else {
           
ViewUtils.setVisible(mTitle, false);
           
ViewUtils.setVisible(mSubtitle, false);
           
ViewUtils.setVisible(mAlbumCover, false);
           
ViewUtils.setVisible(mDescription, false);
           
ViewUtils.setVisible(mLogo, false);
       
}
   
}

扩展 PlaybackCardController

如果希望创建媒体卡片的客户端应用在每次 LiveData 更新中都希望处理其他功能,则应扩展 PlaybackCardController。AAOS 中的现有客户端遵循此模式。首先,应创建一个 PlaybackCardController 子类,例如 MediaCardController。接下来,MediaCardController 应添加一个静态内部 Builder 类,该类扩展了 PlaybackCardController 的 Builder 类。

public class MediaCardController extends PlaybackCardController {

   
// extra fields specific to MediaCardController

   
/** Builder for {@link MediaCardController}. Overrides build() method to
     * return NowPlayingController rather than base {@link PlaybackCardController}
     */

   
public static class Builder extends PlaybackCardController.Builder {

       
@Override
       
public MediaCardController build() {
           
MediaCardController controller = new MediaCardController(this);
            controller
.setupController();
           
return controller;
       
}
   
}

   
public MediaCardController(Builder builder) {
       
super(builder);
   
// any other function calls needed in constructor
   
// ...

 
}
}

实例化 PlaybackCardController 或子类

应从 fragment 或 activity 实例化 Controller 类,以便为 LiveData 观察器提供 LifecycleOwner。

mMediaCardController = (MediaCardController) new MediaCardController.Builder()
                   
.setModels(mViewModel.getPlaybackViewModel(),
                            mViewModel
,
                            mViewModel
.getMediaItemsRepository())
                   
.setViewGroup((ViewGroup) view)
                   
.build();

mViewModelPlaybackCardViewModel(或子类)的实例。

PlaybackCardViewModel 用于保存状态

PlaybackCardViewModel 是一个与 fragment 或 activity 相关联的状态保存型 ViewModel,应在发生配置更改(例如当用户驾车穿过隧道时从浅色主题切换为深色主题)时用于重构媒体卡片的内容。默认的 PlaybackCardViewModel 会处理 MediaModel 实例的存储,以便进行播放,从中可以检索 PlaybackViewModelMediaItemsRepository。使用 PlaybackCardViewModel 通过提供的 getter 和 setter 跟踪队列、历史记录和菜单的状态。

public class PlaybackCardViewModel extends AndroidViewModel {

   
private MediaModels mModels;
   
private boolean mNeedsInitialization = true;
   
private boolean mQueueVisible = false;
   
private boolean mHistoryVisible = false;
   
private boolean mOverflowExpanded = false;

   
public PlaybackCardViewModel(@NonNull Application application) {
       
super(application);
   
}

   
/** Initialize the PlaybackCardViewModel */
   
public void init(MediaModels models) {
        mModels
= models;
        mNeedsInitialization
= false;
   
}

   
/**
     * Returns whether the ViewModel needs to be initialized. The ViewModel may
     * need re-initialization if a config change occurs or if the system kills
     * the Fragment.
     */

   
public boolean needsInitialization() {
       
return mNeedsInitialization;
   
}

   
public MediaItemsRepository getMediaItemsRepository() {
       
return mModels.getMediaItemsRepository();
   
}

   
public PlaybackViewModel getPlaybackViewModel() {
       
return mModels.getPlaybackViewModel();
   
}

   
public MediaSourceViewModel getMediaSourceViewModel() {
       
return mModels.getMediaSourceViewModel();
   
}

   
public void setQueueVisible(boolean visible) {
        mQueueVisible
= visible;
   
}

   
public boolean getQueueVisible() {
       
return mQueueVisible;
   
}

   
public void setHistoryVisible(boolean visible) {
        mHistoryVisible
= visible;
   
}

   
public boolean getHistoryVisible() {
       
return mHistoryVisible;
   
}

   
public void setOverflowExpanded(boolean expanded) {
        mOverflowExpanded
= expanded;
   
}

   
public boolean getOverflowExpanded() {
       
return mOverflowExpanded;
   
}
}

如果需要跟踪其他状态,可以扩展此类。

在媒体卡片中显示队列

PlaybackViewModel 提供了 LiveData API,用于检测 MediaSource 是否支持队列,以及检索队列中的 MediaItemMetadata 对象列表。虽然这些 API 可直接用于使用队列信息填充 RecyclerView 对象,但 car-media-common 库中添加了 PlaybackQueueController 类,以简化此过程。CarUiRecyclerView 中的每个项的布局由客户端应用以及可选的标题布局指定。客户端应用还可以选择使用自定义 UXR 限制来限制在驾车状态下队列中显示的项数量。

PlaybackQueueController 构造函数和 setter 如以下示例所示。如果在前一种情况下,容器中已包含具有 id queue_listCarUiRecyclerView,而在后一种情况下,队列没有标题,则可以将 queueResourceheaderResource 布局资源作为 Resources.ID_NULL 传递。

   /**
    * Construct a PlaybackQueueController. If clients don't have a separate
    * layout for the queue, where the queue is already inflated within the
    * container, they should pass {@link Resources.ID_NULL} as the LayoutRes
    * resource. If clients don't require a UxrContentLimiter, they should pass
    * null for uxrContentLimiter and the int passed for uxrConfigurationId will
    * be ignored.
    */

   
public PlaybackQueueController(
           
ViewGroup container,
           
@LayoutRes int queueResource,
           
@LayoutRes int queueItemResource,
           
@LayoutRes int headerResource,
           
LifecycleOwner lifecycleOwner,
           
PlaybackViewModel playbackViewModel,
           
MediaItemsRepository itemsRepository,
           
@Nullable LifeCycleObserverUxrContentLimiter uxrContentLimiter,
           
int uxrConfigurationId) {
     
// ...
   
}

   
public void setShowTimeForActiveQueueItem(boolean show) {
        mShowTimeForActiveQueueItem
= show;
   
}

   
public void setShowIconForActiveQueueItem(boolean show) {
        mShowIconForActiveQueueItem
= show;
   
}

   
public void setShowThumbnailForQueueItem(boolean show) {
        mShowThumbnailForQueueItem
= show;
   
}

   
public void setShowSubtitleForQueueItem(boolean show) {
        mShowSubtitleForQueueItem
= show;
   
}

   
/** Calls {@link RecyclerView#setVerticalFadingEdgeEnabled(boolean)} */
   
public void setVerticalFadingEdgeLengthEnabled(boolean enabled) {
        mQueue
.setVerticalFadingEdgeEnabled(enabled);
   
}

   
public void setCallback(PlaybackQueueCallback callback) {
        mPlaybackQueueCallback
= callback;
   
}

每个队列项的布局应包含其要显示的视图的 ID,这些 ID 应与 QueueViewHolder 内部类中使用的 ID 相对应。

QueueViewHolder(View itemView) {
           
super(itemView);
            mView
= itemView;
            mThumbnailContainer
= itemView.findViewById(R.id.thumbnail_container);
            mThumbnail
= itemView.findViewById(R.id.thumbnail);
            mSpacer
= itemView.findViewById(R.id.spacer);
            mTitle
= itemView.findViewById(R.id.queue_list_item_title);
            mSubtitle
= itemView.findViewById(R.id.queue_list_item_subtitle);
            mCurrentTime
= itemView.findViewById(R.id.current_time);
            mMaxTime
= itemView.findViewById(R.id.max_time);
            mTimeSeparator
= itemView.findViewById(R.id.separator);
            mActiveIcon
= itemView.findViewById(R.id.now_playing_icon);

           
// ...
}

如需在使用 PlaybackCardController(或子类)创建的媒体卡片中显示队列,可以在 PlaybackCardController 构造函数中使用 mDataModelmItemsRepository 分别为 PlaybackViewModelMediaItemsRepository 实例构建 PlaybackQueueController

显示之前播放的 MediaSource 的历史记录

在本部分中,您将学习如何显示和显示之前播放的媒体来源的历史记录。

使用 PlaybackCardViewModel API 获取历史记录列表

PlaybackCardViewModel 提供了一个名为 getHistoryList() 的 LiveData API 来检索媒体历史记录列表。它会返回一个 LiveData,其中包含之前播放过的 MediaSource 的列表。这些数据可用于填充 CarUiRecyclerView 对象。与 PlaybackQueueController 类似,car-media-common 库中添加了一个名为 PlaybackHistoryController 的类,以简化流程。

public class PlaybackCardViewModel extends AndroidViewModel {

   
public PlaybackCardViewModel(@NonNull Application application) {
   
}

   
/** Initialize the PlaybackCardViewModel */
   
public void init(MediaModels models) {
   
}

   
public LiveData<List<MediaSource>> getHistoryList() {
       
return mHistoryListData;
   
}
}

使用 PlaybackHistoryController 显示历史记录界面

使用新的 PlaybackHistoryController 帮助将历史数据填充到 CarUiRecyclerView。此类的构造函数和主要函数如下所示。从客户端应用传递的容器应包含 ID 为 history_listCarUiRecyclerViewCarUiRecyclerView 会显示列表项和可选标题。列表项和标题的布局都可以从客户端应用传递。如果将 Resources.ID_NULL 设置为 headerResource,系统将不显示标题。将 PlaybackCardViewModel 传递给控制器后,它会监控从 playbackCardViewModel.getHistoryList() 检索的 LiveData<List<MediaSource>>

public class PlaybackHistoryController {

   
public PlaybackHistoryController(
           
LifecycleOwner lifecycleOwner,
           
PlaybackCardViewModel playbackCardViewModel,
           
ViewGroup container,
           
@LayoutRes int itemResource,
           
@LayoutRes int headerResource,
           
int uxrConfigurationId) {
   
}

   
/**
     * Renders the view.
     */

   
public void setupView() {
   
}
}

每个项的布局应包含其要显示的 View 的 ID,这些 ID 应与 ViewHolder 内部类中使用的 ID 相对应。

HistoryItemViewHolder(View itemView) {
           
super(itemView);
            mContext
= itemView.getContext();
            mActiveView
= itemView.findViewById(R.id.history_card_container_active);
            mInactiveView
= itemView.findViewById(R.id.history_card_container_inactive);
            mMetadataTitleView
= itemView.findViewById(R.id.history_card_title_active);
            mAdditionalInfo
= itemView.findViewById(R.id.history_card_subtitle_active);
            mAppIcon
= itemView.findViewById(R.id.history_card_app_thumbnail);
            mAlbumArt
= itemView.findViewById(R.id.history_card_album_art);
            mAppTitleInactive
= itemView.findViewById(R.id.history_card_app_title_inactive);
            mAppIconInactive
= itemView.findViewById(R.id.history_item_app_icon_inactive);
// ...
}

如需在使用 PlaybackCardController(或子类)创建的媒体卡片中显示历史记录列表,可以在 PlaybackCardController 的构造函数中构造 PlaybackHistoryController