在 AAOS 中实现媒体卡片

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

Media card

Media card

Media card

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

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

显示媒体信息的 ViewGroups 观察来自 car-media-common 库*数据*模型 PlaybackViewModel 的 LiveData 更新,以填充 ViewGroup。 每个 LiveData 更新都对应于已更改的媒体信息子集,例如 MediaItemMetadataPlaybackStateWrapperMediaSource

因为这种方法会导致重复代码(每个客户端应用都会在每个 LiveData 片段上添加 Observers,并且许多类似的 View 都被分配了更新的数据),所以我们创建了 **PlaybackCardController**。

PlaybackCardController

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

setupController 函数中,ViewGroup 会按 ID 解析某些视图,使用 mView.findViewById(R.id.xxx) 并分配给受保护的 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。 如果元数据为空,则 View 将被隐藏。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

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

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 或子类

Controller 类应从 Fragment 或 Activity 实例化,以便为 LiveData 观察者提供 LifecycleOwner。

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

mViewModelPlaybackCardViewModel(或子类)的实例。

PlaybackCardViewModel 以保存状态

PlaybackCardViewModel 是一个状态保存 ViewModel,它绑定到 Fragment 或 Activity,如果发生配置更改(例如,当用户驾车穿过隧道时从浅色主题切换到深色主题),则应使用它来重建媒体卡片的内容。 默认的 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 对象,但已将 PlaybackQueueController 类添加到 car-media-common 库中以简化此过程。 CarUiRecyclerView 中每个项目的布局由客户端应用以及可选的 Header 布局指定。 客户端应用还可以选择使用自定义 UXR 限制来限制在驾驶状态期间队列中显示的项目数。

PlaybackQueueController 构造函数和 setter 在以下示例中显示。 如果在前者情况下,容器已包含 ID 为 queue_listCarUiRecyclerView,并且在后者情况下,队列没有 Header,则 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;
    }

每个队列项的布局应包含它想要显示的 View 的 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

显示先前播放的 MediaSources 的历史记录

在本节中,您将学习如何显示和呈现先前播放的媒体源的历史记录。

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

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

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 呈现历史记录 UI

使用新的 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