Um card de mídia é um ViewGroup independente que mostra metadados de mídia, como título, capa do álbum e muito mais, e controles de reprodução, como Reproduzir e Pausar, Pular, e até mesmo 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 mudou, 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 recebem os 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 capa do álbum
art ImageBinder mAlbumArtBinder. Se os metadados forem nulos, as visualizações serão ocultadas. 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 a MediaCardController. Em seguida, a 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
// ...
}
}
Instanciar o PlaybackCardController ou uma subclasse
A classe do controlador precisa ser instanciada de 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 se ocorrer uma mudança de configuração (como uma mudança 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 dos MediaModels para reprodução, de que o PlaybackViewModel e o MediaItemsRepository podem ser recuperados. Use o PlaybackCardViewModel para acompanhar o estado da fila, do histórico e do menu de overflow usando os 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 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 simplificar esse processo. O layout de cada item no CarUiRecyclerView é especificado pelo app cliente, bem como um layout de cabeçalho opcional. O app cliente também pode 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á contiver 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 que correspondam 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 reproduzidos anteriormente
Nesta seção, você vai aprender a mostrar e exibir o histórico de origens de mídia reproduzidas anteriormente.
Receber a lista de histórico com a API PlaybackCardViewModel
PlaybackCardViewModel fornece uma API LiveData chamada getHistoryList() para recuperar a lista de histórico de mídia. Ela retorna um LiveData que contém uma lista de MediaSources que foram reproduzidos 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;
}
}
Interface do histórico com PlaybackHistoryController
Use o novo PlaybackHistoryController para ajudar a preencher os dados do histórico em um CarUiRecyclerView. Os construtores e as principais funções dessa classe são os seguintes. O contêiner transmitido do app cliente precisa conter um CarUiRecyclerView com o ID history_list. O CarUiRecyclerView mostra os itens da lista e um cabeçalho opcional. Os layouts do item da lista e do cabeçalho podem ser transmitidos do app cliente. Se Resources.ID_NULL estiver definido como headerResource, o cabeçalho não será mostrado. Depois que o
PlaybackCardViewModel é transmitido para o controlador, ele monitora o
LiveData<List<MediaSource>> recuperado 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 que correspondam 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 construído no construtor do PlaybackCardController.