메시지 작성 도우미

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

Material3 및 맞춤 Compose 구성요소 혼합

media3-ui-compose-material3 라이브러리는 유연하도록 설계되었습니다. 대부분의 UI에 사전 빌드된 구성요소를 사용할 수 있지만, 더 많은 제어가 필요한 경우 단일 구성요소를 맞춤 구현으로 바꿀 수 있습니다. 이때 media3-ui-compose 라이브러리가 사용됩니다.

예를 들어 Material3 라이브러리에서 표준 PreviousButtonNextButton을 사용하고 싶지만 완전히 맞춤설정된 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에서 SurfaceViewTextureView를 래핑하는 원시 표면

UI 상태 홀더

스캐폴딩 구성요소가 요구사항을 충족하지 않는 경우 상태 객체를 직접 사용할 수도 있습니다. 일반적으로 리컴포지션 간에 UI 모양을 유지하려면 해당하는 remember 메서드를 사용하는 것이 좋습니다.

UI 상태 홀더의 유연성을 컴포저블과 비교하여 더 잘 이해하려면 Compose에서 상태를 관리하는 방법을 읽어보세요.

버튼 상태 홀더

일부 UI 상태의 경우 라이브러리에서는 버튼과 유사한 컴포저블에서 소비될 가능성이 가장 높다고 가정합니다.

상태 remember*State 유형
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState 상수
NextButtonState rememberNextButtonState 상수
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
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),
  )
}

시각적 출력 상태 홀더

PresentationStatePlayerSurface의 동영상 출력을 표시할 수 있는 시점 또는 자리표시자 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.videoSizeDp)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised 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.videoSizeDp를 사용하여 선택한 가로세로 비율로 Surface를 확장하고 (자세한 내용은 ContentScale 문서 참고) presentationState.coverSurface를 사용하여 Surface를 표시하기에 적절하지 않은 시기를 알 수 있습니다. 이 경우 불투명한 셔터를 노출 영역 위에 배치할 수 있으며, 노출 영역이 준비되면 셔터가 사라집니다. ContentFrame를 사용하면 셔터를 후행 람다로 맞춤설정할 수 있지만 기본적으로는 상위 컨테이너의 크기를 채우는 검은색 @Composable Box가 됩니다.

흐름은 어디에 있나요?

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

하지만 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(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

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

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