Thẻ nội dung nghe nhìn là một ViewGroup độc lập, hiển thị siêu dữ liệu nội dung nghe nhìn (chẳng hạn như tiêu đề, ảnh bìa đĩa nhạc, v.v.) và các chế độ điều khiển phát (chẳng hạn như Phát và Tạm dừng, Bỏ qua) và thậm chí cả các thao tác tuỳ chỉnh do ứng dụng nội dung nghe nhìn bên thứ ba cung cấp. Thẻ nội dung nghe nhìn cũng có thể cho thấy một hàng đợi các mục nội dung nghe nhìn, chẳng hạn như danh sách phát.
Hình 1. Ví dụ về cách triển khai Thẻ nội dung nghe nhìn.
Thẻ nội dung nghe nhìn được triển khai như thế nào trong AAOS?
ViewGroup hiển thị thông tin về nội dung nghe nhìn sẽ theo dõi các bản cập nhật LiveData từ mô hình dữ liệu của thư viện car-media-common
, PlaybackViewModel
, để điền sẵn thông tin cho ViewGroup. Mỗi lần cập nhật LiveData tương ứng với một tập hợp con thông tin về nội dung nghe nhìn đã thay đổi, chẳng hạn như MediaItemMetadata
, PlaybackStateWrapper
và MediaSource
.
Vì phương pháp này dẫn đến việc lặp lại mã (mỗi ứng dụng khách đều thêm Đối tượng tiếp nhận dữ liệu vào từng phần của LiveData và nhiều Khung hiển thị tương tự được chỉ định dữ liệu đã cập nhật), nên chúng tôi đã tạo PlaybackCardController
.
PlaybackCardController
PlaybackCardController
đã được thêm vào thư viện car-media-common
để hỗ trợ việc tạo thẻ nội dung nghe nhìn. Đây là một lớp công khai được tạo bằng ViewGroup (mView
), PlaybackViewModel (mDataModel
), PlaybackCardViewModel (mViewModel
) và phiên bản MediaItemsRepository
(mItemsRepository
).
Trong hàm setupController
, ViewGroup được phân tích cú pháp cho một số khung hiển thị nhất định theo mã nhận dạng, với mView.findViewById(R.id.xxx)
và được chỉ định cho các đối tượng View được bảo vệ.
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);
// ...
}
Mỗi bản cập nhật LiveData từ PlaybackViewModel
đều được quan sát trong một phương thức được bảo vệ và thực hiện các lượt tương tác với các View liên quan đến dữ liệu nhận được. Ví dụ: một đối tượng theo dõi trên MediaItemMetadata
đặt tiêu đề trên mTitle
TextView
và truyền MediaItemMetadata.ArtworkRef
đến ảnh bìa đĩa nhạc ImageBinder
mAlbumArtBinder
. Nếu siêu dữ liệu là rỗng, thì các Khung hiển thị sẽ bị ẩn. Các lớp con của Bộ điều khiển có thể ghi đè logic này nếu cần.
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);
}
}
Mở rộng PlaybackCardController
Các ứng dụng khách muốn tạo thẻ nội dung nghe nhìn nên mở rộng PlaybackCardController
nếu có thêm chức năng mà chúng muốn xử lý trong mỗi lần cập nhật LiveData. Các ứng dụng hiện có trong AAOS tuân theo mẫu này.
Trước tiên, bạn nên tạo một lớp con PlaybackCardController
, chẳng hạn như MediaCardController
. Tiếp theo, MediaCardController
sẽ thêm một lớp Builder tĩnh bên trong, mở rộng lớp đó của 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
// ...
}
}
Tạo thực thể PlaybackCardController hoặc một lớp con
Bạn nên tạo thực thể cho lớp Controller từ một Mảnh hoặc Hoạt động để có LifecycleOwner cho các đối tượng tiếp nhận dữ liệu LiveData.
mMediaCardController = (MediaCardController) new MediaCardController.Builder()
.setModels(mViewModel.getPlaybackViewModel(),
mViewModel,
mViewModel.getMediaItemsRepository())
.setViewGroup((ViewGroup) view)
.build();
mViewModel
là một thực thể của PlaybackCardViewModel
(hoặc lớp con).
PlaybackCardViewModel để lưu trạng thái
PlaybackCardViewModel
là một ViewModel lưu trạng thái được liên kết với một Mảnh hoặc Hoạt động. ViewModel này nên được dùng để tạo lại nội dung của thẻ nội dung nghe nhìn nếu có thay đổi về cấu hình (chẳng hạn như chuyển từ giao diện sáng sang giao diện tối khi người dùng lái xe qua đường hầm). PlaybackCardViewModel
mặc định xử lý việc lưu trữ các thực thể của MediaModel
để phát, từ đó có thể truy xuất PlaybackViewModel
và MediaItemsRepository
. Sử dụng PlaybackCardViewModel
để theo dõi trạng thái của hàng đợi, nhật ký và trình đơn tràn thông qua các phương thức getter và setter được cung cấp.
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;
}
}
Bạn có thể mở rộng lớp này nếu cần theo dõi các trạng thái bổ sung.
Hiện danh sách phát trong thẻ nội dung nghe nhìn
PlaybackViewModel
cung cấp các API LiveData để phát hiện xem MediaSource có hỗ trợ hàng đợi hay không và truy xuất danh sách các đối tượng MediaItemMetadata
trong hàng đợi. Mặc dù bạn có thể sử dụng trực tiếp các API này để điền thông tin hàng đợi vào một đối tượng RecyclerView
, nhưng một lớp PlaybackQueueController
đã được thêm vào thư viện car-media-common
để đơn giản hoá quy trình này. Bố cục cho từng mục trong CarUiRecyclerView
được ứng dụng khách chỉ định cũng như bố cục Tiêu đề không bắt buộc. Ứng dụng khách cũng có thể chọn giới hạn số lượng mục xuất hiện trong hàng đợi ở trạng thái lái xe bằng các hạn chế UXR tuỳ chỉnh.
Hàm khởi tạo PlaybackQueueController
và các phương thức thiết lập được minh hoạ trong mẫu sau. Bạn có thể truyền tài nguyên bố cục queueResource
và headerResource
dưới dạng Resources.ID_NULL
nếu trong trường hợp trước, vùng chứa đã chứa một CarUiRecyclerView
có id queue_list
và trong trường hợp sau, hàng đợi không có Tiêu đề.
/**
* 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;
}
Bố cục cho mỗi mục trong hàng đợi phải chứa các mã nhận dạng cho các Thành phần hiển thị mà bạn muốn hiện tương ứng với các mã nhận dạng được dùng trong lớp bên trong 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);
// ...
}
Để hiện hàng đợi trong thẻ nội dung nghe nhìn được tạo bằng PlaybackCardController
(hoặc một lớp con), bạn có thể tạo PlaybackQueueController
trong hàm dựng PlaybackCardController
bằng cách dùng mDataModel
và mItemsRepository
cho các thực thể PlaybackViewModel
và MediaItemsRepository
tương ứng.
Hiện nhật ký của các MediaSource đã phát trước đó
Trong phần này, bạn sẽ tìm hiểu cách hiện và hiển thị nhật ký của các nguồn nội dung nghe nhìn đã phát trước đó.
Lấy danh sách nhật ký bằng PlaybackCardViewModel API
PlaybackCardViewModel
cung cấp một API LiveData có tên là getHistoryList()
để truy xuất danh sách nhật ký nội dung nghe nhìn. Phương thức này trả về một LiveData chứa danh sách MediaSource đã từng được phát. Bạn có thể dùng dữ liệu này để điền sẵn đối tượng CarUiRecyclerView
. Tương tự như PlaybackQueueController
, một lớp có tên PlaybackHistoryController
đã được thêm vào thư viện car-media-common
để đơn giản hoá quy trình.
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;
}
}
Giao diện người dùng nhật ký trên Surface bằng PlaybackHistoryController
Sử dụng PlaybackHistoryController
mới để giúp điền dữ liệu nhật ký vào CarUiRecyclerView
. Hàm khởi tạo và các hàm chính của lớp này như sau. Vùng chứa được truyền từ ứng dụng khách phải chứa một CarUiRecyclerView
có mã nhận dạng history_list
. CarUiRecyclerView
hiển thị các mục trong danh sách và một tiêu đề không bắt buộc. Cả bố cục cho mục danh sách và tiêu đề đều có thể được truyền từ ứng dụng khách. Nếu Resources.ID_NULL
được đặt làm headerResource, thì tiêu đề sẽ không xuất hiện. Sau khi PlaybackCardViewModel
được truyền vào bộ điều khiển, bộ điều khiển sẽ theo dõi LiveData<List<MediaSource>>
được truy xuất từ 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() {
}
}
Bố cục cho mỗi mục phải chứa mã nhận dạng cho các Khung hiển thị mà mục đó muốn hiện, tương ứng với các mã nhận dạng được dùng trong lớp bên trong 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);
// ...
}
Để hiện danh sách nhật ký trong thẻ nội dung nghe nhìn được tạo bằng PlaybackCardController
(hoặc một lớp con), bạn có thể tạo PlaybackHistoryController
trong hàm dựng của PlaybackCardController
.