有時候需要給Android應用添加背景音樂的功能,例如一些小遊戲之類的應用。在應用處於前臺可見時,需要播放背景音樂,當應用處於後臺不可見時(如按了home鍵或進入其它應用或該應用被銷燬時)背景音樂也要隨之暫停或停止。
利用Service實現背景音樂播放功能
Service 是一種可在後臺執行長時間運行操作而不提供界面的應用組件。服務可由其他應用組件啓動,而且即使用戶切換到其他應用,服務仍將在後臺繼續運行。此外,組件可通過綁定到服務與之進行交互,甚至是執行進程間通信 (IPC)。例如,服務可在後臺處理網絡事務、播放音樂,執行文件 I/O 或與內容提供程序進行交互。
可以通過擴展Service或IntentService來實現服務功能。背景音樂需要在整個應用期間持續播放,停止服務的控制權要交給應用組件,而由於IntentService會在完成任務後自行調用stopSelf()來停止服務,所以不適合此處。這裏需要擴展Service來實現功能。
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* BackgroundMusic
* 後臺背景音樂服務(常應用於小遊戲),要求
* 1.應用不可見時(未銷燬),播放暫停,服務保持
* 2.應用恢復可見時,播放繼續(不是重新開始)
* 3.應用退出或被銷燬時,服務停止
*/
public class BgmService extends Service {
public static final String APP_TAG = "SUDOKU_TAG";
public static final String ACTION_MUSIC_PLAY= "com.jack..action.ACTION_MUSIC_PLAY";
public static final String ACTION_MUSIC_PAUSE= "com.jack..action.ACTION_MUSIC_PAUSE";
private MediaPlayer mediaPlayer;
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private final class ServiceHandler extends Handler{
private ServiceHandler(Looper looper){
super(looper);
}
//播放 暫停
@Override
public void handleMessage(@NonNull Message msg) {
Intent intent = (Intent) msg.obj;
if (ACTION_MUSIC_PLAY.equals(intent.getAction())) {
if(mediaPlayer==null) {
mediaPlayer = MediaPlayer.create(BgmService.this,R.raw.bgm72);//create包含prepare()
mediaPlayer.setLooping(true);
}
mediaPlayer.start();//播放或恢復播放
}else if (ACTION_MUSIC_PAUSE.equals(intent.getAction())) {
if(mediaPlayer!=null) {
mediaPlayer.pause();
}
}
//mediaPlayer.setOnPreparedListener(BgmService.this);
//mediaPlayer.prepareAsync(); // prepare async to not block main thread
}
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("BgmServiceHandlerThread");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(APP_TAG," BgmService "+intent.getAction());
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
Log.v(APP_TAG," BgmService onDestroy");
super.onDestroy();
mServiceLooper.quit();
//mediaPlayer非常消耗資源,務必銷燬之
if (mediaPlayer != null) mediaPlayer.release();
}
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
}
播放媒體文件是個耗時的過程,必須要開啓另外的線程來完成播放任務。此次是播放本地的媒體資源,如果是網絡資源,需要實現MediaPlayer.OnPreparedListener接口,當資源準備就緒後才mediaPlayer.start()。
mediaPlayer.prepare();是個阻塞的方法。
在主Activity中開啓或停止服務
背景音樂服務需要在應用銷燬時停止,如果不做處理,服務會一直存在直到被android系統殺掉。當然也可以通過一個開關配置項來控制背景音樂是否播放。
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, BgmService.class);
intent.setAction(BgmService.ACTION_MUSIC_PLAY);
startService(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
stopService(new Intent(this, BgmService.class));
}
...
}
MainActivity銷燬就表示應用銷燬了,這樣就保證了應用銷燬時背景音樂關閉。
那還有個難題,如何控制暫停呢?當應用不可見處於後臺時(如按下Home鍵或進入其它應用),怎樣讓背景音樂暫停呢,然後應用重新回到前臺時背景音樂恢復播放呢?
可能第一想到的是在Activity的onStop()方法中暫停服務。如下:
@Override
protected void onStop() {
super.onStop();
Intent intent = new Intent(this, BgmService.class);
intent.setAction(BgmService.ACTION_MUSIC_PAUSE);
startService(intent);
}
顯然這是可行的,當它只能保證該Activity不可見(stop狀態)時可以暫停播放,但是一個應用包含非常多的Activity,你需要在每個Activity中的onStop()方法都加上上面的代碼,甚至onStart()方法也要加上開啓服務的代碼。顯然這樣是不可行的方法。因爲背景音樂服務是和應用狀態相關的,所以這個控制開啓或暫停的邏輯應該和應用有關,而不是和應用的某個Activity有關。所以這裏用Application.ActivityLifecycleCallbacks來實現纔是最佳實現方式。
應用的前後臺狀態判斷
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* 應用前後臺狀態監聽幫助類,僅在Application中使用
*/
public class AppFrontBackHelper {
private OnAppStatusListener mOnAppStatusListener;
public AppFrontBackHelper() {
}
/**
* 註冊狀態監聽,僅在Application中使用
*/
public void register(Application application, OnAppStatusListener listener){
mOnAppStatusListener = listener;
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
public void unRegister(Application application){
application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
//打開的Activity數量統計
private int activityStartCount = 0;
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
activityStartCount++;
//數值從0變到1說明是從後臺切到前臺
if (activityStartCount == 1){
//從後臺切到前臺
if(mOnAppStatusListener != null){
mOnAppStatusListener.onFront();
}
}
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
activityStartCount--;
//數值從1到0說明是從前臺切到後臺
if (activityStartCount == 0){
//從前臺切到後臺
if(mOnAppStatusListener != null){
mOnAppStatusListener.onBack();
}
}
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
public interface OnAppStatusListener{
void onFront();
void onBack();
}
}
通過給應用註冊ActivityLifecycleCallbacks 監聽,對每個Acitivity的週期回調的監控,來達到目的。這裏只要監聽onActivityStarted和onActivityStopped兩個方法。
import android.app.Application;
import androidx.preference.PreferenceManager;
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
AppFrontBackHelper helper = new AppFrontBackHelper();
helper.register(MyApp.this, new AppFrontBackHelper.OnAppStatusListener() {
@Override
public void onFront() {
//應用切到前臺處理
Intent intent = new Intent(MyApp.this, BgmService.class);
intent.setAction(action);
MyApp.this.startService(intent);
}
@Override
public void onBack() {
//應用切到後臺處理
MyApp.this.stopService(new Intent(MyApp.this, BgmService.class));
}
});
}
}
在清單中申明MyApp
<application
android:name=".util.MyApp"
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=".service.BgmService" android:exported="false"/>
</application>
最後,在MainActivity中只需要onDestroy的銷燬服務,onStart和onStop不需要了。
至此已經實現了背景音樂隨應用狀態改變而播放和暫停了。通過完成此功能,你會對
Service和Activity的生命週期有更深層的瞭解。
《道德經》第十一章:
三十輻共一轂,當其無,有車之用。埏埴以爲器,當其無,有器之用。鑿戶牖以爲室,當其無,有室之用。故有之以爲利,無之以爲用。
譯文:三十根輻條彙集到一根轂中的孔洞當中,有了車轂中空的地方,纔有車的作用。揉和陶土做成器皿,有了器具中空的地方,纔有器皿的作用。開鑿門窗建造房屋,有了門窗四壁內的空虛部分,纔有房屋的作用。所以,“有”給人便利,“無”發揮了它的作用。