這篇文章我想來聊一下如何在Android中寫一個音樂播放器的方法

大家有沒有好奇過,我們每天的用的音樂播放器是怎麼寫出來的呢˚ᆺ˚

比方說像最知名的kkbox或者是各式各樣的播放器

通常大家都會關掉手機螢幕然後聽音樂吧◔_◔

好啦我知道也不是所有人...但有沒有曾經想過這個功能究竟是如何寫出來的呢?

所以今天的重點就是要來聊聊這個功能是怎麼寫的了

 

今天的內容最大的重點就是Service(服務)的應用

Service是什麼鬼?簡單來說就是一個當使用者就算把APP關掉或是把手機螢幕關掉後還可以讓程式對手機有影響力的元件

像是最基本的、也同時是今天的主題"音樂播放器"就是這項功能的應用

以及譬如像是資料下載也同樣是常見的Service應用(畢竟沒有人會希望下載資料的時候不能關手機螢幕...)

所以今天雖然標題是寫音樂播放器,但實際上今天的重點其實是基本款的Service的應用

 

那就來開始今天的內容

今天的程式沒有Gif展示(因為Gif沒辦法展示音樂...),但是基本上整體樣式就如下圖

Screenshot_20200912-175820_EasyMusicPlayerDemo

Screenshot_20200912-175837_EasyMusicPlayerDemo

 

以及Github

->https://github.com/thumbb13555/EasyMusicPlayerDemo/tree/master

 


 

1. 架構介紹&載入需要的檔案

 

今天的內容是做一個音樂播放器

不過我要先講...這真的就是一個音樂播放器而已

我沒有做"上一首"、"下一首"的功能

今天的功能只有

 

1. 播放"特定"音樂(載入在專案中的音樂)

2. 顯示音樂進度條、並可操作音樂播放進度

3. 寫一個Service,使音樂在螢幕關掉或是跳出APP後仍可執行

4. 當音樂在背景運作時,在通知欄顯示通知告訴使用者本應用程式運作中

 

如果知道後還沒有返回上一頁的話就繼續往下看吧XDDD

下圖是本次範例的所有內容

截圖 2020-09-12 下午8.41.32

 

圖檔的部分都是Android提供的,請照以下操作找到他們吧

drawable右鍵->New->Vector Asset

關鍵字分別為:play、pause、audio

截圖 2020-09-12 下午8.59.47

 

至於那張png圖片雖然可有可無,不過在我的Github有提供給你噢

->https://github.com/thumbb13555/EasyMusicPlayerDemo/blob/master/app/src/main/res/drawable-v24/image.png

 

然後關於音樂檔案,請創建一個資料夾叫做raw

res上右鍵->New->Directory->輸入raw

截圖 2020-09-12 下午9.04.08

 

然後音樂在這邊...這裡我是去載"山丘"來放啦...大家可以隨意找喜歡的( ゚Д゚)b

->https://github.com/thumbb13555/EasyMusicPlayerDemo/blob/master/app/src/main/res/raw/hill.mp3

 

最後是Layout

 

activity_main.xml

截圖 2020-09-12 下午9.08.32

<?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又是什麼?來看一下這張圖

Android fundamentals 02.2: Activity lifecycle and state

從onCreate到onDestroy就是一個APP存活的過程

介紹生命週期已經可以在寫一篇文章了,我就暫時不去討論生命週期

但你只要記得三個部分

1. onCreate就是出現第一個畫面時會呼叫的副程式(像是初始化介面就在這)

2. onStop就是一個畫面消失的那一刻會被呼叫(例如把畫面finish掉,或是按下返回鍵等)

3. onDestroy就是你打開應用管理員後把APP滑掉時就會被呼叫,用外行的說法叫做"把APP清掉"

 

那麼Service就是個可以忽略掉onStop那層的控制,就算程式被onStop掉了依然可以執行指定功能的物件

但是一樣逃不過onDestroy就是了XD

也因此以今天的範例而言,我的介面僅是提供使用者操作

然後執行在Service

最後使用者關掉畫面後再回來,依然能夠繼續操作Service內在做的事情

如圖

截圖 2020-09-12 下午9.43.24

所以由圖可知,其實他不是保存畫面

而是再開一個畫面時,跟Service要資料後呈現的

 

那麼,要怎麼要資料?

關鍵在於Binder這個東西

來看一下關於Service跟Binder的生命週期吧

work note: [Android] Service Lifecycle

 

每個Service都可以綁一個Binder(當然也可以不綁XD)

而Binder的功用就是儲存Service執行狀態的關鍵

並且能綁定給Activity UI做使用

 

OK,以上是Service的相關科普

那麼接下來就是實作了

 


 

3. 撰寫音樂工具包Binder

 

接下來要撰寫的的是MusicBinder.java這個檔案

截圖 2020-09-12 下午8.41.32

 

我先PO全部,給你直接複製

 

MusicBinder.java

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

截圖 2020-09-12 下午8.41.32

 

MediaService.java

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的生命週期~對照程式看一下吧

work note: [Android] Service Lifecycle

 

關鍵字: onCreate、onBind、onStartConnend這三個字

找到它在程式的哪邊了嗎ʕ→ᴥ←ʔ

再來請來到AndroidManifest.xml

增加以下標注內容

 

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全部再來針對重點說

 

MainActivity.java

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

Screenshot_20200912-175804_EasyMusicPlayerDemo

 

 


結語

 

Service堪稱Android四大組件之一

四大組件聽起來超帥,像個什麼四大天王似的

不過既取名四大組件,表示他的地位真的非常重要

回想看看音樂播放器跟下載這兩個範例是用得多麽擴長高深↜(╰ •ω•)╯ψ

 

其他的組件或許我之後也會講吧

不過看心情囉XDD

覺得我的文章不錯的話,就幫我按個推推哦~

謝謝各位!

100 Thank You Memes, Images and Funny Thanks Meme Pics | Funny dog photos,  Funny dog pictures, Puppies funny

 

 

 

arrow
arrow
    創作者介紹
    創作者 碼農日常 的頭像
    碼農日常

    碼農日常大小事

    碼農日常 發表在 痞客邦 留言(7) 人氣()