Side Project - Music Streaming App
음악 리스트를 보여주고 선택한 음악을 재생할 수 있는 음악 스트리밍 앱
공부한 내용
- Group
재생목록 화면과 재생 화면을 구성하는 View들을 그룹으로 나누어서 Visible 값을 번갈아가며 적용해서 화면을 전환하는 듯한 효과를 주기 위해서 Group을 사용했다.
Group은 참조한 위젯들의 집합의 visibility를 컨트롤하는 클래스이다.
그룹에 속하는 위젯들의 id를 constraint_referenced_ids 속성에 ,로 분리해서 적어주면 한 그룹으로 관리할 수 있다.
<androidx.constraintlayout.widget.Group
android:id="@+id/playerViewGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="trackTextView, artistTextView, coverImageCardView, bottomBackgroundView, playerSeekBar, playTimeTextView, totalTimeTextView" />
<androidx.constraintlayout.widget.Group
android:id="@+id/playListViewGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="titleTextView, playListRecyclerView, playListSeekBar"/>
그룹에 visibility를 적용하면 constraint_referenced_ids에 참조한 위젯들의 visibility에도 모두 적용된다.
따라서 쉽게 그룹에 속한 View들을 숨기거나 보여줄 수 있다.
https://developer.android.com/reference/androidx/constraintlayout/widget/Group
- Exoplayer2
음악을 플레이하기 위해서 Exoplayer2를 사용했다.
플레이어의 상태가 변경될 때, 하단의 플레이어 컨트롤 버튼을 통해 이전 음악을 재생하거나 다음 음악을 재생할 때, 재생 화면에서 seekBar의 thumb 위치를 변경해서 재생 구간을 변경할 때, 그리고 현재 음악이 끝난 후 자동으로 다음 음악이 실행될 때에 맞춰서 UI와 플레이어를 셋팅해줘야 했다.
player?.addListener(object: Player.EventListener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
fragmentPlayerBinding.playControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
} else {
fragmentPlayerBinding.playControlImageView.setImageResource(R.drawable.ic_baseline_play_arrow_48)
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
val newIndex = mediaItem?.mediaId ?: return
model.currentPosition = newIndex.toInt()
updatePlayerView(model.getCurrentMusicModel())
playListAdapter.submitList(model.getAdapterModels())
}
override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state)
updateSeek()
}
})
}
- 플레이어의 상태가 변경되었을 때
플레이어가 재생된 상태와 그렇지 않은 상태로 플레이어 상태가 변화할 때마다 onIsPlayingChanged()함수가 호출된다.
isPlaying 값이 true이면 재생 중인 상태이고, false이면 반대이다.
따라서, 재생 중일 때는 하단의 플레이어 컨트롤의 버튼을 일시정지 이미지로 변경하고, 재생 중이 아닐 때에는 재생 이미지로 변경해주었다.
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
fragmentPlayerBinding.playControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
} else {
fragmentPlayerBinding.playControlImageView.setImageResource(R.drawable.ic_baseline_play_arrow_48)
}
}
- 하단의 플레이어 컨트롤 버튼을 통해 이전 음악을 재생하거나 다음 음악을 재생할 때 | 현재 음악이 끝난 후 자동으로 다음 음악이 실행될 때
이 경우에는 새롭게 재생된 음악 정보로 UI를 변경해줘야 한다.
플레이어가 플레이리스트 내의 새로운 미디어 아이템으로 변경되면 onMediaItemTransition() 콜백 함수가 호출된다.
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
val newIndex = mediaItem?.mediaId ?: return
model.currentPosition = newIndex.toInt()
updatePlayerView(model.getCurrentMusicModel())
playListAdapter.submitList(model.getAdapterModels())
}
현재 재생 중인 음악을 가리키는 currenPosition 값을 새 아이템의 아이디로 변경해주고, 재생 화면을 갱신 해주고, 재생목록 리스트에서 새 아이템이 재생 중임을 UI에 적용해주는 로직을 적용해주었다.
- 재생 화면에서 seekBar의 thumb 위치를 변경해서 재생 구간을 변경할 때
seek과 관련해서 상태가 변화하면 onPlaybackStateChanged() 콜백 함수가 호출된다.
Playback state는 4가지가 있다.
-
- Player.STATE_IDLE - 초기 상태이다. 플레이어가 멈추거나 재생이 실패했을 때 STATE_IDLE 상태가 된다.
- Player.STATE_BUFFERING - 플레이어가 current position에 있는 미디어 재생할 수 없는 상태이다. 로딩해야하는 더 많은 데이터가 필요할 때 STATE_BUFFERING 상태가 된다.
- Player.STATE_READY - 플레이어가 current position에 있는 미디어를 즉시 재생할 수 있는 상태이다.
- Player.STATE_ENDED - 플레이어가 모든 미디어 재생을 마친 상태이다.
override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state)
updateSeek()
}
seek이 변화하면 SeekBar의 progress와 재생 시간을 나타내는 TextView를 갱신해주었다.
음악이 재생될 때마다 1초씩 progress가 차오르고 TextView가 갱신되도록 Runnable을 사용해서 1초에 한 번씩 UI 업데이트가 이뤄지도록 하였다.
private val updateSeekRunnable = Runnable {
updateSeek()
}
private fun updateSeek() {
val player = this.player ?: return
val duration = if (player.duration >= 0) player.duration else 0
val position = player.currentPosition
updateSeekUi(duration, position)
val state = player.playbackState
view?.removeCallbacks(updateSeekRunnable)
if (state != Player.STATE_IDLE && state != Player.STATE_ENDED) {
view?.postDelayed(updateSeekRunnable, 1000)
}
}
private fun updateSeekUi(duration: Long, position: Long) {
binding?.let { binding ->
binding.playListSeekBar.max = (duration/1000).toInt()
binding.playListSeekBar.progress = (position/1000).toInt()
binding.playerSeekBar.max = (duration/1000).toInt()
binding.playerSeekBar.progress = (position/1000).toInt()
binding.playTimeTextView.text = String.format("%02d:%02d",
TimeUnit.MINUTES.convert(position, TimeUnit.MILLISECONDS),
(position/1000) % 60)
binding.totalTimeTextView.text = String.format("%02d:%02d",
TimeUnit.MINUTES.convert(duration, TimeUnit.MILLISECONDS),
(duration/1000) % 60)
}
}
https://exoplayer.dev/listening-to-player-events.html
'Android > Side Projects' 카테고리의 다른 글
음악 스트리밍 앱 개발하기 (1) | 2022.03.10 |
---|---|
Youtube 앱 개발하기 (0) | 2022.03.05 |
Youtube 앱 개발하기 (0) | 2022.03.03 |
Youtube 앱 개발하기 (0) | 2022.03.03 |
Youtube 앱 개발하기 (0) | 2022.03.01 |