ใช้การ์ดสื่อใน AAOS

การ์ดสื่อคือ ViewGroup แบบสแตนด์อโลนที่แสดงข้อมูลเมตาสื่อ เช่น ชื่อ ภาพหน้าปกอัลบั้ม และอื่นๆ รวมถึงแสดงตัวควบคุมการเล่น เช่น เล่นและหยุดชั่วคราว ข้าม และแม้แต่การดำเนินการที่กำหนดเองซึ่งแอปสื่อของบุคคลที่สาม ระบุไว้ การ์ดสื่อยังแสดงคิวของรายการสื่อ เช่น เพลย์ลิสต์ ได้ด้วย

การ์ดสื่อ

การ์ดสื่อ

การ์ดสื่อ

รูปที่ 1 ตัวอย่างการใช้งานการ์ดสื่อ

มีการติดตั้งใช้งานการ์ดสื่อใน AAOS อย่างไร

ViewGroups ที่แสดงข้อมูลสื่อจะสังเกตการอัปเดต LiveData จากโมเดลcar-media-commonข้อมูลของไลบรารี PlaybackViewModel เพื่อป้อนข้อมูลลงใน ViewGroup การอัปเดต LiveData แต่ละครั้งจะสอดคล้องกับข้อมูลสื่อชุดย่อย ที่มีการเปลี่ยนแปลง เช่น MediaItemMetadata, PlaybackStateWrapper และ MediaSource

เนื่องจากวิธีนี้ทำให้เกิดโค้ดที่ซ้ำกัน (แอปไคลเอ็นต์แต่ละแอปจะเพิ่ม Observer ใน LiveData แต่ละรายการ และระบบจะกำหนดข้อมูลที่อัปเดตให้กับ View ที่คล้ายกันจำนวนมาก) เราจึงสร้าง PlaybackCardController ขึ้น

PlaybackCardController

เราได้เพิ่ม PlaybackCardController ลงในคลัง car-media-common เพื่อช่วยในการสร้างการ์ดสื่อ นี่คือคลาสสาธารณะที่สร้างขึ้นด้วย ViewGroup (mView), PlaybackViewModel (mDataModel), PlaybackCardViewModel (mViewModel) และอินสแตนซ์ MediaItemsRepository (mItemsRepository)

ในsetupControllerฟังก์ชัน ระบบจะแยกวิเคราะห์ ViewGroup สำหรับบาง View ตาม ID ด้วย mView.findViewById(R.id.xxx) และกำหนดให้กับออบเจ็กต์ View ที่ได้รับการปกป้อง

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);

         // ...
}

การอัปเดต LiveData แต่ละครั้งจาก PlaybackViewModel จะสังเกตได้ในเมธอดที่ได้รับการป้องกัน และโต้ตอบกับ View ที่เกี่ยวข้องกับข้อมูล ที่ได้รับ เช่น ผู้สังเกตการณ์ใน MediaItemMetadata ตั้งชื่อใน mTitle TextView และส่ง MediaItemMetadata.ArtworkRef ไปยังภาพปกอัลบั้ม ImageBinder mAlbumArtBinder หากข้อมูลเมตาเป็นค่าว่าง ระบบจะซ่อนยอดดู คลาสย่อยของ Controller สามารถลบล้างตรรกะนี้ได้หากจำเป็น

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);
        }
    }

ขยาย PlaybackCardController

แอปไคลเอ็นต์ที่ต้องการสร้างการ์ดสื่อควรขยาย PlaybackCardController หากมีความสามารถเพิ่มเติมที่ต้องการ จัดการในการอัปเดต LiveData แต่ละครั้ง ไคลเอ็นต์ที่มีอยู่แล้วใน AAOS จะใช้รูปแบบนี้ ก่อนอื่นควรสร้างPlaybackCardControllerคลาสย่อย เช่น MediaCardController จากนั้น MediaCardController ควรเพิ่มคลาส Builder ภายในแบบคงที่ซึ่งขยายคลาสของ 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
    // ...

  }
}

สร้างอินสแตนซ์ของ PlaybackCardController หรือคลาสย่อย

ควรสร้างอินสแตนซ์ของคลาส Controller จาก Fragment หรือ Activity เพื่อให้มี LifecycleOwner สำหรับผู้สังเกตการณ์ LiveData

mMediaCardController = (MediaCardController) new MediaCardController.Builder()
                    .setModels(mViewModel.getPlaybackViewModel(),
                            mViewModel,
                            mViewModel.getMediaItemsRepository())
                    .setViewGroup((ViewGroup) view)
                    .build();

mViewModel คืออินสแตนซ์ของ PlaybackCardViewModel (หรือคลาสย่อย)

PlaybackCardViewModel เพื่อบันทึกสถานะ

PlaybackCardViewModel เป็น ViewModel ที่บันทึกสถานะซึ่งเชื่อมโยงกับ Fragment หรือ Activity ที่ควรใช้เพื่อสร้างเนื้อหาของการ์ดสื่อขึ้นมาใหม่หากมีการ เปลี่ยนแปลงการกำหนดค่า (เช่น การเปลี่ยนจากธีมสว่างเป็นธีมมืดเมื่อ ผู้ใช้ขับรถผ่านอุโมงค์) PlaybackCardViewModel เริ่มต้นจะจัดการ การจัดเก็บอินสแตนซ์ของ MediaModel สำหรับการเล่น ซึ่งสามารถดึงข้อมูล PlaybackViewModel และ MediaItemsRepository ได้ ใช้ PlaybackCardViewModel เพื่อติดตามสถานะของคิว ประวัติ และเมนู การล้นผ่านตัวรับและตัวตั้งค่าที่ระบุ

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;
    }
}

คุณขยายคลาสนี้ได้หากต้องการติดตามรัฐเพิ่มเติม

แสดงคิวในการ์ดสื่อ

PlaybackViewModel มี API ของ LiveData เพื่อตรวจหาว่า MediaSource รองรับคิวหรือไม่ และเพื่อดึงรายการออบเจ็กต์ MediaItemMetadata ในคิว แม้ว่าคุณจะใช้ API เหล่านี้โดยตรงเพื่อป้อนข้อมูลคิวลงในRecyclerViewออบเจ็กต์ได้ แต่เราได้เพิ่มคลาส PlaybackQueueController ลงในไลบรารี car-media-common เพื่อเพิ่มประสิทธิภาพกระบวนการนี้ เลย์เอาต์ สำหรับแต่ละรายการใน CarUiRecyclerView จะได้รับการระบุโดยแอปไคลเอ็นต์เช่นกัน รวมถึงเลย์เอาต์ส่วนหัวที่ไม่บังคับ นอกจากนี้ แอปไคลเอ็นต์ยังเลือกจำกัดจำนวน รายการที่แสดงในคิวขณะอยู่ในสถานะการขับรถด้วยข้อจำกัด UXR ที่กำหนดเองได้ด้วย

ตัวสร้าง PlaybackQueueController และตัวตั้งค่าจะแสดงในตัวอย่างต่อไปนี้ ส่งทรัพยากรเลย์เอาต์ queueResource และ headerResource เป็น Resources.ID_NULL ได้หากในกรณีแรก คอนเทนเนอร์มี CarUiRecyclerView ที่มี id queue_list อยู่แล้ว และในกรณีหลัง คิวไม่มีส่วนหัว

   /**
    * 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;
    }

เลย์เอาต์ของแต่ละรายการในคิวควรมีรหัสของมุมมองที่ต้องการแสดงซึ่งสอดคล้องกับรหัสที่ใช้ในคลาสภายใน 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);

            // ...
}

หากต้องการแสดงคิวในการ์ดสื่อที่สร้างด้วย PlaybackCardController (หรือคลาสย่อย) คุณสามารถสร้าง PlaybackQueueController ใน ตัวสร้าง PlaybackCardController โดยใช้ mDataModel และ mItemsRepository สำหรับอินสแตนซ์ PlaybackViewModel และ MediaItemsRepository ตามลำดับ

แสดงประวัติของ MediaSource ที่เล่นก่อนหน้านี้

ในส่วนนี้ คุณจะได้ดูวิธีแสดงและแสดงประวัติแหล่งที่มาของสื่อที่เล่นก่อนหน้านี้

รับรายการประวัติด้วย PlaybackCardViewModel API

PlaybackCardViewModel มี LiveData API ชื่อ getHistoryList() สำหรับ ดึงรายการประวัติสื่อ โดยจะแสดง LiveData ที่มีรายการ MediaSource ที่เล่นไปแล้ว ระบบจะใช้ข้อมูลนี้เพื่อสร้างออบเจ็กต์ CarUiRecyclerView เราได้เพิ่มคลาสชื่อ PlaybackHistoryController ลงในไลบรารี car-media-common เพื่อปรับปรุงกระบวนการให้มีประสิทธิภาพยิ่งขึ้น ซึ่งคล้ายกับ PlaybackQueueController

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;
    }
}

UI ประวัติของ Surface พร้อม PlaybackHistoryController

ใช้PlaybackHistoryControllerใหม่เพื่อช่วยสร้างข้อมูลประวัติ ไปยังCarUiRecyclerView ตัวสร้างและฟังก์ชันหลักของคลาสนี้มีดังนี้ คอนเทนเนอร์ที่ส่งจากแอปไคลเอ็นต์ควรมีCarUiRecyclerViewที่มีรหัส history_list CarUiRecyclerView จะแสดงรายการและส่วนหัวที่ไม่บังคับ ส่งทั้งเลย์เอาต์สำหรับรายการในลิสต์ และส่วนหัวจากแอปไคลเอ็นต์ได้ หากตั้งค่า Resources.ID_NULL เป็น headerResource ระบบจะไม่แสดงส่วนหัว หลังจากส่ง PlaybackCardViewModel ไปยังตัวควบคุมแล้ว ตัวควบคุมจะตรวจสอบ LiveData<List<MediaSource>> ที่ดึงมาจาก 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() {
    }
}

เลย์เอาต์ของแต่ละรายการควรมีรหัสของมุมมองที่ต้องการแสดงซึ่งสอดคล้องกับรหัสที่ใช้ในคลาสภายใน 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);
// ...
}

หากต้องการแสดงรายการประวัติในการ์ดสื่อที่สร้างด้วย PlaybackCardController (หรือคลาสย่อย) คุณสามารถสร้าง PlaybackHistoryController ในตัวสร้างของ PlaybackCardController ได้