티스토리 뷰

1. 안드로이드 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin

이번에는 멜론 같은 안드로이드 뮤직 재생 앱을 만들어 보는 프로젝트를 진행해보았습니다. 서버 에서 음악 정보들을 받아와 재생 목록을 구성하고 재생 및 일시정지, 다음 곡, 이전 곡, 곡 위치 탐색 등의 기본적인 기능을 수행할 수 있도록 구현했습니다.

주요 기능

  • 음악 서버에서 음악 받아와 재생 목록 구성하기
  • 현재 재생 중인 음악 재생 목록에서 표시 (회색 배경)
  • 재생/일시정지, 이전 곡/다음 곡 재생 기능
  • seekBar 를 통한 음악 탐색 기능
  • 재생 목록 <-> 음악 뷰 전환 가능

사용 기술

  • Exoplayer
    • 커스텀 컨트롤러
    • playlist
  • androidx.constraintlayout.widget.Group

기타

  • SeekBar Custom
  • postDelayed, TimeUnit

결과 화면

최종 앱 스크린샷

2. 기본 레이아웃 구성

사용된 레이아웃 자원은 메인 화면, 플레이어 프레그먼트 그리고 리사이클러 뷰 홀더에 사용 될 음악 아이템 레이아웃 이렇게 3개로 정의해두었습니다.

[activity_main.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...>

    <FrameLayout
        android:id="@+id/fragment_container"
        ... />

</androidx.constraintlayout.widget.ConstraintLayout>

메인 레이아웃은 player_fragment 를 담을 FrameLayout 으로 가득차게 구성을 했는데 미래에 재사용성을 위해 이런 구성을 사용했습니다.


[fragment_player.xml : 뒷 배경 구성 하기]

    <View
        android:id="@+id/top_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/background"
        app:layout_constraintBottom_toTopOf="@id/bottom_background_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_weight="3" />

    <View
        android:id="@+id/bottom_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/top_background_view"
        app:layout_constraintVertical_weight="2" />

다음은 플레이어 프래그먼트입니다. 나눠진 배경의 경우 view 2개로 구성하여 위에 회색, 아래 흰색으로 layout_constraintVertical_weight 속성을 사용해 3:2 비율로 수직으로 채워지도록 설정해주어 위와 같이 만들어줍니다.

[fragment_player.xml : Group 사용 해보기]

    <androidx.constraintlayout.widget.Group
        android:id="@+id/player_view_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="track_text_view, artist_text_view,
        cover_image_card_view, bottom_background_view,
        player_seek_bar, play_time_text_view, total_time_text_view"
        tools:visibility="visible"
        android:visibility="gone"/>


    <androidx.constraintlayout.widget.Group
        android:id="@+id/play_list_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="play_list_recycler_view, title_text_view, play_list_seek_bar"
        android:visibility="visible"
        />

재생화면과 플레이어 뷰를 따로 나누지 않고 한 프레그먼트 안에서 그룹으로 묶어 플레이 리스트 전환 아이콘을 터치 할 때 관련된 뷰들만 보여주도록 구현했습니다.

[fragment_player.xml : 앨범 커버 이미지 CardView]

    <androidx.cardview.widget.CardView
        android:id="@+id/cover_image_card_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="36dp"
        android:layout_marginEnd="36dp"
        android:translationY="30dp"
        app:cardCornerRadius="5dp"
        app:cardElevation="10dp"
        app:layout_constraintBottom_toBottomOf="@id/top_background_view"
        app:layout_constraintDimensionRatio="H,1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <ImageView
            android:id="@+id/cover_image_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:background="@color/purple_200" />
    </androidx.cardview.widget.CardView>

앨범 커버 이미지 영역을 위해 카드뷰를 사용하여 둥근 모양, 바닥에서 띄워진 그림자 효과 등을 이용할 수 있습니다. 다음 속성으로 cardElevation 바닥에서 띄워서 그림자 생성, cardCornerRadius 둥근 모양 형성이 가능합니다.

[fragment_player.xml : exoplayer2]

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:alpha="0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:use_controller="false" />

엑소 플레이어의 경우에는 컨트롤러를 사용할 수 있지만, 해당 프로젝트에서는 따로 커스텀 버튼을 만들어서 사용하도록 해주었습니다. (사실 컨트롤러를 사용하면 더욱 쉽게 구현이 가능할 수 있습니다.)

반응형

그 외로는 seekBar, 버튼(재생, 다음/이전 곡), 리사이클러 뷰에 사용될 아이템 레이아웃 등을 구현을 해주었습니다. (전체 코드는 저의 깃허브 저장소에서 확인가능합니다.)

2.1. Main activity

Fragment를 상속한 PlayerFragment를 구현한 뒤 메인 엑티비티의 프래그먼트 컨테이너에 할당해주어 뮤직 플레이어 프레그먼트가 최초 실행 시 보이도록 설정합니다.

class MainActivity : AppCompatActivity() {

    private var _viewBinding: ActivityMainBinding? = null
    private val viewBinding: ActivityMainBinding get() = requireNotNull(_viewBinding)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        supportFragmentManager.beginTransaction()
            .replace(viewBinding.fragmentContainer.id, PlayerFragment.newInstance())
            .commit()
    }
}

requireNotNull()을 사용해서 nullable이 제거된 바인딩을 사용할 수 있도록 get()을 가진 프로퍼티를 하나 정의해주었습니다.

[PlayerFragment.kt]

class PlayerFragment : Fragment(R.layout.fragment_player) {

    // 코드 생략...

  companion object {
        fun newInstance(): PlayerFragment {
            return PlayerFragment()
        }
    }
}

PlayerFragment를 직접 생성해서 사용할 수도 있지만, companion object로 newInstance 를 만드는 정적 매서드를 하나 추가해서 새로운 인스턴스를 공급 받는 형태로 구현해주었습니다.

2.2. RecyclerView (item_layout, Adapter, Model)

리사이클러뷰는 현재 까지 제 블로그에 올려진 앱들을 만들어 보면서 활용을 많이 하였기에 넘어갈 수도 있지만, 그래도 한 번 간단하게 집고 가도록 하겠습니다.

item_music.xml

뷰홀더에 사용될 아이템의 경우 사용 뮤직앱들과 비슷하게 앨범 커버, 타이틀, 아티스트로 이루어진 레이아웃으로 구성했습니다.

Model (data class)

data class MusicModel (
    val id: Long,
    val track: String,
    val streamUrl: String,
    val artist: String,
    val coverUrl: String,
    val isPlaying: Boolean = false
)

모델으로는 곡의 정보를 담고 있는 data class를 하나 정의해주었습니다. isPlaying 이라는 불 변수를 하나 추가해 현재 재생되고 있는지 상태를 나타내주었습니다. (이후 해당 상태로 아이템 뷰의 배경색을 결정합니다.)

RecyclerView Adapter

class MusicAdapter(private val callback: (MusicModel) -> Unit): ListAdapter<MusicModel, MusicAdapter.ViewHolder>(diffUtil) {

    inner class ViewHolder(private val itemMusicBinding: ItemMusicBinding):RecyclerView.ViewHolder(itemMusicBinding.root){
        fun bind(music: MusicModel){
            itemMusicBinding.itemArtistTextView.text = music.artist
            itemMusicBinding.itemTrackTextView.text = music.track

            Glide.with(itemMusicBinding.itemCoverImageView.context)
                .load(music.coverUrl)
                .into(itemMusicBinding.itemCoverImageView)

            // 재생 중에 따라
            if(music.isPlaying){
                // itemView 를 사용했는데 이건 리사이클러 뷰에서 뷰홀더(아이템 하나) 현재 아이템에 해당
                itemView.setBackgroundColor(Color.GRAY) // 재생중이면 배경 색을 회색
            }else{
                itemView.setBackgroundColor(Color.TRANSPARENT)
            }

            itemView.setOnClickListener {
                callback(music)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemMusicBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object{
        val diffUtil = object : DiffUtil.ItemCallback<MusicModel>(){
            override fun areItemsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean { // id 값만 비교
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean { // 내부 내용 비교
                return oldItem == newItem
            }
        }
    }
}

어뎁터는 리스트 어뎁터를 상속해 구현해주었고 생성자로 콜백함수를 받아 아이템 클릭시 해당 콜백을 실행하도록 구현해주었습니다.

앨범 커버 이미지 로드는 Glide 라이브러리를 활용해 구현해주었습니다.

뷰 홀더의 경우 inner class로 정의해주었고 안드로이드에서 권장하는 뷰 바인딩을 사용해주었습니다. RecyclerView.ViewHolder를 상속해 처음 생성 시 사용 뷰를 넘겨주어야 하기 때문에 바인딩의 루트 뷰를 넘겨주면 됩니다.

diffUtil을 통해서 아이템의 동일 여부 및 내용의 동일 여부를 판단하는데 해당 프로젝트에서는 이를 잘 활용하는데 이후 재생 되는 곡이 변경 될 때 isPlaying 이 변경된 리스트를 갱신하는데 새로운 객체를 할당해 옳바르게 재생 중인 음악이 표시되도록 data classcopy()를 활용합니다.

2.3. SeekBar customizing

    <SeekBar
        android:id="@+id/player_seek_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="50dp"
        android:layout_marginEnd="50dp"
        android:layout_marginBottom="30dp"
        android:maxHeight="4dp"
        android:minHeight="4dp"
        android:paddingStart="0dp"
        android:paddingEnd="0dp"
        android:progressDrawable="@drawable/player_seek_background"
        android:thumb="@drawable/player_seek_thumb"
        app:layout_constraintBottom_toTopOf="@id/player_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:progress="40" />

progressDrawable으로 프로그레스 표시를 커스터마이징 할 수 있습니다.

thumb으로 thumb를 커스터마이징 할 수 있습니다. (thumb는 음악 탐색 시 끌거나 당기거나 하는 그 부분(?) 입니다.)

[player_seek_background.xml]

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background"> <!-- 재정의 -->
        <shape>
            <corners android:radius="2dp" />
            <solid android:color="@color/seek_background" />
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="2dp" />
                <stroke
                    android:width="2dp"
                    android:color="@color/purple_200" />
                <solid android:color="@color/purple_200" />
            </shape>
        </clip>
    </item>
</layer-list>

background, progress를 재정의 해준 코드입니다.

[player_seek_thumb.xml]

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/purple_200" />
    <size
        android:width="4dp"
        android:height="4dp" />

</shape>

thumb를 재정의 해준 코드입니다. 다 기본적으로 보라색 한 줄 처럼(?) 표시되도록 하기 위해 위와 같이 정의해주었습니다.

플레이 리스트에서 표시할 하단 seekbar도 비슷하게 progressTint, thumbTint을 보라색을 주었고 clickable="false"을 주어 클릭할 수 없게 만들었습니다.

3. 음악 서버 (저작권 없는 음반, mocky)

음악의 경우에는 저작권에 민감하기 때문에 저작권이 없는 음악을 사용하였고 서버를 따로 구축하기 보다는 mocky를 사용하여 음악 정보 리스트를 가진 json 을 받아 이를 통해 음악 플레이리스트를 구성해주도록 했습니다.

3.1. Retrofit 사용 하기 (Dto, Entity, Service)

Retrofit 또한 마찬가지로 기본적인 서버 데이터를 받아올 때 많이 사용했었습니다! (이전 앱 만들기 프로젝트들 참고) 핵심 코드만 보고 넘어가겠습니다.

[MusicDto]

data class MusicDto (
    val musics: List<MusicEntity> // 서버에서 받아올 데이터
)

[MusicEntity]

data class MusicEntity(
    @SerializedName("track")val track: String,
    @SerializedName("streamUrl")val streamUrl: String,
    @SerializedName("artist")val artist: String,
    @SerializedName("coverUrl")val coverUrl: String
)

속성명이 동일하지만 @SerializedName 이 사용될 수도 있다라는 것을 알고가면 좋기 때문에 사용해주었습니다.

[MusicService]

interface MusicService {
    @GET("/v3/e4db045a-23a9-4b49-a3fc-78cf51f3f964")
    fun listMusics(): Call<MusicDto>
}

[PlayerFragment.kt : 서버에서 데이터 가져오기]

    private fun getVideoListFromServer() {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://run.mocky.io/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(MusicService::class.java)
            .also {
                it.listMusics()
                    .enqueue(object : Callback<MusicDto> {
                        override fun onResponse(
                            call: Call<MusicDto>,
                            response: Response<MusicDto>
                        ) {
                            response.body()?.let { musicDto ->
                                model = musicDto.mapper()

                                setMusicList(model.getAdapterModels())
                                adapter.submitList(model.getAdapterModels())
                            }
                        }

                        override fun onFailure(call: Call<MusicDto>, t: Throwable) {

                        }

                    })
            }
    }

베이스 url 을 지정, gson 컨버터 사용하도록 하여 Retrofit을 빌드하고 MusicService를 사용해 생성 및 응답 처리를 해줍니다.

이때 response.body()에서 musicDto를 받아오는데 mapper를 이용하여 바로 모델로 바꾸어 사용할 수 있도록 해주었습니다. 아래는 사용된 맵퍼 함수 입니다.

fun MusicEntity.mapper(id: Long): MusicModel =
    MusicModel(id = id, track, streamUrl, artist, coverUrl)

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

기존 데이터에 id만 새로 갱신해서 넣어주도록 해줍니다.

4. Exoplayer 사용 해보기

Exoplayer는 이전 유튜브 앱 만들어 보기 글 에서 한 번 다루어 본 적이 있습니다. 이번에는 SimpleExoPlayer를 사용해 보도록 하겠습니다.

private var player: SimpleExoPlayer? = null

SimpleExoPlayer 형식의 player 라는 변수를 하나 정의하고 null을 할당합니다.

        context?.let {
            player = SimpleExoPlayer.Builder(it).build()
        }

빌드 시 context 가 필요하기 때문에 context를 널 체크 해준뒤 이를 사용해서 빌드해줍니다.

binding.playerView.player = player

레이아웃에서 playerView 로 지정해준 아이디 (엑소 플레이어의 ui playerview)에 빌드해준 player를 대입해줍니다.

미디어 아이템 설정하기 : addMediaItems

    private fun setMusicList(modelList: List<MusicModel>) {
        player ?: return
        context?.let {
            player?.addMediaItems(modelList.map { musicModel ->
                MediaItem.Builder()
                    .setMediaId(musicModel.id.toString()) // 미디어 아이디를 musicModel id로
                    .setUri(musicModel.streamUrl)
                    .build()
                /*
                미디어 아이템에 2가지 태그 지정 가능
                미디어 id, 뷰에 태그 지정했듯 미디어 아이템에 태그 지정 가능
                 */
            })

            player?.prepare()
        }
    }

이전 프로젝트 에서는 Exoplayer를 사용할 때 재생할 동영상을 하나씩 설정해서 재생해주었는데 이번 뮤직 앱 프로젝트에서는 MediaItems를 추가해서 마치 플레이리스트를 가진 플레이어를 사용하는 것 처럼 사용해주기 위해 addMediaItems로 미디어 아이템을 추가해주었습니다.

음악 서버에서 음악 정보를 받아오면 이를 통해 MusicModel 리스트를 받아서 이를 player의 미디어 아이템으로 추가해주었습니다.

이때 MediaId로 뮤직 모델의 아이디를 설정해서 이후 구분 가능하게 사용할 수 있도록 설정해주었습니다.

setUri로 음악이 재생될 주소를 설정해주고 빌드하면 미디어 아이템이 생성됩니다.

이후 플레이어를 prepare를 통해 준비해줍니다.

4.1. Listener 추가 해보기

player?.addListener(object : Player.EventListener {
}

플레이어에는 addListener를 통해서 Player.EventListener 를 구현하는 익명 객체를 넣어주면 쉽게 구현할 수 있습니다.

이후 필요한 이벤트 리스너를 재정의 해서 구현해주면 되겠습니다.

플레이어가 재생 또는 일시 정지 될 때 : onIsPlayingChanged

            override fun onIsPlayingChanged(isPlaying: Boolean) {
                super.onIsPlayingChanged(isPlaying)

                if (isPlaying) {
                    binding.playControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
                } else {
                    binding.playControlImageView.setImageResource(R.drawable.ic_baseline_play_arrow_24)
                }
            }

플레이어가 재생 또는 일시정지 상태가 되면 재생/일시정지 버튼의 아이콘을 알맞게 보여주도록 해주었습니다.

미디어 아이템이 바뀔 때 : onMediaItemTransition

            override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                super.onMediaItemTransition(mediaItem, reason)

                val newIndex: String = mediaItem?.mediaId ?: return
                model.currentPosition = newIndex.toInt()
                adapter.submitList(model.getAdapterModels())

                // 리사이클러 뷰 스크롤 이동
                binding.playListRecyclerView.scrollToPosition(model.currentPosition)

                updatePlayerView(model.currentMusicModel())
            }

onMediaItemTransition 을 재정의 하여 미디어 아이템이 바뀔 때 마다 아이디로 지정했던 mediaId를 가져와서 리사이클러 뷰를 갱신(재생하고 있는 곡은 회색 배경으로 전환) 플레이어뷰를 갱신(이미지로드, 제목, 아티스트 텍스트 갱신) 합니다.

UX가 자연스러워 질 수 있게 다음/이전 곡 버튼을 통해 곡을 이동하면 리사이클러 뷰의 스크롤 포지션도 변경해서 재생 중인 곡으로 이동하도록 해주었습니다.

재생, 재생완료, 버퍼링 등의 상태 변화 시 : inPlaybackStateChanged

            override fun onPlaybackStateChanged(state: Int) {
                super.onPlaybackStateChanged(state)

                updateSeek()
            }

재생 중일 때 seekBar의 잔량과 상태를 갱신해주기 위해서 해당 체인지 리스너를 재정의해주었습니다.

    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) // 1초에 한번씩 실행
        }

    }

updateSeek 에서는 음악 길이 정보를 통해서 seekBar ui를 업데이트하고 1초 마다 updateSeekRunnable 을 실행합니다. updateSeekRunnable은 단지 updateSeek()을 호출합니다.

그래서 재생 중이면 해당 1초마다 seekBar를 갱신해서 여타 뮤집앱 처럼 탐색바가 갱신되도록 해주는 것 이지요. 해당 Runnable이 겹쳐 동작하지 않도록 updateSeek()을 타면 removeCallbacks를 통해 기존에 있던 Runnable을 제거해줍니다.

TimeUnit Convert

    private fun updateSeekUi(duration: Long, position: Long) {
        binding.playListSeekBar.max = (duration / 1000).toInt() // 총 길이를 설정. 1000으로 나눠 작게
        binding.playListSeekBar.progress = (position / 1000).toInt() // 동일하게 1000으로 나눠 작게

        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 // 분 단위를 제외한 초
        )
    }

updateSeekUi() 에서는 전체 길이와 현재 위치로 식바를 갱신합니다. maxprogress 를 원래 위치 값에 1000을 나눠 너무 큰 값이 되지 않게 해줍니다.

이후 시간을 표시하는 텍스트는 String.format() 매서드를 통해 남은 시간 총 시간을 분:초 형식으로 0을 채운 2자리수로 나타내주도록 합니다.

이때 java.utilTimeUnit을 사용하여 밀리초를 분으로 변경해주고 남은 초를 계산해서 텍스트로 뿌려줄 수 있습니다.

4.2. seekBar로 탐색

       binding.playerSeekBar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener{
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
            }

            override fun onStopTrackingTouch(seekBar: SeekBar) {
                player?.seekTo(seekBar.progress * 1000L)
            }

        })

탐색 바를 통한 음악 탐색의 경우 setOnSeekBarChangeListener를 설정하여 터치를 멈출 때 즉 손을 뗄 때 해당 위치의 progress를 플레이어에 설정해서 구현해줍니다.

5. 생명주기를 통한 리소스 관리

의도하지 않은 작업 (백 그라운드로 나갔는데도 계속 재생 등) 및 자원 처리를 위해서 생명주기를 활용합니다.

    override fun onStop() {
        super.onStop()

        player?.pause()
        view?.removeCallbacks(updateSeekRunnable)
    }

사용자가 백그라운드로 나가거나 다른 앱을 사용하는 경우 재생을 멈추고 seekBar를 업데이트하는 콜백을 제거합니다.

    override fun onDestroy() {
        super.onDestroy()

        _binding = null
        player?.release()
        view?.removeCallbacks(updateSeekRunnable)
    }

앱이 완전히 종료되는 경우 바인딩 해제, 플레이어 해제, 콜백 제거를 통한 처리를 진행합니다.

2021.07.13 - [Android/App] - [Android] Youtube 앱 만들어보기 (ExoPlayer, MotionLayout)

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함