🔥 ExoPlayer와 Compose를 활용하여 미니 플레이어 구현하기
동영상 스트리밍 서비스 기능을 포함한 사이드 프로젝트를 구현하면서 미니 플레이어를 구현해보았다.
이번 글에서는 Compose의 큰 장점 중 하나인 애니메이션을 활용하여 미니 플레이어를 구현해본 과정을 정리해보려고 한다.
이전 글에서 작성한 ExoPlayerView를 그대로 사용하여 동영상 플레이어를 구현하였다.
이전 글 : https://velog.io/@pass/Android-ExoPlayer-%EB%8F%99%EC%98%81%EC%83%81-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1-With-Compose
미니 플레이어 구현의 최종 결과는 다음과 같다.
전체 화면 구성을 보면 동영상 플레이어와 아래 내용(제목, 프로필 등)으로 나눌 수 있다.
미니플레이어로 전환 시 실현될 애니메이션은 다음과 같다.
동영상 플레이어
내용
중요한 점은 동영상 플레이어는 줄어들면서 이동하고, 내용물은 사라지고 나타나게 해야 한다는 점이다.
val transition = updateTransition(targetState = isMinimized, label = "Transition")
// 전체 스크린 dp 정의
val fullScreenWidthDp = LocalConfiguration.current.screenWidthDp.dp
val fullScreenHeightDp = LocalConfiguration.current.screenHeightDp.dp
// 비디오 플레이어의 Y 좌표 정의 - paddingValues : MainScreen의 Scaffold paddingValues
val videoPlayerOffsetY by transition.animateDp(
transitionSpec = { spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow) },
label = "OffsetY"
) { state -> if (state) fullScreenHeightDp - paddingValues.calculateBottomPadding() - paddingValues.calculateTopPadding() else 0.dp }
// 비디오 플레이어의 크기 정의
val videoPlayerScale by transition.animateFloat(
transitionSpec = { spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow) },
label = "Scale"
) { state -> if (state) 0.3f else 1f }
data class Video(
val isMinimized: Boolean,
val videoTitle: String,
val videoUrl: String,
val userProfileURL: String,
val userName: String,
)
@Composable
fun VideoStreamingPlayer(
video: Video,
paddingValues: PaddingValues,
onCloseVideoPlayer: () -> Unit
) {
val context = LocalContext.current
var isMinimized by remember { mutableStateOf(video.isMinimized) }
var videoTitle by remember { mutableStateOf(video.videoTitle) }
var videoUrl by remember { mutableStateOf(video.videoUrl) }
var userProfileURL by remember { mutableStateOf(video.userProfileURL) }
var userName by remember { mutableStateOf(video.userName) }
// 비디오 상태 업데이트 (초기)
LaunchedEffect(Unit) {
videoUrl.value = video
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.zIndex(if (!isMinimized) 1f else 0f)
.background(MaterialTheme.colorScheme.background)
.offset { IntOffset(0, videoPlayerOffsetY.roundToPx()) }
.pointerInput(Unit) {
detectVerticalDragGestures { _, dragAmount ->
if (dragAmount > 0) {
onChangeMiniPlayer()
} else if (dragAmount < 0) {
onChangeDefaultPlayer()
}
}
}
.clickable { if (isMinimized) onChangeDefaultPlayer() }
) {
// 동영상 플레이어
ExoPlayerView(
context = context,
videoUri = videoUrl,
modifier = Modifier
.fillMaxWidth(videoPlayerScale)
.height(fullScreenWidthDp * videoPlayerScale * 0.66f)
.background(Color.Black)
.zIndex(1f)
)
if (!isMinimized) {
// 전체화면일 때, 기본 플레이어 내용 정의
} else {
// 미니 플레이어일 때, 내용 정의 'X' 버튼 포함
}
}
val showPlayerState by remember { mutableStateOf<Video?>(null) }
Scaffold(
topBar = { ... },
bottomBar = { ... }
) { paddingValues ->
...
if (showPlayerState != null) {
VideoStreamingPlayer(
video = showPlayerState,
paddingValues = paddingValues,
onCloseVideoPlayer = onCloseVideoPlayer
)
}
}
// 뒤로가기 클릭 이벤트
BackHandler(enabled = !isMinimized.value) {
// 기본 플레이어 일 때, 미니 플레이어로 전환
viewModel.processIntent(VideoStreamingIntent.OnChangeMiniPlayer)
isMinized.value = true
}
전체 코드는 길고 복잡할 수 있어 현재 글에는 작성하지 않았고, github 주소를 첨부하려고 한다.
실제 코드에서는 Clean Architecture + ViewModel + MVI + Orbit 을 사용하였기 때문에 위의 코드와는 조금씩 다를 수 있다.
VideoStreamingPlayer
https://github.com/JungWooGeon/Education/blob/main/presentation/src/main/java/com/pass/presentation/view/component/VideoStreamingPlayer.kt
Repository
https://github.com/JungWooGeon/Education