Karta multimediów to samodzielna grupa ViewGroup, która wyświetla metadane multimediów, takie jak tytuł, okładka albumu itp., oraz elementy sterujące odtwarzaniem, takie jak Odtwórz, Wstrzymaj, Pomiń, a nawet działania niestandardowe udostępniane przez aplikację multimedialną innej firmy. Karta multimediów może też wyświetlać kolejkę elementów multimedialnych, takich jak playlista.
Rysunek 1. Przykładowe implementacje karty multimedialnej
Jak karty multimediów są implementowane w AAOS?
Grupy widoku, które wyświetlają informacje o multimediach, obserwują aktualizacje LiveData z modelu car-media-common
library data (PlaybackViewModel
), aby wypełnić dane w grupie widoku. Każda aktualizacja LiveData odpowiada podzbiorowi informacji o multimediach, które uległy zmianie, takich jak MediaItemMetadata
, PlaybackStateWrapper
i MediaSource
.
Takie podejście prowadzi do powtarzania kodu (każda aplikacja klienta dodaje obserwatorów do każdego elementu LiveData, a wiele podobnych widoków ma przypisane zaktualizowane dane), dlatego utworzyliśmy PlaybackCardController
.
PlaybackCardController
Do biblioteki car-media-common
został dodany element PlaybackCardController
, aby ułatwić tworzenie karty medialnej. To publiczna klasa utworzona z ViewGroup (mView
), PlaybackViewModel (mDataModel
), PlaybackCardViewModel (mViewModel
) i instancji MediaItemsRepository
(mItemsRepository
).
W funkcji setupController
grupa ViewGroup jest analizowana pod kątem określonych widoków za pomocą identyfikatora mView.findViewById(R.id.xxx)
i przypisywana do chronionych obiektów 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);
// ...
}
Każda aktualizacja LiveData z PlaybackViewModel
jest obserwowana w chronionej metodzie i wykonywana w ramach interakcji z widokami odpowiednimi do otrzymanych danych. Na przykład obserwator na MediaItemMetadata
ustawia tytuł w mTitle
TextView
i przekazuje MediaItemMetadata.ArtworkRef
do albumu ImageBinder
mAlbumArtBinder
. Jeśli metadane mają wartość null, widoki są ukryte. W razie potrzeby podklasy klasy Controller mogą zastąpić tę logikę.
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);
}
}
Rozszerzenie klasy PlaybackCardController
Aplikacje klienckie, które chcą tworzyć karty multimedialne, powinny rozszerzać klasę PlaybackCardController
, jeśli mają dodatkowe funkcje, które chcą obsługiwać w każdej aktualizacji LiveData. Obecni klienci w AAOS postępują zgodnie z tym wzorcem.
Najpierw należy utworzyć podklasę PlaybackCardController
, taką jak MediaCardController
. Następnie klasa MediaCardController
powinna dodać stałą wewnętrzną klasę Builder, która rozszerza klasę 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
// ...
}
}
Tworzenie wystąpienia klasy PlaybackCardController lub jej podklasy
Aby klasa Controller mogła mieć obiekt LifecycleOwner dla obserwatorów LiveData, należy utworzyć jej instancję z Fragmentu lub Activity.
mMediaCardController = (MediaCardController) new MediaCardController.Builder()
.setModels(mViewModel.getPlaybackViewModel(),
mViewModel,
mViewModel.getMediaItemsRepository())
.setViewGroup((ViewGroup) view)
.build();
mViewModel
jest wystąpieniem klasy PlaybackCardViewModel
(lub jej podklasy).
PlaybackCardViewModel do zapisywania stanu
PlaybackCardViewModel
to ViewModel zapisujący stan powiązany z Fragmentem lub Aktywnością, który powinien służyć do odtwarzania zawartości karty multimedialnej w przypadku zmiany konfiguracji (np. przejścia z motywu jasnego na ciemny, gdy użytkownik jedzie przez tunel). Domyślna usługa PlaybackCardViewModel
odpowiada za przechowywanie instancji MediaModel
na potrzeby odtwarzania, z których można pobrać PlaybackViewModel
i MediaItemsRepository
. Użyj funkcjiPlaybackCardViewModel
, aby śledzić stan kolejki, historii i menu przepełnienia za pomocą udostępnionych metod getter i 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;
}
}
Ta klasa może zostać rozszerzona, jeśli trzeba śledzić dodatkowe stany.
Wyświetlanie kolejki na karcie multimedialnej
Interfejs PlaybackViewModel
udostępnia interfejsy LiveData API, które umożliwiają wykrycie, czy MediaSource obsługuje kolejkę, oraz pobranie listy obiektów MediaItemMetadata
w tej kolejce. Chociaż tych interfejsów API można używać bezpośrednio do wypełniania obiektu RecyclerView
informacjami o kolejce, do biblioteki car-media-common
dodaliśmy klasę PlaybackQueueController
, aby usprawnić ten proces. Układ każdego elementu w CarUiRecyclerView
jest określany przez aplikację klienta, a także przez opcjonalny układ nagłówka. Aplikacja klienta może też ograniczyć liczbę elementów wyświetlanych w kolejce podczas jazdy za pomocą niestandardowych ograniczeń UXR.
Konstruktor i metody ustawiające PlaybackQueueController
są pokazane w tym przykładzie. Zasoby układu queueResource
i headerResource
mogą być przekazywane jako Resources.ID_NULL
, jeśli w pierwszym przypadku kontener zawiera już CarUiRecyclerView
z id queue_list
, a w drugim przypadku kolejka nie ma nagłówka.
/**
* 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;
}
Układ każdego elementu kolejki powinien zawierać identyfikatory widoków, które mają być wyświetlane, odpowiadające tym używanym w klasie wewnętrznej 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);
// ...
}
Aby wyświetlić kolejkę na karcie multimedialnej utworzonej za pomocą klasy PlaybackCardController
(lub jej podklasy), konstruktor PlaybackCardController
może tworzyć obiekt PlaybackQueueController
, używając do tego obiektu mDataModel
i mItemsRepository
odpowiednio dla instancji PlaybackViewModel
i MediaItemsRepository
.
wyświetlać historię wcześniej odtwarzanych źródeł multimediów.
Z tej sekcji dowiesz się, jak wyświetlać i wyświetlać historię wcześniej odtwarzanych źródeł multimediów.
Pobieranie listy historii za pomocą interfejsu PlaybackCardViewModel API
PlaybackCardViewModel
udostępnia interfejs LiveData API o nazwie getHistoryList()
, który służy do pobierania listy historii multimediów. Zwraca on dane LiveData zawierające listę źródeł multimediów, które były odtwarzane wcześniej. Tych danych można używać do wypełniania obiektu CarUiRecyclerView
. Aby usprawnić proces, do biblioteki car-media-common
dodano klasę PlaybackHistoryController
, która jest podobna do klasy PlaybackQueueController
.
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;
}
}
Interfejs historii Surface z PlaybackHistoryController
Użyj nowego elementu PlaybackHistoryController
, aby wypełnić dane historyczne w elementach CarUiRecyclerView
. Konstruktory i główne funkcje tej klasy są następujące: Kontener przekazany przez aplikację klienta powinien zawierać CarUiRecyclerView
o identyfikatorze history_list
. CarUiRecyclerView
wyświetla elementy listy i opcjonalny nagłówek. Z aplikacji klienta można przekazać oba układy elementu listy i nagłówka. Jeśli jako headerResource ustawiona jest wartość Resources.ID_NULL
, nagłówek nie jest wyświetlany. Gdy PlaybackCardViewModel
zostanie przekazany do kontrolera, ten będzie monitorować LiveData<List<MediaSource>>
pobrany z 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() {
}
}
Układ każdego elementu powinien zawierać identyfikatory widoków, które mają być wyświetlane, odpowiadające tym, które są używane w wewnętrznej klasie 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);
// ...
}
Aby wyświetlić listę historii na karcie multimedialnej utworzonej za pomocą klasy PlaybackCardController
(lub jej podklasy), możesz skonstruować obiekt PlaybackHistoryController
w konstruktorze klasy PlaybackCardController
.