*媒体卡片*是一个独立的 ViewGroup,用于显示媒体元数据,例如标题、专辑封面等,并显示播放控件,例如**播放**和**暂停**、**跳过**,甚至第三方媒体应用提供的自定义操作。媒体卡片还可以显示媒体项队列,例如播放列表。
**图 1.** 媒体卡片示例实现。
媒体卡片在 AAOS 中是如何实现的?
显示媒体信息的 ViewGroups 观察来自 car-media-common
库*数据*模型 PlaybackViewModel
的 LiveData 更新,以填充 ViewGroup。 每个 LiveData 更新都对应于已更改的媒体信息子集,例如 MediaItemMetadata
、PlaybackStateWrapper
和 MediaSource
。
因为这种方法会导致重复代码(每个客户端应用都会在每个 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();
mViewModel
是 PlaybackCardViewModel
(或子类)的实例。
PlaybackCardViewModel 以保存状态
PlaybackCardViewModel
是一个状态保存 ViewModel,它绑定到 Fragment 或 Activity,如果发生配置更改(例如,当用户驾车穿过隧道时从浅色主题切换到深色主题),则应使用它来重建媒体卡片的内容。 默认的 PlaybackCardViewModel
处理存储用于播放的 MediaModel
实例,从中可以检索 PlaybackViewModel
和 MediaItemsRepository
。 使用 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_list
的 CarUiRecyclerView
,并且在后者情况下,队列没有 Header,则 queueResource
和 headerResource
布局资源可以作为 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
构造函数中使用 mDataModel
和 mItemsRepository
分别为 PlaybackViewModel
和 MediaItemsRepository
实例构造 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_list
的 CarUiRecyclerView
。 CarUiRecyclerView
显示列表项和可选的标头。 列表项和标头的两种布局都可以从客户端应用传递。 如果将 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
。