AAOS에서 미디어 카드 구현

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

미디어 카드

미디어 카드

미디어 카드

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

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

미디어 정보를 표시하는 ViewGroup은 car-media-common 라이브러리 data 모델(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의 기존 클라이언트는 이 패턴을 따릅니다. 먼저 MediaCardController과 같은 PlaybackCardController 서브클래스를 만들어야 합니다. 다음으로 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가 있도록 컨트롤러 클래스는 프래그먼트나 활동에서 인스턴스화해야 합니다.

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

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

상태를 저장하는 PlaybackCardViewModel

PlaybackCardViewModel는 프래그먼트 또는 활동에 연결된 상태 저장 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 생성자와 설정자는 다음 샘플에 나와 있습니다. 전자의 경우 컨테이너에 이미 id queue_list이 있는 CarUiRecyclerView이 포함되어 있고 후자의 경우 대기열에 헤더가 없는 경우 queueResourceheaderResource 레이아웃 리소스를 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(또는 하위 클래스)로 만든 미디어 카드에 대기열을 표시하려면 PlaybackViewModelMediaItemsRepository 인스턴스에 각각 mDataModelmItemsRepository를 사용하여 PlaybackCardController 생성자에서 PlaybackQueueController를 구성하면 됩니다.

이전에 재생된 MediaSource의 기록 표시

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

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