Android 根據應用前後臺狀態播放(或暫停)背景音樂

有時候需要給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的生命週期有更深層的瞭解。

《道德經》第十一章:
三十輻共一轂,當其無,有車之用。埏埴以爲器,當其無,有器之用。鑿戶牖以爲室,當其無,有室之用。故有之以爲利,無之以爲用。
譯文:三十根輻條彙集到一根轂中的孔洞當中,有了車轂中空的地方,纔有車的作用。揉和陶土做成器皿,有了器具中空的地方,纔有器皿的作用。開鑿門窗建造房屋,有了門窗四壁內的空虛部分,纔有房屋的作用。所以,“有”給人便利,“無”發揮了它的作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章