Карточка мультимедиа — это автономная группа ViewGroup, которая отображает метаданные медиафайла, такие как название, обложка альбома и т. д., а также элементы управления воспроизведением, такие как «Воспроизвести» и «Пауза» , «Пропустить», и даже настраиваемые действия, предоставляемые сторонним приложением для работы с медиафайлами. Карточка мультимедиа также может отображать очередь медиафайлов, например, плейлист.
Рисунок 1. Примеры реализации медиа-карт.
Как реализованы медиа-карты в AAOS?
Группы ViewGroup, отображающие информацию о медиафайлах, отслеживают обновления LiveData из модели данных библиотеки car-media-common
, PlaybackViewModel
, для заполнения ViewGroup. Каждое обновление LiveData соответствует подмножеству изменившихся данных о медиафайлах, например, MediaItemMetadata
, PlaybackStateWrapper
и MediaSource
.
Поскольку такой подход приводит к повторению кода (каждое клиентское приложение добавляет Observer для каждого фрагмента LiveData и многим аналогичным View назначаются обновленные данные), мы создали 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
отслеживается в защищённом методе и взаимодействует с представлениями, соответствующими полученным данным. Например, наблюдатель MediaItemMetadata
устанавливает заголовок для TextView
mTitle
и передаёт MediaItemMetadata.ArtworkRef
в компонент ImageBinder
обложки альбома mAlbumArtBinder
. Если метаданные равны null, представления скрыты. Подклассы контроллера могут при необходимости переопределить эту логику.
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, привязанная к фрагменту или активности, которая должна использоваться для восстановления содержимого карты памяти при изменении конфигурации (например, при переключении со светлой на тёмную тему при проезде через туннель). 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, содержащий список ранее воспроизведенных медиаисточников. Эти данные можно использовать для заполнения объекта 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
отображает элементы списка и необязательный заголовок. Макеты для элемента списка и заголовка могут быть переданы из клиентского приложения. Если в качестве headerResource задано Resources.ID_NULL
, заголовок не отображается. После передачи 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
.