iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 19
0
Software Development

Kotlin 30 天,通過每天一個小 demo 學習 Android 開發系列 第 19

Kotlin 開發第 19 天 LittleBirdSound ( MediaPlayer + MediaRecorder )

  • 分享至 

  • xImage
  •  

LittleBirdSound

音樂播放器

  • 可以播放、暫停、重置
  • 播放過程進度條也要跟著變化
  • 可以通過拖動進度條來改變播放的進度
  • 可以調整聲音大小
  • 隨著音樂的播放、暫停、重置,小鳥動畫也要跟著變化(旋轉、暫停、還原)

錄音工具

  • 錄音並且在本地存放錄音檔
  • 播放已經錄製好的音樂檔

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 是否播放完畢的方法

mediaPlayer.setOnCompletionListener{}

SeekBar

https://ithelp.ithome.com.tw/upload/images/20171222/201073297o4FThsKfP.png

通過 VolmueSeekBar 來控制音量

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 的進度

val thread = Thread(Runnable {
    while (true) {
        Thread.sleep(500)
        if (!isSeeking) {
            progressSeekBar.progress = mediaPlayer.currentPosition
        }
    }
})
thread.start()

通過 ProgressSeekBar 來控制播放進度

https://ithelp.ithome.com.tw/upload/images/20171222/20107329VMaUfghCZ2.png
首先給 progressSeekBar 設定最大值為播放檔案的總時間

// tootal time duration of sonng
progressSeekBar.max = mediaPlayer.duration

建立一個 isSeeking 來判斷使用者是否拖動進度條

private var isSeeking = false

給 progressSeekBar 加入監聽,當使用者拖動進度條的時候 isSeeking 改為 true 並且同步 MediaPlayer 播放的進度

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

BirdAnimator
在使用者開始播放音樂的時候,通過 ObjectAniamtor 來旋轉小鳥的圖片

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 進而更新進度條

// 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 中加入)

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

權限&設備檢查

麥克風檢查

可以通過 PackageManager 來檢查設備有沒有麥克風

val packageManager = packageManager
if(!packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)){
 Log.e("PackageManager","This device doesn't have a mic")
}

錄音權限檢查

https://ithelp.ithome.com.tw/upload/images/20171222/20107329KuM4eGSjoW.png
通過 ActivityCompat 來檢查錄音的權限。

if(ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED){
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 0)
    return
}

寫入文件權限檢查

https://ithelp.ithome.com.tw/upload/images/20171222/20107329hlKACTu2Og.png
通過 ActivityCompat 檢查寫入文件的權限。

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」

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}")
}

臨時的錄音檔會被加入亂數後綴

https://ithelp.ithome.com.tw/upload/images/20171222/20107329YrQjsvR8YS.png
初始化 MediaRecorder 並且設定使用麥克風、輸出格式、編碼格式。

記得錄音前要先執行 prepare 方法。

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 方法釋放資源

recorder.stop()
recorder.release()

播放錄音檔

初始化 MediaPlayer 並準備好兩個 Listener 一個等 player 準備好以後播放,一個等播放完成時重置 player

player = MediaPlayer()
player!!.setOnPreparedListener(playerPreparedHandler)
player!!.setOnCompletionListener {
    stopPlayer()
}

接下來我們要讓 player 直到我們要播放什麼檔案。

前面錄音的時候,我們通過 var soundFile 紀錄臨時建立的檔案,這裡通過 absolutePath 方法來拿到路徑。

try {
    player?.setDataSource(soundFile!!.absolutePath)
    player?.prepare()

} catch(e: IOException) {
    Log.e("PlayRecording","Failed")
}

在 player 準備好以後,會執行準備好的 playerPreparedHandler

player?.start()

而 player 播放完畢的時候,會執行我們的 stopPlayer()

player?.release()
player = null

更多的機制

  • 通過 isRecording 來判斷使用者點下錄音按鈕的時候,是要錄音還是完成錄音。
  • 通過 player.isPlaying 來判斷使用者點下播放按鈕的時候,是要播放還是停止。

筆記

  • Todo:除了開啟 Thread 定時更新進度條以外,還有沒有更好的方法,(需要離開 Activity 的時候就暫停檢查)。

參考


上一篇
Kotlin 開發第 18 天 SideMenu ( DrawerActivity )
下一篇
Kotlin 開發第 20 天 ActivityTransition
系列文
Kotlin 30 天,通過每天一個小 demo 學習 Android 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言