메시지 작성 도우미

media3-ui-compose 라이브러리는 Jetpack Compose에서 미디어 UI를 빌드하기 위한 기본 구성요소를 제공합니다. media3-ui-compose-material3 라이브러리에서 제공하는 것보다 더 많은 맞춤설정이 필요한 개발자를 위해 설계되었습니다. 이 페이지에서는 핵심 구성요소와 상태 홀더를 사용하여 맞춤 미디어 플레이어 UI를 만드는 방법을 설명합니다.

Material 3 및 맞춤 Compose 구성요소 혼합

media3-ui-compose-material3 라이브러리는 유연하게 설계되었습니다. 대부분의 UI에 사전 빌드된 구성요소를 사용할 수 있지만 더 세부적인 제어가 필요한 경우 단일 구성요소를 커스텀 구현으로 교체할 수 있습니다. 이때 media3-ui-compose 라이브러리가 사용됩니다.

예를 들어 Material3 라이브러리의 표준 PreviousButton 및 NextButton를 사용하려고 하지만 완전히 맞춤설정된 PlayPauseButton가 필요하다고 가정해 보겠습니다. 핵심 media3-ui-compose 라이브러리의 PlayPauseButton를 사용하여 사전 빌드된 구성요소와 함께 배치하면 됩니다.

Row {
  // Use prebuilt component from the Media3 UI Compose Material3 library
  PreviousButton(player)
  // Use the scaffold component from Media3 UI Compose library
  PlayPauseButton(player) {
    // `this` is PlayPauseButtonState
    FilledTonalButton(
      onClick = {
        Log.d("PlayPauseButton", "Clicking on play-pause button")
        this.onClick()
      },
      enabled = this.isEnabled,
    ) {
      Icon(
        imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
        contentDescription = if (showPlay) "Play" else "Pause",
      )
    }
  }
  // Use prebuilt component from the Media3 UI Compose Material3 library
  NextButton(player)
}

사용 가능한 구성요소

media3-ui-compose 라이브러리는 일반적인 재생 컨트롤을 위한 사전 빌드된 컴포저블 집합을 제공합니다. 다음은 앱에서 직접 사용할 수 있는 몇 가지 구성요소입니다.

구성요소 설명
PlayPauseButton 재생과 일시중지 간에 전환하는 버튼의 상태 컨테이너입니다.
SeekBackButton 정의된 증분만큼 뒤로 탐색하는 버튼의 상태 컨테이너입니다.
SeekForwardButton 정의된 증분만큼 앞으로 탐색하는 버튼의 상태 컨테이너입니다.
NextButton 다음 미디어 항목을 찾는 버튼의 상태 컨테이너입니다.
PreviousButton 이전 미디어 항목을 찾는 버튼의 상태 컨테이너입니다.
RepeatButton 반복 모드를 순환하는 버튼의 상태 컨테이너입니다.
ShuffleButton 셔플 모드를 전환하는 버튼의 상태 컨테이너입니다.
MuteButton 플레이어를 음소거 및 음소거 해제하는 버튼의 상태 컨테이너입니다.
TimeText 플레이어 진행 상황을 표시하는 컴포저블의 상태 컨테이너입니다.
ContentFrame 가로세로 비율 관리, 크기 조절, 셔터를 처리하는 미디어 콘텐츠를 표시하는 노출 영역
PlayerSurface AndroidView에서 SurfaceView 및 TextureView를 래핑하는 원시 표면입니다.

Player 컴포저블 맞춤설정

media3-ui-compose-material3 라이브러리를 사용할 때 Player 컴포저블은 완전한 미디어 재생 환경을 제공합니다. 콘텐츠 슬롯에 자체 컴포저블을 제공하여 레이아웃을 맞춤설정하거나 PlayerDefaults 객체를 활용하여 UI의 특정 부분(예: TopControls, CenterControls, BottomControls, ErrorOverlay)을 맞춤설정할 수 있습니다.

그림 1은 Player 컴포저블이 슬롯을 배치하는 위치를 보여줍니다. 그림 2는 레이어를 그리는 순서를 보여줍니다.

슬롯을 표시하는 점선 윤곽선이 있는 동영상 플레이어 위에서 아래로: 상단 가장자리를 따라 있는 topControls, 중앙 위에 있는 errorOverlay, backSecondary, back, central, forward, forwardSecondary 버튼이 있는 중앙의 centerControls, 하단 근처의 subtitles, above, left, progressSlider, right, below 슬롯이 있는 하단 가장자리를 따라 있는 bottomControls ContentFrame은 그 뒤의 영역을 채웁니다.
그림 1. Player 컴포저블의 슬롯 중앙 및 하단 컨트롤은 PlayerDefaults.CenterControls 및 PlayerDefaults.BottomControls의 슬롯을 보여줍니다. topControls, above, below 슬롯은 기본적으로 비어 있습니다.
6개의 레이어가 쌓인 분해도입니다. 아래에서 위로: 1, PlayerSurface, 2, 셔터, 3, 아트워크, 4, 자막, 5, errorOverlay, 6, topControls, centerControls, bottomControls 레이어 1~4는 ContentFrame을 구성하고 6개 레이어 모두 Player를 구성합니다.
그림 2. Player 컴포저블의 레이어는 그려지는 순서대로 번호가 매겨집니다.

예를 들어 PlayerDefaults.CenterControls를 사용하여 중앙 재생/일시중지 버튼만 재정의하고 나머지 중앙 컨트롤은 그대로 둘 수 있습니다. 또한 topControls과 같은 매개변수에 완전히 맞춤설정된 컴포저블을 전달하고 bottomControls과 같은 다른 매개변수는 완전히 생략하여 기본값으로 둘 수 있습니다.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

UI 상태 홀더

스캐폴딩 구성요소 중 어느 것도 요구사항을 충족하지 않는 경우 상태 객체를 직접 사용할 수도 있습니다. 일반적으로 상응하는 remember 메서드를 사용하여 리컴포지션 간에 UI 디자인을 유지하는 것이 좋습니다.

컴포저블과 비교하여 UI 상태 홀더의 유연성을 사용하는 방법을 더 잘 이해하려면 Compose가 상태를 관리하는 방법을 참고하세요.

버튼 상태 홀더

일부 UI 상태의 경우 라이브러리는 버튼과 같은 컴포저블에서 사용할 가능성이 높다고 가정합니다.

주 remember*State 유형
PlayPauseButtonState rememberPlayPauseButtonState 2-전환
PreviousButtonState rememberPreviousButtonState 상수
NextButtonState rememberNextButtonState 상수
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-전환
PlaybackSpeedState rememberPlaybackSpeedState 메뉴 또는 N-Toggle

PlayPauseButtonState 사용 예시:

val state = rememberPlayPauseButtonState(player)

IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
  Icon(
    imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
    contentDescription =
      if (state.showPlay) stringResource(R.string.playpause_button_play)
      else stringResource(R.string.playpause_button_pause),
  )
}

시각적 출력 상태 홀더

CurrentMediaItemState는 현재 재생 중인 MediaItem에 관한 정보를 제공하고 PlaylistState는 플레이어에 설정된 MediaItems에 관한 정보를 노출합니다. 이는 커스텀 UI에 메타데이터 정보를 표시하는 데 유용합니다.

ErrorState는 플레이어의 현재 오류 상태에 관한 정보를 제공하며, 이 정보는 오류 오버레이를 표시하는 데 사용할 수 있습니다.

PresentationState는 PlayerSurface의 동영상 출력을 표시할 수 있거나 자리표시자 UI 요소로 가려야 하는 경우에 관한 정보를 보유합니다. ContentFrame 컴포저블은 가로세로 비율 처리와 아직 준비되지 않은 노출 영역에 셔터를 표시하는 처리를 결합합니다.

@Composable
fun ContentFrame(
  player: Player?,
  modifier: Modifier = Modifier,
  surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
  contentScale: ContentScale = ContentScale.Fit,
  keepContentOnReset: Boolean = false,
  shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) },
) {
  val presentationState = rememberPresentationState(player, keepContentOnReset)
  val scaledModifier =
    modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(player, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

여기서는 presentationState.videoAspectRatio를 사용하여 노출 영역을 선택한 가로세로 비율로 조정하고 (더 많은 유형은 콘텐츠 배율 참고) presentationState.coverSurface를 사용하여 노출 영역을 표시하기에 적절한 시점이 아닌 경우를 알 수 있습니다. 이 경우 노출 영역 위에 불투명 셔터를 배치할 수 있으며, 노출 영역이 준비되면 이 셔터가 사라집니다. ContentFrame를 사용하면 셔터를 후행 람다로 맞춤설정할 수 있지만 기본적으로 상위 컨테이너의 크기를 채우는 검은색 @Composable Box입니다.

Flows는 어디에 있나요?

많은 Android 개발자는 Kotlin Flow 객체를 사용하여 끊임없이 변화하는 UI 데이터를 수집하는 데 익숙합니다. 예를 들어 수명 주기 인식 방식으로 collect할 수 있는 Player.isPlaying 흐름을 찾고 있을 수 있습니다. 또는 Player.eventsFlow와 같은 것을 사용하여 원하는 방식으로 filter할 수 있는 Flow<Player.Events>를 제공할 수 있습니다.

하지만 Player UI 상태에 흐름을 사용하면 몇 가지 단점이 있습니다. 주요 우려사항 중 하나는 데이터 전송의 비동기적 특성입니다. Player.Event와 UI 측에서의 소비 간 지연 시간을 최대한 줄여 Player와 동기화되지 않은 UI 요소를 표시하지 않으려고 합니다.

기타 사항은 다음과 같습니다.

  • 모든 Player.Events가 있는 흐름은 단일 책임 원칙을 준수하지 않으며 각 소비자는 관련 이벤트를 필터링해야 합니다.
  • 각 Player.Event의 흐름을 만들려면 각 UI 요소에 대해 흐름을 결합(combine 사용)해야 합니다. Player.Event와 UI 요소 변경사항 간에는 다대다 매핑이 있습니다. combine를 사용해야 하면 UI가 잠재적으로 잘못된 상태가 될 수 있습니다.

맞춤 UI 상태 만들기

기존 UI 상태가 요구사항을 충족하지 않는 경우 맞춤 UI 상태를 추가할 수 있습니다. 기존 상태의 소스 코드를 확인하여 패턴을 복사합니다. 일반적인 UI 상태 홀더 클래스는 다음을 실행합니다.

  1. Player 객체를 사용합니다.
  2. 코루틴을 사용하여 Player를 구독합니다. 자세한 내용은 Player.listen를 참조하세요.
  3. 내부 상태를 업데이트하여 특정 Player.Events에 응답합니다.
  4. 적절한 Player 업데이트로 변환될 비즈니스 로직 명령어를 허용합니다.
  5. UI 트리의 여러 위치에서 만들 수 있으며 항상 플레이어 상태의 일관된 뷰를 유지합니다.
  6. 컴포저블에서 사용하여 변경사항에 동적으로 응답할 수 있는 Compose State 필드를 노출합니다.
  7. 컴포지션 간에 인스턴스를 기억하는 remember*State 함수가 함께 제공됩니다.

가입 방법:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A))
    private set

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe(): Nothing = player.listen { events ->
    if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
      someFieldValue = this.someField
      isEnabled = this.isCommandAvailable(COMMAND_ACTION_A)
    }
  }
}

자체 Player.Events에 반응하려면 Player.listen를 사용하여 포착하면 됩니다. 이는 코루틴 세계로 들어가 Player.Events를 무기한 수신 대기할 수 있는 suspend fun입니다. 다양한 UI 상태의 Media3 구현은 최종 개발자가 Player.Events에 관해 학습하는 데 신경 쓰지 않도록 도와줍니다.