메시지 작성 도우미

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를 래핑하는 원시 화면입니다.

Player 컴포저블 맞춤설정

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

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

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

UI 상태 홀더

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

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

버튼 상태 홀더

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

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

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 는 오류 오버레이를 표시하는 데 사용할 수 있는 플레이어의 현재 오류 상태에 관한 정보를 제공합니다.

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.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가 됩니다.

흐름은 어디에 있나요?

많은 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(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.Events를 무기한 수신할 수 있는 suspend funPlayer.listen 를 사용하여 이를 포착하면 됩니다. 다양한 UI 상태의 Media3 구현을 사용하면 최종 개발자가 Player.Events에 관해 학습하는 데 신경 쓰지 않아도 됩니다.