音樂播放器
- 可以播放、暫停、重置
- 播放過程進度條也要跟著變化
- 可以通過拖動進度條來改變播放的進度
- 可以調整聲音大小
- 隨著音樂的播放、暫停、重置,小鳥動畫也要跟著變化(旋轉、暫停、還原)
錄音工具
- 錄音並且在本地存放錄音檔
- 播放已經錄製好的音樂檔
Components
- MediaPlayer / MediaRecorder
- Animator
- Seekbar
- Thread
MediaPlayer
我們在 /res/raw 文件夾下放一個 audio_bird.mp3 並通過 MediaPlayer 來播放。
這裡是 MediaPlayer 的一些常用方法
- var mediaPlayer = MediaPlayer.create(this, R.raw.audio_bird) – 創建 MediaPlayer
- mediaPlayer.start() – 開始播放檔案(會從最後停止的地方開始)
- mediaPlayer.pause() – 暫停播放檔案
- mediaPlayer.reset() – 重置(目前測試重置後沒辦法馬上重新播放)
- mediaPlayer.isPlaying – 返回當前的播放狀態
- mediaPlayer.seekTo() – 移動檔案播放進度(單位毫秒)
- mediaPlayer.currentPosition – 取得當前的播放進度(單位毫秒)
- mediaPlayer.duration – 取得檔案總時間(單位毫秒)
- mediaPlayer.setVolume() – 設置左右聲道的音量
監聽 MediaPlayer 是否播放完畢的方法
1 |
mediaPlayer.setOnCompletionListener{} |
SeekBar
通過 VolmueSeekBar 來控制音量
1 2 3 4 5 6 7 8 9 10 11 12 |
volumeSeekBar.setOnSeekBarChangeListener(object:SeekBar.OnSeekBarChangeListener{ override fun onProgressChanged(p0: SeekBar?, progress: Int, p2: Boolean) { // update progress text volumeTextView.setText("Volumn: ${volumeSeekBar.progress}%") // update volumn mediaPlayer.setVolume(progress / 100f, progress / 100f) } override fun onStartTrackingTouch(p0: SeekBar?) {} override fun onStopTrackingTouch(p0: SeekBar?) {} }) |
通過 Thread 每個 500 毫秒根據播放進度來更新當前 SeekBar 的進度
1 2 3 4 5 6 7 8 9 |
val thread = Thread(Runnable { while (true) { Thread.sleep(500) if (!isSeeking) { progressSeekBar.progress = mediaPlayer.currentPosition } } }) thread.start() |
通過 ProgressSeekBar 來控制播放進度
首先給 progressSeekBar 設定最大值為播放檔案的總時間
1 2 |
// tootal time duration of sonng progressSeekBar.max = mediaPlayer.duration |
建立一個 isSeeking 來判斷使用者是否拖動進度條
1 |
private var isSeeking = false |
給 progressSeekBar 加入監聽,當使用者拖動進度條的時候 isSeeking 改為 true 並且同步 MediaPlayer 播放的進度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
progressSeekBar.setOnSeekBarChangeListener(object:SeekBar.OnSeekBarChangeListener{ override fun onProgressChanged(p0: SeekBar?, progress: Int, p2: Boolean) { if(isSeeking) { mediaPlayer.seekTo(progressSeekBar.progress) } } override fun onStartTrackingTouch(p0: SeekBar?) { isSeeking = true } override fun onStopTrackingTouch(p0: SeekBar?) { isSeeking = false } }) |
BirdAnimator
在使用者開始播放音樂的時候,通過 ObjectAniamtor 來旋轉小鳥的圖片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
lateinit var birdAnimator:ObjectAnimator private fun startAnimateBirdImageView() { if(birdAnimator.isPaused){ birdAnimator.resume() } else { birdAnimator.start() } } private fun pauseAnimateBirdImageView() { birdAnimator.pause() } private fun resetAnimateBirdImageView() { birdAnimator.end() } |
通過 Thread 定時更新進度
在播放的過程中,會不斷地去檢查 mediaPlayer.currentPostion 進而更新進度條
1 2 3 4 5 6 7 8 9 10 |
// continuously updating progress val thread = Thread(Runnable { while (true) { Thread.sleep(500) if (!isSeeking) { progressSeekBar.progress = mediaPlayer.currentPosition } } }) thread.start() |
更多的機制
- 當音樂播放完畢之後,將小鳥圖片、進度條、播放按鈕還原(播放按鈕呈現 PLAY 字樣)
- 播放音樂的過程中,小鳥的圖片會不停的旋轉
- 離開 Activity 的時候,音樂、小鳥動畫停止。
MediaRecorder
需要用到錄音和文件寫入的權限(在 AndroidManifest.xml 中加入)
1 2 |
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> |
權限&設備檢查
麥克風檢查
可以通過 PackageManager 來檢查設備有沒有麥克風
1 2 3 4 |
val packageManager = packageManager if(!packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)){ Log.e("PackageManager","This device doesn't have a mic") } |
錄音權限檢查
通過 ActivityCompat 來檢查錄音的權限。
1 2 3 4 |
if(ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 0) return } |
寫入文件權限檢查
通過 ActivityCompat 檢查寫入文件的權限。
1 2 3 4 |
if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),0) return } |
開始錄音
檢查完寫入權限以後,通過 File.CreateTempFile 建立一個臨時的錄音檔案為「birdRecording.3gp」
1 2 3 4 5 6 7 8 |
val path = File(Environment.getExternalStorageDirectory().path) try { soundFile = File.createTempFile ("birdRecording", ".3gp", path) println("created file $soundFile") } catch (e: IOException) { Log.e("Setup sound File","failed ${e.message}") } |
臨時的錄音檔會被加入亂數後綴
初始化 MediaRecorder 並且設定使用麥克風、輸出格式、編碼格式。
記得錄音前要先執行 prepare 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
recorder = MediaRecorder() recorder.setAudioSource(MediaRecorder.AudioSource.MIC) recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) recorder.setOutputFile(soundFile?.absolutePath) try { recorder.prepare() } catch (e: IOException) { } recorder.start() |
完成錄音
通過 stop 方法結束錄音,並通過 release 方法釋放資源
1 2 |
recorder.stop() recorder.release() |
播放錄音檔
初始化 MediaPlayer 並準備好兩個 Listener 一個等 player 準備好以後播放,一個等播放完成時重置 player
1 2 3 4 5 |
player = MediaPlayer() player!!.setOnPreparedListener(playerPreparedHandler) player!!.setOnCompletionListener { stopPlayer() } |
接下來我們要讓 player 直到我們要播放什麼檔案。
前面錄音的時候,我們通過 var soundFile 紀錄臨時建立的檔案,這裡通過 absolutePath 方法來拿到路徑。
1 2 3 4 5 6 7 |
try { player?.setDataSource(soundFile!!.absolutePath) player?.prepare() } catch(e: IOException) { Log.e("PlayRecording","Failed") } |
在 player 準備好以後,會執行準備好的 playerPreparedHandler
1 |
player?.start() |
而 player 播放完畢的時候,會執行我們的 stopPlayer()
1 2 |
player?.release() player = null |
更多的機制
- 通過 isRecording 來判斷使用者點下錄音按鈕的時候,是要錄音還是完成錄音。
- 通過 player.isPlaying 來判斷使用者點下播放按鈕的時候,是要播放還是停止。
筆記
- Todo:除了開啟 Thread 定時更新進度條以外,還有沒有更好的方法,(需要離開 Activity 的時候就暫停檢查)。
參考
- 官方文件 – MediaPlayer
- 官方文件 – MediaRecorder
- 可以到 Github 上看對應的 Source Code
RecordActivity.kt
114行目、例外を握り潰してますよ