這篇文章我想來聊一下如何在Android中寫一個音樂播放器的方法
大家有沒有好奇過,我們每天的用的音樂播放器是怎麼寫出來的呢˚ᆺ˚
比方說像最知名的kkbox或者是各式各樣的播放器
通常大家都會關掉手機螢幕然後聽音樂吧◔_◔
好啦我知道也不是所有人...但有沒有曾經想過這個功能究竟是如何寫出來的呢?
所以今天的重點就是要來聊聊這個功能是怎麼寫的了
今天的內容最大的重點就是Service(服務)的應用
Service是什麼鬼?簡單來說就是一個當使用者就算把APP關掉或是把手機螢幕關掉後還可以讓程式對手機有影響力的元件
像是最基本的、也同時是今天的主題"音樂播放器"就是這項功能的應用
以及譬如像是資料下載也同樣是常見的Service應用(畢竟沒有人會希望下載資料的時候不能關手機螢幕...)
所以今天雖然標題是寫音樂播放器,但實際上今天的重點其實是基本款的Service的應用
那就來開始今天的內容
今天的程式沒有Gif展示(因為Gif沒辦法展示音樂...),但是基本上整體樣式就如下圖
以及Github
->https://github.com/thumbb13555/EasyMusicPlayerDemo/tree/master
1. 架構介紹&載入需要的檔案
今天的內容是做一個音樂播放器
不過我要先講...這真的就是一個音樂播放器而已
我沒有做"上一首"、"下一首"的功能
今天的功能只有
1. 播放"特定"音樂(載入在專案中的音樂)
2. 顯示音樂進度條、並可操作音樂播放進度
3. 寫一個Service,使音樂在螢幕關掉或是跳出APP後仍可執行
4. 當音樂在背景運作時,在通知欄顯示通知告訴使用者本應用程式運作中
如果知道後還沒有返回上一頁的話就繼續往下看吧XDDD
下圖是本次範例的所有內容
圖檔的部分都是Android提供的,請照以下操作找到他們吧
drawable右鍵->New->Vector Asset
關鍵字分別為:play、pause、audio
至於那張png圖片雖然可有可無,不過在我的Github有提供給你噢
然後關於音樂檔案,請創建一個資料夾叫做raw
res上右鍵->New->Directory->輸入raw
然後音樂在這邊...這裡我是去載"山丘"來放啦...大家可以隨意找喜歡的( ゚Д゚)b
->https://github.com/thumbb13555/EasyMusicPlayerDemo/blob/master/app/src/main/res/raw/hill.mp3
最後是Layout
<?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" tools:context=".MainActivity"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/image" app:layout_constraintBottom_toTopOf="@+id/seekBar_Position" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <SeekBar android:id="@+id/seekBar_Position" android:layout_width="300dp" android:layout_height="wrap_content" android:padding="10dp" app:layout_constraintBottom_toTopOf="@+id/textview_RemainingTime" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/textview_ElapsedTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:text="0:11" app:layout_constraintBottom_toTopOf="@+id/guideline" app:layout_constraintStart_toStartOf="@+id/seekBar_Position" /> <TextView android:id="@+id/textview_RemainingTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:text="-1:49" app:layout_constraintBottom_toTopOf="@+id/guideline" app:layout_constraintEnd_toEndOf="@+id/seekBar_Position" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.6" /> <Button android:id="@+id/button_Play" android:layout_width="40dp" android:layout_height="40dp" android:background="@drawable/ic_baseline_play_arrow_24" app:layout_constraintBottom_toTopOf="@+id/button_Stop" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline" /> <Button android:id="@+id/button_Stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="離開播放" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline" /> </androidx.constraintlayout.widget.ConstraintLayout>
2. 什麼是Service?
service中譯為服務
他所做的事情就像是一個沒有介面的Activity,但是他卻又能夠無視於onStop後的過程
onStop又是什麼?來看一下這張圖
從onCreate到onDestroy就是一個APP存活的過程
介紹生命週期已經可以在寫一篇文章了,我就暫時不去討論生命週期
但你只要記得三個部分
1. onCreate就是出現第一個畫面時會呼叫的副程式(像是初始化介面就在這)
2. onStop就是一個畫面消失的那一刻會被呼叫(例如把畫面finish掉,或是按下返回鍵等)
3. onDestroy就是你打開應用管理員後把APP滑掉時就會被呼叫,用外行的說法叫做"把APP清掉"
那麼Service就是個可以忽略掉onStop那層的控制,就算程式被onStop掉了依然可以執行指定功能的物件
但是一樣逃不過onDestroy就是了XD
也因此以今天的範例而言,我的介面僅是提供使用者操作
然後執行在Service
最後使用者關掉畫面後再回來,依然能夠繼續操作Service內在做的事情
如圖
所以由圖可知,其實他不是保存畫面
而是再開一個畫面時,跟Service要資料後呈現的
那麼,要怎麼要資料?
關鍵在於Binder這個東西
來看一下關於Service跟Binder的生命週期吧
每個Service都可以綁一個Binder(當然也可以不綁XD)
而Binder的功用就是儲存Service執行狀態的關鍵
並且能綁定給Activity UI做使用
OK,以上是Service的相關科普
那麼接下來就是實作了
3. 撰寫音樂工具包Binder
接下來要撰寫的的是MusicBinder.java這個檔案
我先PO全部,給你直接複製
class MusicBinder extends Binder { private MediaPlayer mediaPlayer; public MusicBinder(MediaPlayer mediaPlayer) { this.mediaPlayer = mediaPlayer; } /** * 偵測是否播放中 */ public boolean isPlaying() { return mediaPlayer.isPlaying(); } /** * 暂停 */ public void pauseMusic() { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); } } /** * 播放 */ public void playMusic() { if (!mediaPlayer.isPlaying()) { mediaPlayer.start(); } } /** * 取得歌曲總長度 **/ public int getProgress() { return mediaPlayer.getDuration(); } /** * 將播到到的位置傳出 */ public int getPlayPosition() { if (mediaPlayer != null) { return mediaPlayer.getCurrentPosition(); } return 0; } /** * 播放指定位置 */ public void seekToPosition(int sec) { mediaPlayer.seekTo(sec); } /** * 關閉播放器 */ public void closeMedia() { if (mediaPlayer != null) { mediaPlayer.stop(); mediaPlayer.release(); } } }
這邊就是Binder,可以看到他都是一些跟音樂操作本身有關的事情
而關於MediaPlayer的這個類,簡單來說就是一個播放音樂的一個工具
裡面有很多方法可以用,就請各位參照內容吧
3. 撰寫Service
接著來撰寫Service,就是MediaService.java這個檔案。一樣先全部PO
public class MediaService extends Service { private MediaPlayer mediaPlayer = new MediaPlayer(); private static final String CHANNEL_ID = "MyMusicPlayer"; private MusicBinder mBinder; @Override public void onCreate() { super.onCreate(); /**將歌曲載入到MediaPlayer中*/ mediaPlayer = MediaPlayer.create(this,R.raw.hill); /**載入mediaPlayer給Binder音樂工具包*/ mBinder = new MusicBinder(mediaPlayer); /**設置音樂播放屬性為Loop*/ mediaPlayer.setLooping(true); /**將音樂歸0*/ mediaPlayer.seekTo(0); /**有取用到本Service的話,則在通知欄顯示通知 * notificationIntent的部分是使如果使用者點擊通知的話,便會跳回本APP中*/ Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID ) .setContentTitle("注意") .setContentText("音樂撥放中") .setSmallIcon(R.drawable.ic_baseline_audiotrack_24) .setContentIntent(pendingIntent) .build(); startForeground(1, notification); } @Override public int onStartCommand(Intent intent, int flags, int startId) { /**此處為取得Notification的使用權限*/ NotificationChannel notificationChannel = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationChannel = new NotificationChannel(CHANNEL_ID, "My Service" , NotificationManager.IMPORTANCE_DEFAULT); } NotificationManager notificationManager = getSystemService(NotificationManager.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { assert notificationManager != null; notificationManager.createNotificationChannel(notificationChannel); } return super.onStartCommand(intent, flags, startId); } @Nullable @Override /**綁定音樂處理的各種方法*/ public IBinder onBind(Intent intent) { return mBinder; } }
首先...紅色底線的public一定要加入!!!!!不然會閃退
然後回頭複習一下Service的生命週期~對照程式看一下吧
關鍵字: onCreate、onBind、onStartConnend這三個字
找到它在程式的哪邊了嗎ʕ→ᴥ←ʔ
再來請來到AndroidManifest.xml內
增加以下標注內容
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jetec.easymusicplayerdemo"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" > <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MediaService"/> </application> </manifest>
以上操作,就完成Service設置囉!
4. 撰寫主要畫面
加油,剩最後一步了(∩╹□╹∩)
恩...其實我是講給自己聽的XD
接下來就是主要畫面的部分,也就是MainActivity的部分
一樣尿性,PO全部再來針對重點說
public class MainActivity extends AppCompatActivity { SeekBar sbPosition; TextView tvElapsed, tvRemain; Button btPlay, btStop; private MusicBinder myBinder; private Handler mHandler = new Handler(); Intent MediaServiceIntent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); /**開始使用播放音樂的Service*/ MediaServiceIntent = new Intent(this, MediaService.class); startService(MediaServiceIntent); /**將Service的播放狀態進行監聽,並綁定給介面*/ bindService(MediaServiceIntent, mServiceConnection, BIND_AUTO_CREATE); /**取得介面*/ btPlay = findViewById(R.id.button_Play); sbPosition = findViewById(R.id.seekBar_Position); tvElapsed = findViewById(R.id.textview_ElapsedTime); tvRemain = findViewById(R.id.textview_RemainingTime); btStop = findViewById(R.id.button_Stop); } private ServiceConnection mServiceConnection = new ServiceConnection() { /**如果介面有和MediaService成功綁定時,便會跳至onServiceConnected,反之onServiceDisconnected*/ @Override public void onServiceConnected(ComponentName name, IBinder service) { /**取用Binder中的各種音樂操作的方法*/ myBinder = (MusicBinder) service; if (myBinder.isPlaying()) { btPlay.setBackgroundResource(R.drawable.ic_baseline_pause_24); } else { btPlay.setBackgroundResource(R.drawable.ic_baseline_play_arrow_24); } sbPosition.setMax(myBinder.getProgress()); sbPosition.setOnSeekBarChangeListener(position); btPlay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { setIsPlayButton(btPlay); } }); btStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mHandler.removeCallbacks(runnable); myBinder.closeMedia(); stopService(MediaServiceIntent); finish(); } }); /**開始使用執行緒,使之每秒更新一次進度條*/ mHandler.post(runnable); } @Override public void onServiceDisconnected(ComponentName name) { } }; /**使用Handler配合runnable,始進度條每秒進行更新*/ private Runnable runnable = new Runnable() { @Override public void run() { try { /**取得現在音樂播放的進度,並顯示在SeekBar上*/ sbPosition.setProgress(myBinder.getPlayPosition()); /**設置現在播放的的時間*/ String elapsedTime = createTimeLabel(myBinder.getPlayPosition()); tvElapsed.setText(elapsedTime); String remainingTime = createTimeLabel(myBinder.getProgress() - myBinder.getPlayPosition()); tvRemain.setText("- " + remainingTime); /**使這個執行緒每秒跑一次*/ mHandler.postDelayed(runnable, 1000); }catch (Exception e){ } } }; /**設置播放音樂與暫停音樂*/ private void setIsPlayButton(Button bt) { if (myBinder.isPlaying()) { myBinder.pauseMusic(); bt.setBackgroundResource(R.drawable.ic_baseline_play_arrow_24); } else { myBinder.playMusic(); bt.setBackgroundResource(R.drawable.ic_baseline_pause_24); } } /**畫面消失時,則不監聽目前播放狀態*/ @Override protected void onStop() { super.onStop(); unbindService(mServiceConnection); } /**設置SeekBar拉動事件,使之能調整音樂播放的進度*/ private SeekBar.OnSeekBarChangeListener position = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean isSeekbarOnTouch) { /**isSeekbarOnTouch是我自己取的,本判斷是為偵測Seekbar是否有被使用所碰觸而改變進度條*/ if (isSeekbarOnTouch) { myBinder.seekToPosition(i); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }; /**將MediaPlayer傳回的進度轉換為時間*/ private String createTimeLabel(int time) { String timeLabel = ""; int min = time / 1000 / 60; int sec = time / 1000 % 60; timeLabel = min + ":"; if (sec < 10) timeLabel += "0"; timeLabel += sec; return timeLabel; }
首先,如何啟用Service?靠著是這三行
/**開始使用播放音樂的Service*/ MediaServiceIntent = new Intent(this, MediaService.class); startService(MediaServiceIntent); /**將Service的播放狀態進行監聽,並綁定給介面*/ bindService(MediaServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
然後,解除綁定則是這邊
/**畫面消失時,則不監聽目前播放狀態*/ @Override protected void onStop() { super.onStop(); unbindService(mServiceConnection); }
寫在onStop內,使畫面不再綁定於Service的進度
再來,監聽Service是否有成功綁定,靠得就是這邊啦!
private ServiceConnection mServiceConnection = new ServiceConnection() { /**如果介面有和MediaService成功綁定時,便會跳至onServiceConnected,反之onServiceDisconnected*/ @Override public void onServiceConnected(ComponentName name, IBinder service) { . . . } @Override public void onServiceDisconnected(ComponentName name) { } };
然後~進度條怎麼即時更新呢?在這裡!
/**使用Handler配合runnable,始進度條每秒進行更新*/ private Runnable runnable = new Runnable() { @Override public void run() { try { /**取得現在音樂播放的進度,並顯示在SeekBar上*/ sbPosition.setProgress(myBinder.getPlayPosition()); /**設置現在播放的的時間*/ String elapsedTime = createTimeLabel(myBinder.getPlayPosition()); tvElapsed.setText(elapsedTime); String remainingTime = createTimeLabel(myBinder.getProgress() - myBinder.getPlayPosition()); tvRemain.setText("- " + remainingTime); /**使這個執行緒每秒跑一次*/ mHandler.postDelayed(runnable, 1000); }catch (Exception e){ } } };
看不懂嗎?看不懂請參考我這篇文章!
碼農日常-『Android studio』Android背景執行之Thread、Handler及AsyncTask的基礎用法
最後我們是如何控制進度條呢?在這邊!
sbPosition.setOnSeekBarChangeListener(position);
/**設置SeekBar拉動事件,使之能調整音樂播放的進度*/ private SeekBar.OnSeekBarChangeListener position = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean isSeekbarOnTouch) { /**isSeekbarOnTouch是我自己取的,本判斷是為偵測Seekbar是否有被使用所碰觸而改變進度條*/ if (isSeekbarOnTouch) { myBinder.seekToPosition(i); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } };
這邊是分解喔~所以看不懂的話可以參照一開始給你的"全部程式"參照著看吧!
到這邊就可以執行了!趕快來看看自己有沒有完成吧( ゚Д゚)b
結語
Service堪稱Android四大組件之一
四大組件聽起來超帥,像個什麼四大天王似的
不過既取名四大組件,表示他的地位真的非常重要
回想看看音樂播放器跟下載這兩個範例是用得多麽擴長高深↜(╰ •ω•)╯ψ
其他的組件或許我之後也會講吧
不過看心情囉XDD
覺得我的文章不錯的話,就幫我按個推推哦~
謝謝各位!
留言列表