AAOS에서 미디어 카드 구현

미디어 카드는 제목, 앨범 아트 등 미디어 메타데이터를 표시하고 재생, 일시중지, 건너뛰기와 같은 재생 컨트롤을 표시하며 서드 파티 미디어 앱에서 제공하는 맞춤 작업까지 표시하는 독립형 ViewGroup입니다. 미디어 카드는 재생목록과 같은 미디어 항목의 현재 재생목록도 표시할 수 있습니다.

미디어 카드

미디어 카드

미디어 카드

그림 1. 미디어 카드 샘플 구현

AAOS에서 미디어 카드는 어떻게 구현되나요?

미디어 정보를 표시하는 ViewGroup은 car-media-common 라이브러리 데이터 모델인 PlaybackViewModel의 LiveData 업데이트를 관찰하여 ViewGroup을 채웁니다. 각 LiveData 업데이트는 변경된 미디어 정보의 하위 집합(예: MediaItemMetadata, PlaybackStateWrapper, MediaSource)에 해당합니다.

이 접근 방식으로는 반복 코드가 발생하므로 (각 클라이언트 앱이 각 LiveData 부분에 관찰자를 추가하고 유사한 여러 뷰에 업데이트된 데이터가 할당됨) PlaybackCardController를 만들었습니다.

PlaybackCardController

미디어 카드를 만드는 데 도움이 되도록 PlaybackCardControllercar-media-common 라이브러리에 추가되었습니다. 이 클래스는 ViewGroup (mView), PlaybackViewModel (mDataModel), PlaybackCardViewModel(mViewModel), MediaItemsRepository 인스턴스 (mItemsRepository)로 구성된 공개 클래스입니다.

setupController 함수에서 특정 뷰의 ViewGroup은 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);

         // ...
}

PlaybackViewModel의 각 LiveData 업데이트는 보호된 메서드에서 관찰되며 수신된 데이터와 관련된 뷰와 상호작용합니다. 예를 들어 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 확장

미디어 카드를 만들려는 클라이언트 앱은 각 LiveData 업데이트에서 처리하려는 추가 기능이 있는 경우 PlaybackCardController를 확장해야 합니다. AAOS의 기존 클라이언트는 이 패턴을 따릅니다. 먼저 PlaybackCardController 서브클래스(예: MediaCardController)를 만들어야 합니다. 그런 다음 MediaCardControllerPlaybackCardController의 클래스를 확장하는 정적 내부 빌더 클래스를 추가해야 합니다.

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 또는 서브클래스 인스턴스화

LiveData 관찰자의 LifecycleOwner를 보유하려면 Controller 클래스를 Fragment 또는 Activity에서 인스턴스화해야 합니다.

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

mViewModelPlaybackCardViewModel (또는 서브클래스)의 인스턴스입니다.

상태 저장을 위한 PlaybackCardViewModel

PlaybackCardViewModel는 Fragment 또는 Activity에 연결된 상태 저장 ViewModel로, 구성 변경이 발생할 때 (예: 사용자가 터널을 통과할 때 밝은 테마에서 어두운 테마로 전환) 미디어 카드의 콘텐츠를 재구성하는 데 사용해야 합니다. 기본 PlaybackCardViewModel는 재생을 위한 MediaModel 인스턴스의 저장을 처리하며, 여기에서 PlaybackViewModelMediaItemsRepository를 검색할 수 있습니다. PlaybackCardViewModel를 사용하여 제공된 getter 및 setter를 통해 현재 재생목록, 기록, 오버플로 메뉴의 상태를 추적합니다.

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는 MediaSource가 현재 재생목록을 지원하는지 감지하고 현재 재생목록의 MediaItemMetadata 객체 목록을 검색하는 LiveData API를 제공합니다. 이러한 API를 사용하여 RecyclerView 객체를 대기열 정보로 직접 채울 수 있지만, 이 프로세스를 간소화하기 위해 car-media-common 라이브러리에 PlaybackQueueController 클래스가 추가되었습니다. CarUiRecyclerView의 각 항목의 레이아웃은 클라이언트 앱에서 선택적 헤더 레이아웃으로 지정합니다. 클라이언트 앱은 맞춤 UXR 제한을 사용하여 운전 상태 중에 대기열에 표시되는 항목 수를 제한하도록 선택할 수도 있습니다.

PlaybackQueueController 생성자와 setter는 다음 샘플에 나와 있습니다. queueResourceheaderResource 레이아웃 리소스는 queueResource의 경우 컨테이너에 이미 id queue_list가 있는 CarUiRecyclerView가 포함되어 있고 headerResource의 경우 대기열에 헤더가 없는 경우 Resources.ID_NULL로 전달할 수 있습니다.

   /**
    * 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 내부 클래스에서 사용되는 뷰의 ID와 일치하는 표시할 뷰의 ID가 포함되어야 합니다.

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(또는 서브클래스)로 만든 미디어 카드에 큐를 표시하려면 PlaybackCardController 생성자에서 PlaybackViewModelMediaItemsRepository 인스턴스에 각각 mDataModelmItemsRepository를 사용하여 PlaybackQueueController를 구성하면 됩니다.

이전에 재생한 MediaSources 기록 표시

이 섹션에서는 이전에 재생된 미디어 소스의 기록을 표시하는 방법을 알아봅니다.

PlaybackCardViewModel API를 사용하여 기록 목록 가져오기

PlaybackCardViewModel는 미디어 기록 목록을 가져오는 getHistoryList()라는 LiveData API를 제공합니다. 이전에 재생된 MediaSource 목록이 포함된 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를 사용한 표시 경로 UI

PlaybackHistoryController를 사용하여 CarUiRecyclerView에 기록 데이터를 채웁니다. 이 클래스의 생성자와 기본 함수는 다음과 같습니다. 클라이언트 앱에서 전달된 컨테이너에는 ID가 history_listCarUiRecyclerView가 포함되어야 합니다. CarUiRecyclerView는 목록 항목과 선택적 헤더를 표시합니다. 목록 항목의 레이아웃과 헤더는 모두 클라이언트 앱에서 전달할 수 있습니다. Resources.ID_NULL가 headerResource로 설정된 경우 헤더가 표시되지 않습니다. PlaybackCardViewModel가 컨트롤러에 전달되면 playbackCardViewModel.getHistoryList()에서 가져온 LiveData<List<MediaSource>>를 모니터링합니다.

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 내부 클래스에서 사용되는 뷰의 ID와 일치하는 표시할 뷰의 ID가 포함되어야 합니다.

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 (또는 서브클래스)로 만든 미디어 카드에 기록 목록을 표시하려면 PlaybackCardController의 생성자에서 PlaybackHistoryController를 생성할 수 있습니다.