김호쭈
DevForYou
김호쭈
전체 방문자
오늘
어제
  • 분류 전체보기 (321)
    • • 데이터베이스(DB) (9)
      • __SQL__ (9)
    • •알고리즘(Algorithm ) (117)
      • 문제풀이 (99)
      • 스터디 (14)
      • 알고리즘 팁 (4)
    • •Compter Science (57)
      • Operating System (25)
      • Computer Network (1)
      • Computer Vision (16)
      • Artificial Intelligence (14)
      • Software Technology (1)
    • • 독서 (36)
      • Design Pattern (24)
      • 객체지향의 사실과 오해 (1)
      • Object Oriented Software En.. (11)
    • • 개발 (26)
      • React (3)
      • node.js (6)
      • Django (11)
      • Spring boot (6)
    • • 개발Tip (4)
      • GitHub (0)
    • •프로젝트 (2)
      • 물물 (2)
    • •App (54)
      • 안드로이드 with Kotlin (50)
      • 코틀린(Kotiln) (4)
    • •회고 (8)
    • •취준일기 (3)
    • • 기타 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • ㄱ
  • Remote저장소
  • 로컬저장소
  • KMU_WINK
  • local저장소
  • GitHubDesktop
  • 깃허브데스크탑
  • 원격저장소

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
김호쭈

DevForYou

[안드로이드&코틀린] 뮤직플레이어#2, ExoPlayer로 음악재생하기, mapepr, SeekBar로 타임라인 구현하기, ExoPlayer 리스너
•App/안드로이드 with Kotlin

[안드로이드&코틀린] 뮤직플레이어#2, ExoPlayer로 음악재생하기, mapepr, SeekBar로 타임라인 구현하기, ExoPlayer 리스너

2022. 7. 13. 15:52
 

[안드로이드&코틀린] 뮤직플레이어#1, exoPlayer를 통해 노래 재생, 그룹 만들기, drawable id 재정의, mo

# 완성작  간단한 뮤직플레이어다. 간단하지만 어려웠다. ViewModel의 개념이 들어가면서 깊게 다루지는 않았지만 꽤나 헷갈리는게 많았다. api를 통해 받아온 노래들을 통해 앱을 구성했다.  레트

devforyou.tistory.com

 

# 완성작 

 

# TODO LIST

 저번 포스팅에서는 exoPlayer에 노래가 재생되겠끔하는 것, 재생목록에 뮤직 리스트가 보여지게 했다. 그러나 next나 prev버튼을 눌렀을때는 별 다른 동작을 하지 않기때문에 해당 행위에 대해서 동작을 정의해줘야 한다. 추가적으로 Model을 정의해 사용하기 때문에 그 흐름 순서에 대해서 잘 정리 하도록 하겠다. 

 

# PlayerModel 사용하기

 이게 정확히 ViewModel의 개념인지는 모르겠지만, 앱 내에서 사용되는 모델을 정리해서 사용하는 데이터 클래스다. 

package com.example.part3.musicplayer

data class PlayerModel(
    private val playMusicList : List<MusicModel> = emptyList(),
    var currentPosition : Int = -1,
    var isWatchingPlayListView : Boolean = true
){
    /*
        현재 뮤직모델이 재생중인지의 여부를 알기 위해서
        copy를 사용하여 깊은복사된 data class를 반환시켜줌
     */
    fun getAdapterModels() : List<MusicModel> {
        return this.playMusicList.mapIndexed { index, musicModel ->
            val newItem = musicModel.copy(
                isPlaying = index == currentPosition
            )
            newItem
        }
    }
}

 next와 prev를 위해 position을 구하는 함수들에 대해서는 순서대로 추가하도록 하겠다. PlayerModel 데이터 클래스를 만들고, 여기서 음악 리스트와 현재 재생중인 포지션, 음악목록을 보여지고 있는지를 관리하도록 한다.

// 전역객체 생성
private var model : PlayerModel = PlayerModel()

private fun getMusicsFromServer() {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://run.mocky.io")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    retrofit.create(RetrofitService::class.java).getMusics()
        .enqueue(object : Callback<MusicsDto>{
            override fun onResponse(call: Call<MusicsDto>, response: Response<MusicsDto>) {
                if(response.isSuccessful.not()) return

                // 맵퍼를 통해 모델을 만들고 map반복문을 사용하여 모델리스트를 최종적으로 반환
                response.body()?.let { musicsDto ->
                    model = musicsDto.mapper()
                    setMusicList(model.getAdapterModels())
                    listAdapter.submitList(model.getAdapterModels())
                    }
                }
            override fun onFailure(call: Call<MusicsDto>, t: Throwable) {
            }
        })
}

 

fun MusicsDto.mapper() : PlayerModel =
    PlayerModel(
        playMusicList = this.musics.mapIndexed { index, musicEntity ->
            musicEntity.mapper(index.toLong())
        }
    )

API 호출을 통해 데이터를 받아오면 전역에 선언된 model에 맵퍼를 통해서 할당해 준다. 

 

# Next, Prev 버튼 구현하기

다음 또는 이전 버튼이 눌리면, 다음노래가 뭔지를 찾아낸후 재생시키는 것이다. 다음 노래가 뭔지를 찾고, 그 노래의 인덱스를 exoPlayer에 알려준 후 재생시킨후, model(PlayerModel)에서도 currentMusicPosition을 업데이트 해준다. 그리고 나서 exoPlayer에 리스너를 추가해 음악이 바뀌었다는 것에 콜백함수를 등록하여 리사이클러뷰에 현재 재생중인 음악의 뒷 배경이 바뀌도록 해주면 된다.

 

## Next, Prev Button 리스너

private fun initPlayControlButtons(mBinding: FragmentPlayerBinding) {
    mBinding.playerControlImageView.setOnClickListener {
        //생략
    }

    mBinding.skipPreviousImageView.setOnClickListener {
        val prevMusic = model.prevMusic() ?: return@setOnClickListener
        playMusic(prevMusic)
    }

    mBinding.skipNextImageView.setOnClickListener {
        val nextMusic = model.nextMusic() ?: return@setOnClickListener
        playMusic(nextMusic)
    }
}

이렇게 prevMusic또는 nextMusic의 MusicModel을 찾아 낸후 playMusic이라는 함수를 만들어 인자로 넘겨준다.

private fun playMusic(musicModel: MusicModel) {
    model.updateCurrentPosition(musicModel)
    exoPlayer?.seekTo(model.currentPosition, 0)
    exoPlayer?.play()
}

playMusic은 다음과 같이 동작하는데, model에 udpateCurrentPosistion을 통해서 재생중인 노래의 인덱스를 관리할 수 있도록 한다.

 

## PlayerModel에 기능 추가

data class PlayerModel(
    private val playMusicList : List<MusicModel> = emptyList(),
    var currentPosition : Int = -1,
    var isWatchingPlayListView : Boolean = true
){
    /*
        현재 뮤직모델이 재생중인지의 여부를 알기 위해서
        copy를 사용하여 깊은복사된 data class를 반환시켜줌
     */

    fun getAdapterModels() : List<MusicModel> {
        return this.playMusicList.mapIndexed { index, musicModel ->
            val newItem = musicModel.copy(
                isPlaying = index == currentPosition
            )
            newItem
        }
    }

    fun updateCurrentPosition(musicModel: MusicModel) {
        currentPosition = playMusicList.indexOf(musicModel)
    }

    fun nextMusic() : MusicModel? {
        if(playMusicList.isEmpty()) return null
        currentPosition = if((currentPosition + 1) == playMusicList.size) 0 else currentPosition + 1
        return playMusicList[currentPosition]
    }

    fun prevMusic() : MusicModel? {
        if(playMusicList.isEmpty()) return null
        currentPosition = if((currentPosition - 1) < 0) playMusicList.size-1 else currentPosition -1
        return playMusicList[currentPosition]
    }

    fun currentMusicModel(): MusicModel? {
        if (playMusicList.isEmpty()) return null
        return playMusicList[currentPosition]

    }
}

프래그먼트에서 쓰인 함수들을 정의해준다. 

 

## EXOPLAYER

private fun initPlayer(mBinding: FragmentPlayerBinding) {
    // 플레이어 초기화
    context?.let {
        exoPlayer = SimpleExoPlayer.Builder(it).build()
    }
    mBinding.playerView.player = exoPlayer

    // 플레이어 리스너
    binding?.let{ binding ->
        exoPlayer?.addListener(object : Player.Listener {
            override fun onIsPlayingChanged(isPlaying: Boolean) {
                 if(isPlaying) {
                     // 재생상태이기에 퍼스 아이콘
                     binding.playerControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
                 }else {
                     binding.playerControlImageView.setImageResource(R.drawable.ic_baseline_play_48)
                 }
            }

            // item이 바뀌는 경우 리사이클러뷰 초기화 해줘야함 ( currentPositon )
            override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                super.onMediaItemTransition(mediaItem, reason)
                val newIndex = mediaItem?.mediaId ?: return
                model.currentPosition = newIndex.toInt()
                updatePlayerView(model.currentMusicModel())
                listAdapter.submitList(model.getAdapterModels())
            }

            // 재생 상태 버퍼링중 완료중 ..
            override fun onPlaybackStateChanged(playbackState: Int) {
                super.onPlaybackStateChanged(playbackState)
                updateSeek()
            }
        })
    }
}

onMediaItemTransition을 통해서 재생중인 노래가 변경됐음을 감지했을때 실행되는 콜백함수에 listAdapter에 submit을 통해 재생중인 노래의 배경색이 바뀌도록 해준다.

아래와 같은 동작이 된다.

 

## 재생UI 변경

private fun updatePlayerView(currentMusicModel: MusicModel?) {
    currentMusicModel ?: return

    // 메인뷰UI 바꿔주기
    binding?.let { mBinding ->
        mBinding.trackTextView.text = currentMusicModel.track
        mBinding.artistTextView.text = currentMusicModel.artist
        Glide.with(mBinding.coverImageView.context)
            .load(currentMusicModel.coverUrl)
            .into(mBinding.coverImageView)
    }
}

onMediaItemTransition 안에 위 함수를 정의해줌으로써, 노래가 바뀔때 메인뷰 UI도 업데이트 될 수 있도록 한다.

 

# SeekBar 연동구현하기

private fun initPlayer(mBinding: FragmentPlayerBinding) {
    //생략
    binding?.let{ binding ->
        exoPlayer?.addListener(object : Player.Listener {
            override fun onIsPlayingChanged(isPlaying: Boolean) {
                 //생략
            }

            // item이 바뀌는 경우 리사이클러뷰 초기화 해줘야함 ( currentPositon )
            override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                //생략
            }

            // 재생 상태 버퍼링중 완료중 ..
            override fun onPlaybackStateChanged(playbackState: Int) {
                super.onPlaybackStateChanged(playbackState)
                updateSeek()
            }
        })
    }
}

노래의 재생 상태 재생정지 재생중 재생완료 등등에 대한 상태를 player의 콜백 리스너를 통해서 받는다. 이후 updateSeek함수를 통해서 SeekBar의 UI를 조작해 줄 수 있도록 하자.

private fun updateSeek() {
    val player = exoPlayer ?:return
    //전체 길이
    val duration = if(player.duration >= 0 ) player.duration else 0
    val position = player.currentPosition
    
    updateSeekUi(duration,position)
    
    val state = player.playbackState
    // state가 재생중이 아닐때에 ui를 초기화 하지 않도록 하도록
    view?.removeCallbacks(updateSeekRunnable)

    // 재생중일때 실시간으로 SeekBar 변경하기 postDelayed를 통해서
    if( state != Player.STATE_IDLE && state != Player.STATE_ENDED) {
        view?.postDelayed(updateSeekRunnable,1000)
    }
}

postDelayed를 통해 1초마다 UI를 그려 줄 수 있도록 한다. Runnable을 작성해준다. 

private val updateSeekRunnable = Runnable {
    updateSeek()
}

다시 updateSeek함수를 재호출하게 하여 UI를 그려주도록 하면 된다.

private fun updateSeekUi(duration: Long, position: Long) {
    binding?.let { mBinding ->

        mBinding.playListSeekBar.max = (duration / 1000).toInt() //전체길이의 seekBar
        mBinding.playListSeekBar.progress = (position / 1000).toInt() //현재 진행상태
        mBinding.playerSeekBar.max = (duration / 1000).toInt()
        mBinding.playerSeekBar.progress = (position / 1000).toInt()

        mBinding.currentPlayTimeTextView.text = String.format("%02d:%02d",
        TimeUnit.MINUTES.convert(position,TimeUnit.MILLISECONDS),
            (position/1000) % 60)
        mBinding.totalPlayTimeTextView.text = String.format("%02d:%02d",
            TimeUnit.MINUTES.convert(duration,TimeUnit.MILLISECONDS),
            (duration/1000) % 60)
    }
}

UI를 그려주는 함수이다.

아래와 같이 SeekBar가 바뀌는 걸 볼 수 있다.

 

# 마무리 

 이번 강의를 보면서 공부하면서, 뭔가 구현되는 것들이 많고 분리되는 것도 많아서 어려움을 느꼈다. 그전에는 onCreated함수에 다 때려박게 코딩했는데.. 사실 안드로이드 공식문서에서는 가장 권장하지 않는 방법이 이었다. 그치만 시작부터 아키텍처를 사용하면서 의존성 분리를 해버리면 왜 그게 필요한지도 모르는데.. 지금 나는 딱 그런 분리에 필요성에 대해서 조금씩 느끼고 있었던 터라 이번 강의가 어렵지만 재밌었다. 그래도 완벽 100%이해는 되지 않았지만. 내 목표가 그게 아니니까...! 남은 강의도 완강 해야겠다.

저작자표시 (새창열림)

'•App > 안드로이드 with Kotlin' 카테고리의 다른 글

[안드로이드 코드랩/아키텍처-1] 뷰모델(ViewModel) 초간단 이해 및 사용하는 이유, MVVM 아키텍처  (0) 2022.08.13
[안드로이드 코드랩/생명주기] 생명주기와 로깅(Lifecycles and logging),Timber, Application클래스, 데이터바이딩(data binding)  (0) 2022.08.13
[안드로이드&코틀린] 뮤직플레이어#1, exoPlayer를 통해 노래 재생, 그룹 만들기, drawable id 재정의, model mapper만들기, 리사이클러뷰 어댑터  (0) 2022.07.12
[안드로이드&코틀린] 유튜브#2, 모션레이아웃 커스텀, ExoPlayer, 액티비티에서 프래그먼트 함수 가져오기, 안드로이드 onTouchEvent, 터치이벤트 분리하기  (0) 2022.07.07
[안드로이드&코틀린] 유튜브#1, 모션레이아웃(MotionLayout)을 통해 스와이프 구현, 레이아웃 스와이프, 유튜브 플레이어, 프래그먼트, 프레임레이아웃  (0) 2022.07.06
    '•App/안드로이드 with Kotlin' 카테고리의 다른 글
    • [안드로이드 코드랩/아키텍처-1] 뷰모델(ViewModel) 초간단 이해 및 사용하는 이유, MVVM 아키텍처
    • [안드로이드 코드랩/생명주기] 생명주기와 로깅(Lifecycles and logging),Timber, Application클래스, 데이터바이딩(data binding)
    • [안드로이드&코틀린] 뮤직플레이어#1, exoPlayer를 통해 노래 재생, 그룹 만들기, drawable id 재정의, model mapper만들기, 리사이클러뷰 어댑터
    • [안드로이드&코틀린] 유튜브#2, 모션레이아웃 커스텀, ExoPlayer, 액티비티에서 프래그먼트 함수 가져오기, 안드로이드 onTouchEvent, 터치이벤트 분리하기
    김호쭈
    김호쭈
    공부하고 정리하고 기록하기

    티스토리툴바