media3-ui-compose 라이브러리는 Jetpack Compose에서 미디어 UI를 빌드하기 위한 기본 구성요소를 제공합니다. media3-ui-compose-material3 라이브러리에서 제공하는 것보다 더 많은 맞춤설정이 필요한 개발자를 위해 설계되었습니다. 이 페이지에서는 핵심 구성요소와 상태 홀더를 사용하여 맞춤 미디어 플레이어 UI를 만드는 방법을 설명합니다.
Material3 및 맞춤 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 객체를 활용하여 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 상태의 경우 라이브러리는 버튼과 유사한 컴포저블에서 소비될 가능성이 가장 높다고 가정합니다.
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가 됩니다.
흐름은 어디에 있나요?
많은 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 상태 홀더 클래스는 다음 작업을 실행합니다.
Player객체를 가져옵니다.- 코루틴을 사용하여
Player를 구독합니다. 자세한 내용은Player.listen을 참고하세요. - 내부 상태를 업데이트하여 특정
Player.Events에 응답합니다. - 적절한
Player업데이트로 변환될 비즈니스 로직 명령어를 허용합니다. - UI 트리 전체에서 여러 위치에 만들 수 있으며 항상 플레이어 상태의 일관된 뷰를 유지합니다.
- 컴포저블에서 소비하여 변경사항에 동적으로 응답할 수 있는 Compose
State필드를 노출합니다. - 컴포지션 간에 인스턴스를 기억하기 위한
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 fun인 Player.listen
를 사용하여 이를 포착하면 됩니다. 다양한 UI 상태의 Media3 구현을 사용하면 최종 개발자가 Player.Events에 관해 학습하는 데 신경 쓰지 않아도 됩니다.