Реализация медиа-карты в AAOS

Медиа-карточка — это автономный ViewGroup, отображающий метаданные медиафайла, такие как название, обложка альбома и многое другое, а также элементы управления воспроизведением, такие как «Воспроизвести» и «Пауза» , «Пропустить» и даже пользовательские действия, предоставляемые сторонним медиаприложением. Медиа-карточка также может отображать очередь медиафайлов, например, плейлист.

Карта памяти

Карта памяти

Карта памяти

Рисунок 1. Примеры реализации карты памяти.

Как реализованы медиакарты в AAOS?

ViewGroups, отображающие информацию о медиафайлах, отслеживают обновления LiveData из общей модели данных библиотеки car-media-common , PlaybackViewModel , для заполнения ViewGroup. Каждое обновление LiveData соответствует подмножеству измененной информации о медиафайлах, такой как MediaItemMetadata , PlaybackStateWrapper и MediaSource .

Поскольку такой подход приводит к повторению кода (каждое клиентское приложение добавляет наблюдателей к каждому элементу LiveData, и многим похожим представлениям назначаются обновленные данные), мы создали PlaybackCardController .

PlaybackCardController

В библиотеку car-media-common добавлен класс PlaybackCardController , помогающий в создании медиакарточек. Это публичный класс, который создается с помощью ViewGroup ( mView ), PlaybackViewModel ( mDataModel ), PlaybackCardViewModel ( mViewModel ) и экземпляра MediaItemsRepository ( mItemsRepository ).

В функции setupController происходит поиск ViewGroup по ID для определенных представлений с помощью mView.findViewById(R.id.xxx) , после чего ViewGroup присваивается защищенным объектам 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, представления скрываются. Подклассы контроллера могут переопределить эту логику при необходимости.

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 или его подкласса.

Чтобы обеспечить наличие LifecycleOwner для наблюдателей LiveData, класс Controller следует создавать из фрагмента или активности.

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 информацией об очереди, в библиотеку car-media-common был добавлен класс PlaybackQueueController для упрощения этого процесса. Макет для каждого элемента в 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 соответственно.

Показать историю ранее воспроизведенных медиаисточников

В этом разделе вы узнаете, как отображать и показывать историю ранее воспроизведенных медиафайлов.

Получите список истории с помощью API PlaybackCardViewModel.

PlaybackCardViewModel предоставляет API LiveData под названием getHistoryList() для получения списка истории воспроизведения медиафайлов. Он возвращает объект LiveData, содержащий список воспроизведенных ранее медиафайлов. Эти данные можно использовать для заполнения объекта 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 с ID history_list . CarUiRecyclerView отображает элементы списка и необязательный заголовок. Оба макета для элементов списка и заголовка могут быть переданы из клиентского приложения. Если в качестве headerResource установлено значение Resources.ID_NULL , заголовок не отображается. После передачи 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 .