הטמעת כרטיס מדיה ב-AAOS

כרטיס מדיה הוא ViewGroup עצמאי שמוצגים בו מטא-נתונים של מדיה, כמו השם, תמונת האלבום ועוד, וגם אמצעי בקרה על ההפעלה, כמו הפעלה, השהיה, דילוג ואפילו פעולות בהתאמה אישית שמספקת אפליקציית המדיה של הצד השלישי. בכרטיס מדיה יכולה להופיע גם תור של פריטי מדיה, כמו פלייליסט.

כרטיס מדיה

כרטיס מדיה

כרטיס מדיה

איור 1. הטמעות לדוגמה של כרטיסי מדיה.

איך כרטיסי מדיה מוטמעים ב-AAOS?

באמצעות ViewGroups אפשר לראות נתוני מדיה שצופים בעדכוני LiveData מודל נתונים של הספרייה car-media-common, ה-PlaybackViewModel, כדי לאכלס את ViewGroup. כל עדכון LiveData תואם לקבוצת משנה של פרטי מדיה שהשתנה, למשל MediaItemMetadata, PlaybackStateWrapper ו MediaSource.

כי הגישה הזו מובילה לקוד חוזר (כל אפליקציית לקוח מוסיפה צופים באמצעות לכל חלק מ-LiveData ולתצוגות דומות רבות מוקצים הנתונים המעודכנים), אנחנו יצר את PlaybackCardController.

PlaybackCardController

הפריט PlaybackCardController נוסף לספרייה car-media-common כדי כדי לעזור ביצירת כרטיס מדיה. זוהי מחלקה ציבורית הבנויה באמצעות ViewGroup (mView), PlaybackViewModel (mDataModel), הפעלותCardViewModel (mViewModel) ומופע MediaItemsRepository (mItemsRepository).

בפונקציה setupController, מתבצע ניתוח של ViewGroup לגבי תצוגות מסוימות לפי מזהה, עם mView.findViewById(R.id.xxx) ומוקצה לאובייקטים של תצוגה מוגנת.

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 מתבצע בשיטה מוגנת, ומבצע אינטראקציות עם התצוגות הרלוונטיות לנתונים שהתקבלו. לדוגמה, משתמש שמתבונן ב-MediaItemMetadata מגדיר את השם ב-mTitle TextView ומעביר את MediaItemMetadata.ArtworkRef לתמונת האלבום ImageBinder mAlbumArtBinder. אם המטא-נתונים הם null, התצוגות מוסתרות. אם צריך, ניתן לשנות את הלוגיקה הזו באמצעות מחלקות משנה של הבקר.

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
    // ...

  }
}

יצירת מופע של PlayCardController או מחלקה משנית

יש ליצור את סיווג נאמן המידע ממקטע או מפעילות לפי הסדר להיות בעלים של מחזור חיים בשביל הצופים ב-LiveData.

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

mViewModel הוא מופע של PlaybackCardViewModel (או של תת-הסוג).

PlayCardViewModel לשמירת מצב

ה-PlaybackCardViewModel הוא ViewModel בשמירת מצב שמקושר למקטע או פעילות שבה יש להשתמש כדי לשחזר את התוכן של כרטיס המדיה אם מתבצע שינוי של ההגדרות האישיות (למשל מעבר מעיצוב בהיר לעיצוב כהה כאשר כשמשתמש עובר דרך מנהרה). ברירת המחדל PlaybackCardViewModel מטפלת באחסון של המופעים של MediaModel להפעלה, שמהם אפשר לאחזר את PlaybackViewModel ו-MediaItemsRepository. אפשר להשתמש ב-PlaybackCardViewModel כדי לעקוב אחרי המצב של התור, ההיסטוריה ותפריט הoverflow באמצעות ה-getters וה-setters שסופקו.

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 מותאמות אישית.

ה-constructor והמגדירים של 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 ב-constructor של PlaybackCardController באמצעות mDataModel ו-mItemsRepository למופעים של PlaybackViewModel ו-MediaItemsRepository, בהתאמה.

הצגת ההיסטוריה של מקורות המדיה שהושמעו בעבר

בחלק הזה תלמדו איך להציג ולהציג היסטוריה של מקורות מדיה שהופעלו.

הצגת רשימת ההיסטוריה עם PlaybackCardViewModel API

PlaybackCardViewModel מספק ממשק API של LiveData שנקרא getHistoryList() כדי לאחזר את רשימת היסטוריית המדיה. היא מחזירה LiveData שמכיל רשימה של MediaSources שהופעלו בעבר. אפשר להשתמש בנתונים האלה כדי לאכלס אובייקט CarUiRecyclerView. דומה ל-PlaybackQueueController, כיתה בשם PlaybackHistoryController נוסף אל car-media-common כדי לייעל את התהליך.

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

הצגת היסטוריית הגלישה באמצעות PlayHistoryController

אפשר להשתמש ב-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 ב-constructor של PlaybackCardController.