안드로이드/앱개발(Android)

(Android) 음악스트리밍 앱

김염인 2022. 2. 20. 16:38

목차

  1. 인트로 (완성앱 & 구현 기능 소개)
  2. 재생화면 UI 구성하기
  3. 플레이리스트 UI 구성하기
  4. 음악 목록 API 만들기
  5. ExoPlayer를 이용하여 음악 재생하기 (1)
  6. ExoPlayer를 이용하여 음악 재생하기 (2)
  7. ExoPlayer를 이용하여 음악 재생하기 (3)
  8. ExoPlayer를 이용하여 음악 재생하기 (4)
  9. 아웃트로

결과화면

이 챕터를 통해 배우는 것

  • Exoplayer 사용하기 (2)
    • custom controller
    • Playlist 등
  • androidx.constraintlayout.widget.Group
  • Seekbar Custom 하기

ExoPlayer

  • Google이 Android SDK 와 별도로 배포되는 오픈소스 프로젝트
  • 오디오 및 동영상 재생 가능
  • 오디오 및 동영상 재생 관련 강력한 기능들 포함
  • 유튜브 앱에서 사용하는 라이브러리
  • https://exoplayer.dev/hello-world.html

음악 스트리밍 앱

Retrofit 을 이용하여 재생 목록을 받아와 구성함

재생 목록을 클릭하여 ExoPlayer 를 이용하여 음악을 재생할 수 있음.

이전, 다음 트랙 버튼을 눌러서 이전, 다음 음악으로 재생하고, ui 를 업데이트 할 수 있음.

PlayList 화면과 Player 화면 간의 전환을 할 수 있음.

Seekbar 를 custom 하여 원하는 UI 로 표시할 수 있음.


1. 재생화면, 플레이리스트 UI 구성하기

먼저 화면구성에 앞서 멜론과 같은 스트리밍 음악 앱의 구성의 경우 간단한 앱의 Layout구조를 생각해보면

1) 나의 음악 재생리스트에 내가 선택한 음악들이 나열 돼있다.

2) 특정 음악을 클릭하면 음악이 실행되고 왼쪽하단 햄버거버튼을 클릭할 시 그 음악의 커버이미지, 제목, 진행시간을 볼 수 있는 Layout이 열린다.

3) 1번과 2번을 햄버거 버튼을 통해 열고 닫을 수 있다.

 

이렇게 큰 구조를 생각하고 레이아웃을 만들어보았다.

MainActivity에 FrameLayout을 넣어서 FrameLayout에 Fragment를 쌓아주는 식으로 화면을 구성하였다.

package com.example.aop_part4_chaptor02

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

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

MainActivity는 supportFramgentManager를 통해 시작하자마자 replace로 Fragment를 넣어주고, playFragment라는 xml을 구성해준다.

playFragment.xml 완성 화면

위에 화면은 MainActivity FrameLayout에 쌓여있는 fragment이다. 볼수있듯이 화면두개가 겹쳐져 있는 느낌이 나는데 이것은 왼쪽아래 햄버거 버튼을 통해 어느 화면은 보이도록 어느화면은 보이지 않도록 두그룹을 나누어서 한 XML에 구현을 한 상황이다. visiblity만 이용한다면 Intent필요 없이도 true false로만 간단히 화면을 숨길 수 있다. 그러기 위해선 각 구성영역에 Group 이라는 위젯을 사용한다.

<androidx.constraintlayout.widget.Group
    android:id="@+id/playerViewGroup"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    app:constraint_referenced_ids="trackTextView, artistTextView, coverImageCardView, bottomBackgroundView, playerSickBar, playTimeTextView, totalTimeTextView"
    tools:visibility="visible" />

<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" />

이렇게 같이 보여주고 싶은 View들을 constatint_referenced_ids를 통해 그룹화 시켜주고 visibility를 조정한다.

 - 각 View의 핵심 요소

1) cardview의 위치 설정과, elevation

센터에 있는 음악의 커버이미지가 들어갈 수 있는 파란색 cardView영역은 조금 떠 있는 듯 한 느낌이 든다. 그것은 cardview의 cardElevation = 10dp로 설정해주어 입체적인 느낌을 주었고 cardView의 위치를 translationY = 30dp 정도 주어 아래로 30dp만큼 위치 시켜주어 자연스럽게 위치를 설정해주었다.

 

2) SeekBar 설정

seekbar는 음악이 어디까지 재생돼 있는지 알 수 있고, 시간에 따라 늘어나는 bar형태이다.

먼저 seekbar의 background 색상을 배경색보다 진한 gray색으로 맞춰주고 progress 영역의 색상을 연보라색으로 설정해주 었다. 그렇게 하기 위해선 아래와 같이 커스텀 progressBar를 해야한다.

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

    <item android:id="@+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>

 

위와같이 custom하면 진한회색 배경에 보라색 progress 상태가 완성된다.

여기서 thumb를 그냥 seekbar progress와 똑같이해주어 thumb의 위치가 보이지 안도록 해주었다.

thumb란 seekbar의 진행상태를 알리는 모양을 설정할 수있는 방법이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Group
        android:id="@+id/playerViewGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:constraint_referenced_ids="trackTextView, artistTextView, coverImageCardView, bottomBackgroundView, playerSickBar, playTimeTextView, totalTimeTextView"
        tools:visibility="visible" />

    <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" />

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

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

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:textColor="@color/white"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="재생목록" />

    <TextView
        android:id="@+id/trackTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:textColor="@color/white"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="노래제목" />

    <TextView
        android:id="@+id/artistTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:textColor="@color/gray_a"
        android:textSize="15sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/trackTextView"
        tools:text="가수이름" />

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

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/playListRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        app:layout_constraintBottom_toTopOf="@id/playerView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/titleTextView" />

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/playerView"
        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
        android:id="@+id/playerSickBar"
        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_bar"
        android:thumb="@drawable/player_seek_sum"
        app:layout_constraintBottom_toTopOf="@id/playerView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:progress="40" />

    <TextView
        android:id="@+id/playTimeTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textColor="@color/gray_97"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="@id/playerSickBar"
        app:layout_constraintTop_toBottomOf="@id/playerSickBar"
        tools:text="12:12" />

    <TextView
        android:id="@+id/totalTimeTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textColor="@color/gray_97"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="@id/playerSickBar"
        app:layout_constraintTop_toBottomOf="@id/playerSickBar"
        tools:text="20:12" />

    <SeekBar
        android:id="@+id/playListSeekBar"
        android:layout_width="0dp"
        android:layout_height="2dp"
        android:clickable="false"
        android:paddingStart="0dp"
        android:paddingEnd="0dp"
        android:progressTint="@color/purple_200"
        android:thumbTint="@color/purple_200"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/playerView"
        tools:progress="40" />

    <ImageView
        android:id="@+id/playContollImageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@drawable/play"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/playerView"
        app:tint="@color/black" />

    <ImageView
        android:id="@+id/skipNextImageView"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@drawable/next"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.2"
        app:layout_constraintStart_toEndOf="@id/playContollImageView"
        app:layout_constraintTop_toTopOf="@id/playerView"
        app:tint="@color/black" />

    <ImageView
        android:id="@+id/skipPrevImageView"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@drawable/skip_previous"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/playContollImageView"
        app:layout_constraintHorizontal_bias="0.8"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/playerView"
        app:tint="@color/black" />

    <ImageView
        android:id="@+id/playlistImageView"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginStart="24dp"
        android:src="@drawable/playlist_play"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/playerView"
        app:tint="@color/black" />

</androidx.constraintlayout.widget.ConstraintLayout>

완성된 xml Layout 구조 설정, 위와같이 설정하면 위에서 보여주었던 아래와 같은 xml이 완성된다.

playFragment.xml 완성 화면

2. 음악 목록 API 만들기

저작권이 무료 음악인 사이트에서 가져온 음악 정보를 이용하여 mocky.io를 통해 mock Data를 만들어 주었다.

https://run.mocky.io/v3/9f853a2a-62b3-48f4-91d5-65eabd0b32f5

위와 같이 json 형태의 mock Data가 만들어져 있으면 이 데이터를 이용하기 위해 설정해준다.

 

<uses-permission android:name="android.permission.INTERNET"/>
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'

 -  Internet Permission 허용

- api를 불러오기 위한 retrofit2, gson 변환, 이미지 적용을 위한 glide를 불러와준다.

 

 

먼저 데이터를 불러올 musicService interface를 만든다.

package com.example.aop_part4_chaptor02.service

import retrofit2.Call
import retrofit2.http.GET

interface MusicService {

    @GET("/v3/9f853a2a-62b3-48f4-91d5-65eabd0b32f5")
    fun listMusics() : Call<MusicDto>
}

Retorit을 이용해 API의 JSON DATA를 불러오는 순서

1) interface MusicService를 정의하여 @GET 형식으로 fun listmusics()라는 musicDto를 call하는 함수를 정의한다.

2) musicDto에는 val musics를 정의하고 거기에 MusicEntity가 담겨 있는 List<MusicEntity>를 List를 return 받는다.

3) MusicEntity에는 Json의 각 객체의 속성들이 정의돼있고 @serializedname를통해 name을 매칭하고 data를 가져온다.

4) 여기서 더 해줘야 하는 작업이있다. 그건 mapper를 통해 mapping을 해주는 작업을 해야한다.

- mapping을 해야 하는 이유는 다음과 같다. Json 속성에 ID 값이 없으므로 RecylcerView Adapter의 DiffUtil을 통한 중복방지 비교 하기 위한 Id 값도 넣어줘야 한다. 그리고 IsPlaying또한 지금 실행중인 음악인지 아닌지 여부를 파악하기 위해 넣어주어야 한다.

fun MusicDto.mapper(): PlayerModel =
    PlayerModel(
        playMusicList = musics.mapIndexed { index, musicEntity ->
            MusicModel(
                id = index.toLong(),
                streamUrl = musicEntity.streamUrl,
                coverUrl = musicEntity.coverUrl,
                track = musicEntity.track,
                artist = musicEntity.artist,
            )
        }
    )

위와 같이 mapIndexed로 매핑해줄 수 있다.

5) 최종적으로 currentPosition을 찾아 현재 진행중인 음악의 position과 일치할시 MusicModel의 isplaying을 True로 바꾸어 준다.

mediaitem의 mediaId 는 0부터 시작하므로 맨위에 있는 0번리스트의 음악이 먼저 실행 된다.

data class PlayerModel(
    private val playMusicList: List<MusicModel> = emptyList(),
    var currentPosition: Int = -1,
    var isWatchingPlayListView : Boolean = true
){
    fun getAdapterModels(): List<MusicModel>{
        return playMusicList.mapIndexed { index, musicModel ->
            val newItem = musicModel.copy(
                isPlaying = index == currentPosition
            )
            newItem
        }
    }

아래와 같이 retrofit을 최종적으로 만들어준다.

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>,
                    ) {
                        if (response.isSuccessful.not()) {
                            Log.d("this", "실패")
                            return
                        }
                        response.body()?.let {
                            musicDto ->
                            model = musicDto.mapper()
                            setMusicList(model.getAdapterModels())
                            playListAdapter.submitList(model.getAdapterModels())
                        }
                    }

                    override fun onFailure(call: Call<MusicDto>, t: Throwable) {
                        Log.d("this", "에러내용 : ${t.toString()}")
                    }
                })
        }
}


3) ExoPlayer를 이용하여 음악 재생하기

ExoPlayer?

 

 

구글에서 만든 오픈 소스 미디어 플레이 라이브러리 입니다. 기존에 오디오와 비디오 재생은 MediaPlayer를 사용했었지만 ExoPlayer가 나온 이후에는 MediaPlayer 보다 더욱 작고 유연하며 안정적이므로 많은 개발자들이 사용하는 오픈 소스 라이브러리가 되었습니다. 물론 우리가 즐겨보는 유튜브와 구글 무비도 ExoPlayer 를 사용해서 만들었습니다.

 

Exoplayer 부분

def exoplayer_version = '2.16.1'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"

ㅁ 먼저 app 수준 build.gradle에 Exoplayer2를 추가 해줍니다.

private var player: ExoPlayer? = null

 

private fun initPlayView(fragmentPlayerBinding: FragmentPlayerBinding) {
    context?.let {
        player = ExoPlayer.Builder(it).build()
    }

    fragmentPlayerBinding.playerView.player = player // PlayerView 즉 Exoplayer2의 view에 player 설정,

    binding?.let { binding ->
        player?.addListener(object: Player.Listener {
            override fun onIsPlayingChanged(isPlaying: Boolean) {
            // isplaying 여부에 따라 작동하는 overide
                super.onIsPlayingChanged(isPlaying)

                if (isPlaying) {
                    binding.playContollImageView.setImageResource(R.drawable.pause)
                    // 음악이 재생중일 때는 음악을 끄는 중지 drawable로 변경
                } else {
                    binding.playContollImageView.setImageResource(R.drawable.play)
                    // 음악이 종료중일 때는 음악을 키는 시작 drawable로 변경
                }
            }

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

                updateSeek()
                // 1초에 한번씩 seekBar가 늘어나도록 updateseek()을 구현해줌,
                // removeCallbacks과 callback을 이용하여 구현함,

            }

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

                val newIndex = mediaItem?.mediaId ?: return
                model.currentPosition = newIndex.toInt()
                updatePlayerView(model.currentMusicModel())
                playListAdapter.submitList(model.getAdapterModels())
            }

        })


    }
}

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

 

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

 

재생 중일 때 seekBar의 잔량과 상태를 갱신해주기 위한 작업
private fun updateSeek() {
    val player = this.player?:return
    val duration = if(player.duration >= 0) player.duration else 0
    val position = player.currentPosition

    //todo ui update
    updateSeekUi(duration, position)

    val state = player.playbackState

    view?.removeCallbacks(updateSeekRunnable) // 중복 방지 1초 대기하는 코드를 지우고 다시 !!

    if(state != Player.STATE_IDLE && state != Player.STATE_ENDED){
        view?.postDelayed(updateSeekRunnable, 1000)
    } // 1초에 한번씩 실행되는 무한루프 !!
}

private fun updateSeekUi(duration: Long, position: Long) {
    binding?.let { binding ->
        binding.playListSeekBar.max = (duration / 1000).toInt()
        binding.playListSeekBar.progress = (position / 1000).toInt()

        binding.playerSickBar.max = (duration / 1000).toInt()
        binding.playerSickBar.progress = (position / 1000).toInt()

        binding.playTimeTextView.text = String.format("%02d:%02d", // 02는 두자리 수 인데 두자리수가 못미추면 0으로 채운다 !
            TimeUnit.MINUTES.convert(position, TimeUnit.MILLISECONDS),
            (position / 1000) % 60)
        binding.totalTimeTextView.text = String.format("%02d:%02d", // 02는 두자리 수 인데 두자리수가 못미추면 0으로 채운다 !
            TimeUnit.MINUTES.convert(position, TimeUnit.MILLISECONDS),
            (duration / 1000) % 60)
    }
}
private val updateSeekRunnable = Runnable{
    updateSeek()
}
runnable은 어떤 객체도 리턴하지 않는다 즉 Exaption이 없다. 그래서 Tread에 인자를 바로 전달 할 수 있다는 점이 Callable과의
차이점이다.
updateSeekRunnalbe을 1초에 한번씩 postDelayed를 통해 전달
새로운 updateSeek()이 실행되면 만약 postDelayed가 시간이 남아 있다면
removeCallback을 통해 updateSeekRunnalbe을 없애주고 진행해준다.
private fun initSeekBar(fragmentPlayerBinding: FragmentPlayerBinding) {
    fragmentPlayerBinding.playerSickBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
        override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {

        }

        override fun onStartTrackingTouch(p0: SeekBar?) {
        }

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

    })
    fragmentPlayerBinding.playListSeekBar.setOnTouchListener { view, motionEvent -> false }

}

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

 

4) 생명주기 설정


override fun onStop() {
    super.onStop()

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

override fun onDestroy() {
    super.onDestroy()
    binding = null
    player?.release()
    view?.removeCallbacks(updateSeekRunnable)
}

onStop과 onDestroy가 될때 player의 상태를 바꾸어 준다. removecallback 음악재생시 루프를 돌 고 있으니 종료해주어야 한다.

'안드로이드 > 앱개발(Android)' 카테고리의 다른 글

(Android) 심리테스트 앱 - 직접 구현해보기 (1)  (0) 2022.04.08
(Android) OTT 앱  (0) 2022.03.08
(Android) 유튜브 앱  (0) 2022.02.12
(Android) 에어비엔비앱  (0) 2022.02.12
(Android) 중고거래 앱  (0) 2022.02.12