Карточка мультимедиа — это автономная ViewGroup, которая отображает метаданные мультимедиа, такие как название, обложка альбома и т. д., а также элементы управления воспроизведением, такие как «Воспроизвести» и «Пауза» , «Пропустить» и даже пользовательские действия, предоставляемые сторонним приложением мультимедиа. Карточка мультимедиа также может отображать очередь элементов мультимедиа, например, список воспроизведения.
Рисунок 1. Примеры реализации медиа-карт.
Как реализованы медиа-карты в AAOS?
ViewGroups, которые показывают информацию о медиа, наблюдают за обновлениями LiveData из модели данных библиотеки car-media-common
, PlaybackViewModel
, чтобы заполнить ViewGroup. Каждое обновление LiveData соответствует подмножеству измененной информации о медиа, например MediaItemMetadata
, PlaybackStateWrapper
и MediaSource
.
Поскольку такой подход приводит к повторению кода (каждое клиентское приложение добавляет Observer для каждого фрагмента LiveData, и многим похожим Views назначаются обновленные данные), мы создали PlaybackCardController
.
PlaybackCardController
PlaybackCardController
был добавлен в библиотеку car-media-common
для помощи в создании медиа-карты. Это открытый класс, который создается с помощью ViewGroup ( mView
), PlaybackViewModel ( mDataModel
), PlaybackCardViewModel ( mViewModel
) и экземпляра MediaItemsRepository
( mItemsRepository
).
В функции setupController
ViewGroup анализируется на предмет определенных представлений по идентификатору с помощью 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);
// ...
}
Каждое обновление LiveData из PlaybackViewModel
наблюдается в защищенном методе и выполняет взаимодействия с Views, соответствующими полученным данным. Например, наблюдатель MediaItemMetadata
устанавливает заголовок в mTitle
TextView
и передает MediaItemMetadata.ArtworkRef
в обложку альбома ImageBinder
mAlbumArtBinder
. Если метаданные равны null, Views скрыты. Подклассы 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, чтобы иметь LifecycleOwner для наблюдателей LiveData.
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
для отслеживания состояния очереди, истории и меню переполнения с помощью предоставленных геттеров и сеттеров.
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
предоставляет API LiveData для определения, поддерживает ли MediaSource очередь, и для извлечения списка объектов MediaItemMetadata
в очереди. Хотя эти API можно использовать напрямую для заполнения объекта RecyclerView
информацией об очереди, в библиотеку car-media-common
был добавлен класс PlaybackQueueController
для упрощения этого процесса. Макет для каждого элемента в CarUiRecyclerView
указывается клиентским приложением, а также необязательным макетом заголовка. Клиентское приложение также может ограничить количество элементов, отображаемых в очереди во время состояния вождения, с помощью настраиваемых ограничений UXR.
Конструктор и сеттеры PlaybackQueueController
показаны в следующем примере. Ресурсы макета queueResource
и headerResource
могут быть переданы как Resources.ID_NULL
, если в первом случае контейнер уже содержит CarUiRecyclerView
с id queue_list
, а во втором случае очередь не имеет заголовка.
/**
* 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;
}
Макет для каждого элемента очереди должен содержать идентификаторы представлений, которые он хотел бы отобразить, соответствующие тем, которые используются во внутреннем классе QueueViewHolder
.
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
(или подкласса), PlaybackQueueController
можно создать в конструкторе PlaybackCardController
с использованием mDataModel
и mItemsRepository
для экземпляров PlaybackViewModel
и MediaItemsRepository
соответственно.
Показать историю ранее воспроизведенных MediaSources
В этом разделе вы узнаете, как отображать и выводить на экран историю ранее воспроизведенных медиаисточников.
Получить список истории с помощью API PlaybackCardViewModel
PlaybackCardViewModel
предоставляет API LiveData, называемый getHistoryList()
, для получения списка истории мультимедиа. Он возвращает 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
Используйте новый PlaybackHistoryController
для заполнения данных истории в CarUiRecyclerView
. Конструкторы и основные функции этого класса следующие. Контейнер, переданный из клиентского приложения, должен содержать CarUiRecyclerView
с идентификатором history_list
. CarUiRecyclerView
отображает элементы списка и необязательный заголовок. Оба макета для элемента списка и заголовка могут быть переданы из клиентского приложения. Если Resources.ID_NULL
установлен как headerResource, заголовок не отображается. После того, как PlaybackCardViewModel
передается в контроллер, он отслеживает LiveData<List<MediaSource>>
извлеченный из playbackCardViewModel.getHistoryList()
.
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() {
}
}
Макет для каждого элемента должен содержать идентификаторы представлений, которые он хочет отобразить, соответствующие идентификаторам, используемым во внутреннем классе ViewHolder
.
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
(или подкласса), можно создать PlaybackHistoryController
в конструкторе PlaybackCardController
.