Um card de mídia é um ViewGroup independente que mostra metadados de mídia, como o título, a capa do álbum e muito mais, e exibe controles de reprodução, como Reproduzir, Pausar, Pular e até ações personalizadas fornecidas pelo app de mídia de terceiros. Um card de mídia também pode mostrar uma fila de itens de mídia, como uma playlist.
Figura 1. Exemplos de implementação de cards de mídia.
Como os cards de mídia são implementados no AAOS?
Os ViewGroups que mostram informações de mídia observam as atualizações do LiveData do
modelo de dados da biblioteca car-media-common
, o PlaybackViewModel
, para preencher o
ViewGroup. Cada atualização do LiveData corresponde a um subconjunto de informações de mídia
que mudaram, como MediaItemMetadata
, PlaybackStateWrapper
e
MediaSource
.
Como essa abordagem leva a um código repetido (cada app cliente adiciona observadores em
cada parte do LiveData e muitas visualizações semelhantes são atribuídas aos dados atualizados), criamos
o PlaybackCardController
.
PlaybackCardController
O PlaybackCardController
foi adicionado à biblioteca car-media-common
para
ajudar na criação de um card de mídia. Essa é uma classe pública construída com
um ViewGroup (mView
), PlaybackViewModel (mDataModel
), PlaybackCardViewModel
(mViewModel
) e uma instância MediaItemsRepository
(mItemsRepository
).
Na função setupController
, o ViewGroup é analisado para determinadas visualizações por
ID, com mView.findViewById(R.id.xxx)
e atribuído a objetos de visualização protegidos.
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);
// ...
}
Cada atualização do LiveData do PlaybackViewModel
é observada em um método
protegido e realiza interações com as visualizações relevantes para os dados
recebidos. Por exemplo, um observador em MediaItemMetadata
define o título no
mTitle
TextView
e transmite o MediaItemMetadata.ArtworkRef
para a arte
do álbum ImageBinder
mAlbumArtBinder
. Se os metadados forem nulos, as visualizações serão
ocultas. As subclasses do controlador podem substituir essa lógica, se necessário.
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);
}
}
Estender o PlaybackCardController
Os apps clientes que quiserem criar um card de mídia precisam estender o
PlaybackCardController
se tiverem mais recursos que gostariam de
processar em cada atualização do LiveData. Os clientes atuais no AAOS seguem esse padrão.
Primeiro, uma subclasse PlaybackCardController
precisa ser criada, como
MediaCardController
. Em seguida, o MediaCardController
precisa adicionar uma classe Builder interna
estática que estenda a do 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
// ...
}
}
Instânciar o PlaybackCardController ou uma subclasse
A classe Controller precisa ser instanciada em um fragmento ou atividade para ter um LifecycleOwner para os observadores do LiveData.
mMediaCardController = (MediaCardController) new MediaCardController.Builder()
.setModels(mViewModel.getPlaybackViewModel(),
mViewModel,
mViewModel.getMediaItemsRepository())
.setViewGroup((ViewGroup) view)
.build();
mViewModel
é uma instância da PlaybackCardViewModel
(ou subclasse).
PlaybackCardViewModel para salvar o estado
O PlaybackCardViewModel
é um ViewModel de salvamento de estado vinculado a um fragmento ou
atividade que precisa ser usado para reconstruir o conteúdo do card de mídia caso ocorra uma
mudança de configuração, como a troca do tema claro para o escuro quando um
usuário passa por um túnel. O PlaybackCardViewModel
padrão processa o
armazenamento de instâncias de MediaModel
s para reprodução, de onde o
PlaybackViewModel
e o MediaItemsRepository
podem ser recuperados. Use o
PlaybackCardViewModel
para rastrear o estado da fila, do histórico e do menu de
overflow pelos getters e setters fornecidos.
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;
}
}
Essa classe pode ser estendida se outros estados precisarem ser rastreados.
Mostrar uma fila em um card de mídia
O PlaybackViewModel
fornece APIs do LiveData para detectar se o MediaSource
oferece suporte a uma fila e para recuperar a lista de objetos MediaItemMetadata
na
fila. Embora essas APIs possam ser usadas diretamente para preencher um objeto RecyclerView
com as informações da fila, uma classe PlaybackQueueController
foi
adicionada à biblioteca car-media-common
para agilizar esse processo. O layout
de cada item no CarUiRecyclerView
é especificado pelo app cliente e
também como um layout de cabeçalho opcional. O app cliente também pode optar por limitar o número de
itens mostrados na fila durante o estado de direção com restrições personalizadas de UXR.
O construtor e os setters PlaybackQueueController
são mostrados no exemplo
a seguir. Os recursos de layout queueResource
e headerResource
podem ser transmitidos
como Resources.ID_NULL
se, no primeiro caso, o contêiner já tiver um
CarUiRecyclerView
com id queue_list
e, no segundo caso, a fila
não tiver um cabeçalho.
/**
* 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;
}
O layout de cada item da fila precisa conter os IDs das visualizações que ele
quer mostrar e que correspondem aos usados na classe interna 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);
// ...
}
Para mostrar uma fila em um card de mídia criado com o PlaybackCardController
(ou uma subclasse), o PlaybackQueueController
pode ser construído no
construtor PlaybackCardController
usando mDataModel
e mItemsRepository
para as instâncias PlaybackViewModel
e MediaItemsRepository
, respectivamente.
Mostrar o histórico de MediaSources tocadas anteriormente
Nesta seção, você vai aprender a mostrar e exibir o histórico de fontes de mídia tocadas anteriormente.
Receber a lista de histórico com a API PlaybackCardViewModel
O PlaybackCardViewModel
fornece uma API LiveData chamada getHistoryList()
para
extrair a lista de histórico de mídia. Ele retorna um LiveData contendo uma lista de
MediaSources que foram tocadas antes. Esses dados podem ser usados para preencher
um objeto CarUiRecyclerView
. Semelhante ao PlaybackQueueController
, uma classe
chamada PlaybackHistoryController
foi adicionada à biblioteca car-media-common
para simplificar o processo.
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;
}
}
Mostrar a interface do histórico com PlaybackHistoryController
Use o novo PlaybackHistoryController
para preencher os dados históricos
em um CarUiRecyclerView
. Os construtores e as funções principais desta classe são
os seguintes. O contêiner transmitido pelo app cliente precisa conter um
CarUiRecyclerView
com o ID history_list
. O CarUiRecyclerView
mostra os itens da lista e um cabeçalho opcional. Os dois layouts do item de lista
e do cabeçalho podem ser transmitidos pelo app cliente. Se Resources.ID_NULL
for definido
como headerResource, o cabeçalho não será mostrado. Depois que o
PlaybackCardViewModel
é transmitido ao controlador, ele monitora o
LiveData<List<MediaSource>>
extraído de
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() {
}
}
O layout de cada item precisa conter os IDs das visualizações que ele quer
mostrar e que correspondem aos usados na classe interna 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);
// ...
}
Para mostrar uma lista de histórico em um card de mídia criado com o
PlaybackCardController
(ou uma subclasse), o PlaybackHistoryController
pode ser
criado no construtor do PlaybackCardController
.