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 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 상태 홀더 클래스는 다음을 실행합니다.
Player을 사용합니다.- 코루틴을 사용하여
Player를 구독합니다. 자세한 내용은Player.listen를 참고하세요. - 내부 상태를 업데이트하여 특정
Player.Events에 응답합니다. - 적절한
Player업데이트로 변환될 비즈니스 로직 명령어를 허용합니다. - UI 트리 전체에서 여러 위치에 만들 수 있으며 항상 플레이어 상태의 일관된 뷰를 유지합니다.
- 컴포저블이 변경사항에 동적으로 응답하기 위해 사용할 수 있는 Compose
State필드를 노출합니다. - 컴포지션 간에 인스턴스를 기억하기 위한
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 fun인 Player.listen를 사용하여 이를 포착하면 됩니다. 다양한 UI 상태의 Media3 구현은 최종 개발자가 Player.Events에 대해 학습하지 않아도 되도록 지원합니다.