Triển khai thẻ nội dung nghe nhìn trong AAOS

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 như tiêu đề, ảnh bìa đĩa nhạc và nhiều nội dung khác, đồng thời hiển thị các nút điều khiển phát như PhátTạm dừng, Bỏ qua, và thậm chí cả các thao tác tuỳ chỉnh do ứng dụng đa phương tiện bên thứ ba cung cấp. Thẻ nội dung nghe nhìn cũng có thể hiển thị 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.

Thẻ nội dung nghe nhìn

Thẻ nội dung nghe nhìn

Thẻ nội dung nghe nhìn

Hình 1. Mẫu 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 nội dung nghe nhìn quan sát 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 vào ViewGroup. Mỗi bản cập nhật LiveData tương ứng với một tập hợp con của thông tin nội dung nghe nhìn đã thay đổi, chẳng hạn như MediaItemMetadata, PlaybackStateWrapperMediaSource.

Vì phương pháp này dẫn đến việc lặp lại mã (mỗi ứng dụng khách thêm Đối tượng quan sát trên từng phần của LiveData và nhiều Khung hiển thị tương tự được gán 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ợ 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à thực thể MediaItemsRepository (mItemsRepository).

Trong hàm setupController, ViewGroup được phân tích cú pháp cho một số khung hiển thị theo mã nhận dạng, với mView.findViewById(R.id.xxx) và được gán cho các đối tượng Khung hiển thị đượ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 được quan sát trong một phương thức được bảo vệ và thực hiện các tương tác với Khung hiển thị liên quan đến dữ liệu nhận được. Ví dụ: đối tượng quan sát trên MediaItemMetadata đặt tiêu đề trên mTitle TextView và chuyển MediaItemMetadata.ArtworkRef đến ảnh bìa album ImageBinder mAlbumArtBinder. Nếu siêu dữ liệu là rỗng, thì 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 khả năng mà họ muốn xử lý trong mỗi bản cập nhật LiveData. Các ứng dụng khách 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 nên thêm một lớp Builder tĩnh bên trong, lớp này 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
    // ...

  }
}

Khởi tạo PlaybackCardController hoặc một lớp con

Lớp Bộ điều khiển phải được khởi tạo từ một Mảnh hoặc Hoạt động để có LifecycleOwner cho các đối tượng quan sát 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 mà bạn nên dùng để tái cấu trúc nội dung của thẻ nội dung nghe nhìn nếu xảy ra 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 PlaybackViewModelMediaItemsRepository. Hãy 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 khác.

Hiện hàng đợi 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ể dùng trực tiếp các API này để điền đối tượng RecyclerView bằng thông tin hàng đợi, nhưng 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 mỗi 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 trong trạng thái lái xe với các hạn chế tuỳ chỉnh về UXR.

Hàm khởi tạo và các phương thức setter của PlaybackQueueController được minh hoạ trong mẫu sau. Bạn có thể truyền các tài nguyên bố cục queueResourceheaderResource dưới dạng Resources.ID_NULL nếu, trong trường hợp trước, vùng chứa đã chứa CarUiRecyclerViewid 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 mã nhận dạng cho các Khung hiển thị mà bạn muốn hiển thị 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 khởi tạo PlaybackCardController bằng cách dùng mDataModelmItemsRepository cho các thực thể PlaybackViewModelMediaItemsRepository 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 đó.

Tải danh sách nhật ký bằng API PlaybackCardViewModel

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. API này trả về một LiveData chứa danh sách các MediaSource đã phát trước đó. Bạn có thể dùng dữ liệu này để điền đối tượng CarUiRecyclerView. Tương tự như PlaybackQueueController, một lớp có tên là 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ý hiển thị bằng PlaybackHistoryController

Hãy dùng PlaybackHistoryController mới để giúp điền dữ liệu nhật ký vào CarUiRecyclerView. Sau đây là các hàm khởi tạo và hàm chính của lớp này. Vùng chứa được truyền từ ứng dụng khách phải chứa CarUiRecyclerView có mã nhận dạng history_list. CarUiRecyclerView hiển thị các mục trong danh sách và tiêu đề không bắt buộc. Bạn có thể truyền cả hai bố cục cho mục trong danh sách và tiêu đề 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 này 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à bạn muốn hiển thị 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 khởi tạo của PlaybackCardController.