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

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

כרטיס מדיה

כרטיס מדיה

כרטיס מדיה

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

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

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

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

PlaybackCardController

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

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

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

mViewModel הוא מופע של PlaybackCardViewModel (או של מחלקת משנה).

‫PlaybackCardViewModel to Save State

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

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 האלה כדי לאכלס אובייקט RecyclerViewPlaybackQueueController במידע על התור, אבל כדי לייעל את התהליך הזה, נוסף מחלקה PlaybackQueueController לספריית car-media-common. הפריסה של כל פריט ב-CarUiRecyclerView מוגדרת על ידי אפליקציית הלקוח, וגם פריסת כותרת אופציונלית. אפליקציית הלקוח יכולה גם להגביל את מספר הפריטים שמוצגים בתור במהלך מצב נהיגה, באמצעות הגבלות UXR מותאמות אישית.

בדוגמה הבאה מוצגים ה-constructor וה-setters של 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, בהתאמה.

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

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

קבלת רשימת היסטוריה באמצעות PlaybackCardViewModel API

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

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

ממשק המשתמש של היסטוריית הצפייה עם 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 inner class.

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.