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을 래핑하는 원시 표면입니다. |
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), ) }
시각적 출력 상태 홀더
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.videoSizeDp) // 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.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 상태 홀더 클래스는 다음을 실행합니다.
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에 대해 학습하지 않아도 되도록 지원합니다.