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 đa phương tiện như tiêu đề, ảnh bìa đĩa nhạc, v.v., đồng thời hiện các bộ điều khiển chế độ 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 của bên thứ ba cung cấp. Thẻ nội dung đa phương tiện cũng có thể hiển thị hàng đợi mục nội dung nghe nhìn, chẳng hạn như danh sách phát.

Thẻ phương tiện

Thẻ phương tiện

Thẻ phương tiện

Hình 1. Triển khai mẫu Thẻ nội dung đa phương tiện.

Thẻ nội dung nghe nhìn được triển khai như thế nào trong AAOS?

Các ViewGroup hiển thị thông tin nội dung nghe nhìn sẽ quan sát nội dung 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 vào 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 đa phương tiện đã thay đổi, chẳng hạn như MediaItemMetadata, PlaybackStateWrapperMediaSource.

Vì phương pháp này dẫn đến mã lặp lại (mỗi ứng dụng khách thêm Đối tượng tiếp nhận dữ liệu trên mỗi phần LiveData và nhiều Chế độ xem 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ẻ phương tiện. Đây là một lớp công khai được tạo bằng một thực thể ViewGroup (mView), PlaybackViewModel (mDataModel), PlaybackCardViewModel (mViewModel) và MediaItemsRepository (mItemsRepository).

Trong hàm setupController, ViewGroup được phân tích cú pháp cho một số thành phần 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 lầ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 lượt tương tác với Khung hiển thị có liên quan đến dữ liệu đã nhận. Ví dụ: một trình quan sát 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à giá trị rỗng, thì Khung hiển thị sẽ bị ẩn. Các lớp con của Đơn vị kiểm soát 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 phải mở rộng PlaybackCardController nếu có thêm chức năng mà họ muốn xử lý trong mỗi lần cập nhật LiveData. Các ứng dụng hiện tại trong AAOS đều 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 Trình tạo bên trong tĩnh 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 bản sao PlaybackCardController hoặc một lớp con

Bạn nên tạo bản sao lớp Trình điều khiển từ một Mảnh hoặc Hoạt động để có một LifecycleOwner cho các trình 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 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 một đườ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. Sử dụng PlaybackCardViewModel để theo dõi trạng thái của hàng đợi, nhật ký và trình đơn mục bổ sung 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 thêm các trạng thái.

Hiển thị 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 đố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 đối tượng RecyclerView, nhưng chúng tôi đã thêm lớp PlaybackQueueController 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 do ứ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 hiển thị trong hàng đợi trong trạng thái lái xe bằng các quy định hạn chế UXR tuỳ chỉnh.

Hàm khởi tạo PlaybackQueueController và phương thức setter được hiển thị trong mẫu sau. Tài nguyên bố cục queueResourceheaderResource có thể được truyền dưới dạng Resources.ID_NULL nếu, trong trường hợp trước, vùng chứa đã chứa CarUiRecyclerView với 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 mã nhận dạng cho các Chế độ xem mà bạn muốn hiển thị tương ứng với các mã nhận dạng được sử 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 thị hàng đợi trong thẻ nội dung nghe nhìn được tạo bằng PlaybackCardController (hoặc lớp con), bạn có thể tạo PlaybackQueueController trong hàm khởi tạo PlaybackCardController bằng cách sử dụng mDataModelmItemsRepository cho thực thể PlaybackViewModelMediaItemsRepository tương ứng.

Hiển thị 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 thị và hiển thị nhật ký của các nguồn nội dung đa phương tiện đã phát trước đó.

Lấy danh sách bản phát hành 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. Phương thức này trả về một LiveData chứa danh sách 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 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 để điền dữ liệu nhật ký vào CarUiRecyclerView. Sau đây là các hàm khởi tạo và các chức năng 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 là 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. Ứng dụng khách có thể chuyển cả bố cục cho mục danh sách và tiêu đề. Nếu bạn đặt Resources.ID_NULL 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 của từng mục phải chứa mã nhận dạng của các Khung hiển thị mà mục đó muốn hiển thị tương ứng với 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 thị danh sách nhật ký trong thẻ nội dung nghe nhìn được tạo bằng PlaybackCardController (hoặc lớp con), bạn có thể tạo PlaybackHistoryController trong hàm khởi tạo của PlaybackCardController.