Android開源音樂播放器之播放器基本功能

系列文章

前言

音樂播放器是我們最常用的應用之一晰骑,也是每部手機都會預裝的應用蕉毯。作為一個合格的音樂播放器曼库,應該具有哪些功能呢惹悄?“無非是播放脆贵、暫停忆植、切換歌曲、進度調節(jié)法褥、切換播放模式茫叭、專輯封面顯示、歌詞顯示半等、歌曲列表揍愁、歌曲管理(由于國產手機大多都是修改過的Android系統(tǒng),因此系統(tǒng)自帶播放器功能也不一樣杀饵,這里以Android原生播放器為參考)這些功能” 一開始我也是這么認為的吗垮,但當我著手做的時候,才發(fā)現(xiàn)這些功能遠遠不夠凹髓。如手機來電時,音樂需要自動暫停播放怯屉,耳機拔出時蔚舀,同樣需要暫停,還要支持耳機線控锨络,等等赌躺,這些都是需要我們考慮的。

一個合格的音樂播放器應該具有哪些基本素質羡儿?

由于播放礼患、暫停、切換歌曲掠归、進度調節(jié)等這些功能過于簡單缅叠,因此不過多討論,這里只討論一些容易被忽略的功能虏冻。

掃描本地音樂

掃描歌曲是播放器的基本功能肤粱,一般通過ContentProvider配合Media相關類查詢系統(tǒng)數(shù)據(jù)庫,獲得媒體庫中的歌曲信息厨相。

/**
 * 掃描歌曲
 */
public static void scanMusic(Context context, List<Music> musicList) {
    musicList.clear();
    Cursor cursor = context.getContentResolver().query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            new String[]{
                    BaseColumns._ID,
                    MediaStore.Audio.AudioColumns.IS_MUSIC,
                    MediaStore.Audio.AudioColumns.TITLE,
                    MediaStore.Audio.AudioColumns.ARTIST,
                    MediaStore.Audio.AudioColumns.ALBUM,
                    MediaStore.Audio.AudioColumns.ALBUM_ID,
                    MediaStore.Audio.AudioColumns.DATA,
                    MediaStore.Audio.AudioColumns.DISPLAY_NAME,
                    MediaStore.Audio.AudioColumns.SIZE,
                    MediaStore.Audio.AudioColumns.DURATION
            },
            null,
            null,
            MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
    if (cursor == null) {
        return;
    }
    while (cursor.moveToNext()) {
        // 是否為音樂
        int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC));
        if (isMusic == 0) {
            continue;
        }
        long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
        // 標題
        String title = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));
        // 藝術家
        String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST));
        // 專輯
        String album = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM)));
        // 專輯封面id领曼,根據(jù)該id可以獲得專輯封面圖片
        long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
        // 持續(xù)時間
        long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
        // 音樂文件路徑
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA));
        // 音樂文件名
        String fileName = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DISPLAY_NAME)));
        // 音樂文件大小
        long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE));
        Music music = new Music();
        music.set...
        musicList.add(music);
    }
    cursor.close();
}

/**
 * 從媒體庫加載封面
 */
private Bitmap loadCoverFromMediaStore(long albumId) {
    ContentResolver resolver = mContext.getContentResolver();
    Uri uri = MusicUtils.getMediaStoreAlbumCoverUri(albumId);
    InputStream is;
    try {
        is = resolver.openInputStream(uri);
    } catch (FileNotFoundException ignored) {
        return null;
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    return BitmapFactory.decodeStream(is, null, options);
}

通過以上方法基本可以獲得音樂的所有信息鸥鹉,弊端是依賴于Android系統(tǒng)媒體庫,有時新增音樂后沒有通知系統(tǒng)掃描庶骄,就無法獲得該音樂的信息毁渗,不夠靈活。

避免播放器內存被系統(tǒng)回收

我們都知道Android系統(tǒng)有自動回收內存機制单刁,如果系統(tǒng)內存緊張灸异,就會觸發(fā)該機制,應用就有可能被回收幻碱,不過Android提供了前臺機制绎狭,保證內存不足時也不會回收該應用。

/**
 * 播放時啟動前臺機制
 */
public static void showPlay(Music music) {
    playService.startForeground(NOTIFICATION_ID, buildNotification(playService, music, true));
}

/**
 * 暫停時取消前臺機制
 */
public static void showPause(Music music) {
    playService.stopForeground(false);
    notificationManager.notify(NOTIFICATION_ID, buildNotification(playService, music, false));
}

捕獲/丟棄音樂焦點

大家可能不懂這個標題是什么意思褥傍,別著急儡嘶,讓我細細道來。
大家有沒有試過恍风,如果手機上安裝了兩個音樂播放器蹦狂,當一個正在播放的時候,打開第二個播放歌曲朋贬,有沒有發(fā)現(xiàn)第一個自動暫停了凯楔?
或者正在聽歌時來電話了,音樂暫停了锦募,掛斷電話后音樂又繼續(xù)播放了摆屯,
或者收到通知的時候音樂的音量變小了一下又恢復。

“-納尼糠亩!難道不是自動暫停虐骑?”
“-圖樣圖森破!”

這其實是因為播放器在后臺處理了音頻焦點的原因赎线。

public class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener {
    private PlayService mPlayService;
    private AudioManager mAudioManager;
    private boolean isPausedByFocusLossTransient;
    private int mVolumeWhenFocusLossTransientCanDuck;

    public AudioFocusManager(@NonNull PlayService playService) {
        mPlayService = playService;
        mAudioManager = (AudioManager) playService.getSystemService(AUDIO_SERVICE);
    }

    /**
     * 播放音樂前先請求音頻焦點
     */
    public boolean requestAudioFocus() {
        return mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

    /**
     * 退出播放器后不再占用音頻焦點
     */
    public void abandonAudioFocus() {
        mAudioManager.abandonAudioFocus(this);
    }

    /**
     * 音頻焦點監(jiān)聽回調
     */
    @Override
    public void onAudioFocusChange(int focusChange) {
        int volume;
        switch (focusChange) {
            // 重新獲得焦點
            case AudioManager.AUDIOFOCUS_GAIN:
                if (!willPlay() && isPausedByFocusLossTransient) {
                    // 通話結束廷没,恢復播放
                    mPlayService.playPause();
                }

                volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume == mVolumeWhenFocusLossTransientCanDuck / 2) {
                    // 恢復音量
                    mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck,
                            AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                }

                isPausedByFocusLossTransient = false;
                mVolumeWhenFocusLossTransientCanDuck = 0;
                break;
            // 永久丟失焦點,如被其他播放器搶占
            case AudioManager.AUDIOFOCUS_LOSS:
                if (willPlay()) {
                    forceStop();
                }
                break;
            // 短暫丟失焦點垂寥,如來電
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                if (willPlay()) {
                    forceStop();
                    isPausedByFocusLossTransient = true;
                }
                break;
            // 瞬間丟失焦點颠黎,如通知
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 音量減小為一半
                volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                if (willPlay() && volume > 0) {
                    mVolumeWhenFocusLossTransientCanDuck = volume;
                    mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck / 2,
                            AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                }
                break;
        }
    }

    private boolean willPlay() {
        return mPlayService.isPreparing() || mPlayService.isPlaying();
    }
}

耳機拔出時暫停播放

“-納尼!難道耳機拔出時不是自動暫停嗎滞项?”
“-圖樣……”

private IntentFilter mNoisyFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

public class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 耳機拔出時暫停播放
        PlayService.startCommand(context, Actions.ACTION_MEDIA_PLAY_PAUSE);
    }
}

播放時注冊廣播接收器狭归,暫停時取消注冊即可。

聯(lián)動系統(tǒng)媒體中心

這個標題大家可能也不懂文判,先放張圖吧


明白了吧唉铜,我的播放器除了播放了一首音樂之外什么都沒做,就可以分別在任務管理律杠、鎖屏潭流、負一屏控制我的播放器了竞惋,是不是感覺碉堡了。
這些圖是在我的小米手機上截的灰嫉,不保證所有手機都有這些控制功能拆宛,但是只要你的Android版本是5.0以上,應該都會有媒體中心讼撒,無非是表現(xiàn)形式不同浑厚。
Android 5.0中新增了MediaSession類,官方說明是

允許與媒體控制器根盒、音量鍵钳幅、媒體按鈕和傳輸控件交互。

一個類包含了媒體控制和線控等功能炎滞,是不是很好用敢艰。
現(xiàn)在support-v4包加入了MediaSessionCompat用于在低版本上也能使用這個高大上的功能,
但是低版本上并不能實現(xiàn)媒體控制和線控等功能册赛,低版本的線控功能我會在后面講钠导。

public class MediaSessionManager {
    private static final String TAG = "MediaSessionManager";
    private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY
            | PlaybackStateCompat.ACTION_PAUSE
            | PlaybackStateCompat.ACTION_PLAY_PAUSE
            | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
            | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
            | PlaybackStateCompat.ACTION_STOP
            | PlaybackStateCompat.ACTION_SEEK_TO;

    private PlayService mPlayService;
    private MediaSessionCompat mMediaSession;

    public MediaSessionManager(PlayService playService) {
        mPlayService = playService;
        setupMediaSession();
    }

    /**
     * 初始化并激活MediaSession
     */
    private void setupMediaSession() {
        mMediaSession = new MediaSessionCompat(mPlayService, TAG);
        mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
                | MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
        mMediaSession.setCallback(callback);
        mMediaSession.setActive(true);
    }

    /**
     * 更新播放狀態(tài),播放/暫停/拖動進度條時調用
     */
    public void updatePlaybackState() {
        int state = (mPlayService.isPlaying() || mPlayService.isPreparing())
                ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
        mMediaSession.setPlaybackState(
                new PlaybackStateCompat.Builder()
                        .setActions(MEDIA_SESSION_ACTIONS)
                        .setState(state, mPlayService.getCurrentPosition(), 1)
                        .build());
    }

    /**
     * 更新正在播放的音樂信息森瘪,切換歌曲時調用
     */
    public void updateMetaData(Music music) {
        if (music == null) {
            mMediaSession.setMetadata(null);
            return;
        }

        MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, music.getTitle())
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, music.getArtist())
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, music.getAlbum())
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, music.getArtist())
                .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, music.getDuration())
                .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, CoverLoader.getInstance().loadThumbnail(music));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, AppCache.getMusicList().size());
        }

        mMediaSession.setMetadata(metaData.build());
    }

    /**
     * 釋放MediaSession牡属,退出播放器時調用
     */
    public void release() {
        mMediaSession.setCallback(null);
        mMediaSession.setActive(false);
        mMediaSession.release();
    }

    private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
        @Override
        public void onPlay() {
            mPlayService.playPause();
        }

        @Override
        public void onPause() {
            mPlayService.playPause();
        }

        @Override
        public void onSkipToNext() {
            mPlayService.next();
        }

        @Override
        public void onSkipToPrevious() {
            mPlayService.prev();
        }

        @Override
        public void onStop() {
            mPlayService.stop();
        }

        @Override
        public void onSeekTo(long pos) {
            mPlayService.seekTo((int) pos);
        }
    };
}

耳機線控(適用于API 19及以下)

“-納尼……”
“-Shut up !”

是的,需要我們自己控制扼睬。
如果你已經(jīng)按照上面的方法激活了MediaSession逮栅,那么在5.0以上的系統(tǒng)你已經(jīng)不需要關心線控功能了,但是在5.0以下仍然需要自己監(jiān)聽耳機按鍵窗宇。

public class RemoteControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
        if (event == null || event.getAction() != KeyEvent.ACTION_UP) {
            return;
        }
        Intent serviceIntent;
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            case KeyEvent.KEYCODE_HEADSETHOOK:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_PLAY_PAUSE);
                context.startService(serviceIntent);
                break;
            case KeyEvent.KEYCODE_MEDIA_NEXT:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_NEXT);
                context.startService(serviceIntent);
                break;
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_PREVIOUS);
                context.startService(serviceIntent);
                break;
        }
    }
}

<!--在AndroidManifest中注冊Receiver-->
<receiver android:name=".receiver.RemoteControlReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

總結

感謝你能耐心的看到最后证芭,本文主要討論了音樂播放器容易忽略的重要功能,如果還有其他的本文沒有提到的担映,請大家不吝賜教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末叫潦,一起剝皮案震驚了整個濱河市蝇完,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌矗蕊,老刑警劉巖短蜕,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異傻咖,居然都是意外死亡朋魔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門卿操,熙熙樓的掌柜王于貴愁眉苦臉地迎上來警检,“玉大人孙援,你說我怎么就攤上這事∩鹊瘢” “怎么了拓售?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長镶奉。 經(jīng)常有香客問我础淤,道長,這世上最難降的妖魔是什么哨苛? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任鸽凶,我火速辦了婚禮,結果婚禮上建峭,老公的妹妹穿的比我還像新娘玻侥。我一直安慰自己,他們只是感情好迹缀,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布使碾。 她就那樣靜靜地躺著,像睡著了一般祝懂。 火紅的嫁衣襯著肌膚如雪票摇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天砚蓬,我揣著相機與錄音矢门,去河邊找鬼。 笑死灰蛙,一個胖子當著我的面吹牛祟剔,可吹牛的內容都是我干的。 我是一名探鬼主播摩梧,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼物延,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了仅父?” 一聲冷哼從身側響起叛薯,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎笙纤,沒想到半個月后耗溜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡省容,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年抖拴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腥椒。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡阿宅,死狀恐怖候衍,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情家夺,我是刑警寧澤脱柱,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站拉馋,受9級特大地震影響榨为,放射性物質發(fā)生泄漏。R本人自食惡果不足惜煌茴,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一随闺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蔓腐,春花似錦矩乐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至傀蓉,卻和暖如春欧漱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葬燎。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工误甚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谱净。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓窑邦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親壕探。 傳聞我的和親對象是個殘疾皇子冈钦,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容