Una tarjeta de contenido multimedia es un ViewGroup independiente que muestra metadatos de contenido multimedia, como el título, la portada del álbum y mucho más, y muestra controles de reproducción, como Reproducir y Pausar,Omitir y hasta acciones personalizadas proporcionadas por la app de música de terceros. Una tarjeta de contenido multimedia también puede mostrar una fila de elementos multimedia, como una playlist.
Figura 1: Implementaciones de ejemplo de tarjetas multimedia
¿Cómo se implementan las tarjetas multimedia en AAOS?
Los ViewGroup que muestran información de medios observan las actualizaciones de LiveData desde el modelo de datos de la biblioteca car-media-common
, el PlaybackViewModel
, para completar el ViewGroup. Cada actualización de LiveData corresponde a un subconjunto de información multimedia que cambió, como MediaItemMetadata
, PlaybackStateWrapper
y MediaSource
.
Dado que este enfoque genera código repetido (cada app cliente agrega observadores en cada fragmento de LiveData y a muchas Views similares se les asignan los datos actualizados), creamos PlaybackCardController
.
PlaybackCardController
Se agregó PlaybackCardController
a la biblioteca de car-media-common
para ayudar a crear una tarjeta de medios. Esta es una clase pública que se construye con un ViewGroup (mView
), un PlaybackViewModel (mDataModel
), un PlaybackCardViewModel (mViewModel
) y una instancia de MediaItemsRepository
(mItemsRepository
).
En la función setupController
, el ViewGroup se analiza para detectar ciertas vistas por ID, con mView.findViewById(R.id.xxx)
y se asigna a objetos View 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 actualización de LiveData del PlaybackViewModel
se observa en un método protegido y realiza interacciones con las Views relevantes para los datos recibidos. Por ejemplo, un observador en MediaItemMetadata
establece el título en mTitle
TextView
y pasa MediaItemMetadata.ArtworkRef
a la portada del álbum ImageBinder
mAlbumArtBinder
. Si los metadatos son nulos, las vistas se ocultan. Las subclases del controlador pueden anular esta lógica si es necesario.
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);
}
}
Extiende PlaybackCardController
Las apps cliente que deseen crear una tarjeta de medios deben extender PlaybackCardController
si tienen alguna capacidad adicional que deseen controlar en cada actualización de LiveData. Los clientes existentes en AAOS siguen este patrón.
Primero, se debe crear una subclase de PlaybackCardController
, como MediaCardController
. A continuación, el MediaCardController
debe agregar una clase Builder interna estática que extienda la del 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
// ...
}
}
Crea una instancia de PlaybackCardController o una subclase
La clase Controller debe crearse a partir de un Fragment o una Activity para tener un LifecycleOwner para los observadores de LiveData.
mMediaCardController = (MediaCardController) new MediaCardController.Builder()
.setModels(mViewModel.getPlaybackViewModel(),
mViewModel,
mViewModel.getMediaItemsRepository())
.setViewGroup((ViewGroup) view)
.build();
mViewModel
es una instancia de PlaybackCardViewModel
(o una subclase).
PlaybackCardViewModel para guardar el estado
El PlaybackCardViewModel
es un ViewModel que guarda el estado y está vinculado a un Fragment o una Activity que se debe usar para reconstruir el contenido de la tarjeta de medios si se produce un cambio de configuración (como un cambio del tema claro al oscuro cuando un usuario conduce por un túnel). El PlaybackCardViewModel
predeterminado controla el almacenamiento de instancias de los MediaModel
para la reproducción, desde los cuales se pueden recuperar PlaybackViewModel
y MediaItemsRepository
. Usa PlaybackCardViewModel
para hacer un seguimiento del estado de la fila, el historial y el menú de desbordamiento a través de los métodos get y set proporcionados.
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;
}
}
Esta clase se puede extender si se deben hacer un seguimiento de estados adicionales.
Cómo mostrar una fila en una tarjeta de contenido multimedia
PlaybackViewModel
proporciona APIs de LiveData para detectar si MediaSource admite una cola y para recuperar la lista de objetos MediaItemMetadata
en la cola. Si bien estas APIs se pueden usar directamente para completar un objeto RecyclerView
con la información de la fila, se agregó una clase PlaybackQueueController
a la biblioteca car-media-common
para optimizar este proceso. La app cliente también especifica el diseño de cada elemento en CarUiRecyclerView
, así como un diseño de encabezado opcional. La app cliente también puede optar por limitar la cantidad de elementos que se muestran en la fila durante el estado de conducción con restricciones personalizadas de UXR.
En el siguiente ejemplo, se muestran el constructor y los métodos de configuración de PlaybackQueueController
. Los recursos de diseño queueResource
y headerResource
se pueden pasar como Resources.ID_NULL
si, en el primer caso, el contenedor ya contiene un CarUiRecyclerView
con id queue_list
y, en el segundo caso, la fila no tiene un encabezado.
/**
* 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;
}
El diseño de cada elemento de la fila debe contener los IDs de las vistas que desea mostrar y que corresponden a los que se usan en la clase 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 una fila en una tarjeta de medios creada con PlaybackCardController
(o una subclase), el PlaybackQueueController
se puede construir en el constructor PlaybackCardController
con mDataModel
y mItemsRepository
para las instancias PlaybackViewModel
y MediaItemsRepository
, respectivamente.
Mostrar el historial de los objetos MediaSource reproducidos anteriormente
En esta sección, aprenderás a mostrar y exponer el historial de las fuentes de contenido multimedia que se reprodujeron anteriormente.
Obtén el historial con la API de PlaybackCardViewModel
PlaybackCardViewModel
proporciona una API de LiveData llamada getHistoryList()
para recuperar la lista del historial de contenido multimedia. Devuelve un LiveData que contiene una lista de MediaSources que se reprodujeron antes. Estos datos se pueden usar para completar un objeto CarUiRecyclerView
. De manera similar a PlaybackQueueController
, se agregó una clase llamada PlaybackHistoryController
a la biblioteca car-media-common
para optimizar el proceso.
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;
}
}
IU del historial de la superficie con PlaybackHistoryController
Usa el nuevo PlaybackHistoryController
para ayudar a propagar los datos del historial en un CarUiRecyclerView
. Los constructores y las funciones principales de esta clase son los siguientes. El contenedor que se pasa desde la app cliente debe contener un CarUiRecyclerView
con el ID history_list
. El elemento CarUiRecyclerView
muestra los elementos de la lista y un encabezado opcional. Ambos diseños para el elemento de la lista y el encabezado se pueden pasar desde la app cliente. Si Resources.ID_NULL
se establece como headerResource, no se muestra el encabezado. Después de que se pasa el PlaybackCardViewModel
al controlador, este supervisa el 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() {
}
}
El diseño de cada elemento debe contener los IDs de las Views que desea mostrar y que corresponden a los que se usan en la clase 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 una lista de historial en una tarjeta de medios creada con PlaybackCardController
(o una subclase), se puede construir PlaybackHistoryController
en el constructor de PlaybackCardController
.