Android 手機影音 開發過程記錄(五)

前一篇已經將視頻模塊弄好了,今天主要理一下音樂這一模塊,包括:

  1. 通過ContentProvider獲取音樂列表數據
  2. 音樂播放界面的動畫和佈局
  3. 音樂播放的界面AudioPlayerActivity和AudioPlayService交互。(實現播放、暫停、上下一首、播放模式的切換)

效果圖

這裏寫圖片描述 這裏寫圖片描述

結構圖

這裏寫圖片描述

獲取音樂列表數據

  1. 分析:這裏還是通過內容提供者ContentProvider去獲取。前面獲取視頻時沒有將爲什麼這樣獲取,這裏補充一下,面試概率還挺高的:使用ContentProvider可以實現不同應用程序下數據的共享,主要是通過把自己的數據通過uri的形式共享出去來實現共享的。這樣的好處就是:屏蔽數據存儲的細節,對開發者透明,我們只需要關心操作數據的uri就可以了。
  2. 觀察音樂列表,我們需要這些數據:音樂名稱(DISPLAY_NAME),音樂時長(DURATION),藝術家(ARTIST),音樂路徑路徑(DATA)。
  3. 下面是具體獲取的方法(跟獲取視頻一模一樣)。

    
    private AudioListAdapter adapter;
    private SimpleQueryHandler queryHandler;
    
    @Override
    protected void initData() {
        adapter = new AudioListAdapter(getActivity(), null, false);
        lv.setAdapter(adapter);
        queryHandler = new SimpleQueryHandler(getActivity().getContentResolver());
        String[] projection = {Media._ID, Media.DISPLAY_NAME, Media.DURATION,
                Media.DATA, Media.ARTIST};
        queryHandler.startQuery(0, adapter, Media.EXTERNAL_CONTENT_URI,
                projection, null, null, null);
    }
    
    
    class SimpleQueryHandler extends AsyncQueryHandler {
    
        public SimpleQueryHandler(ContentResolver cr) {
            super(cr);
        }
    
        /**
         * token: 查詢的標識
         */
        @Override
        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
            super.onQueryComplete(token, cookie, cursor);
            if (cookie != null && cookie instanceof CursorAdapter) {
                CursorAdapter adapter = (CursorAdapter) cookie;
                adapter.changeCursor(cursor);//相當於notifyDatasetChange
                CursorUtil.printCursor(cursor);
            }
        }
    }

至此,可以實現上面音樂的列表。


音樂播放界面的佈局和動畫

  1. 音樂之所以能播放並且能切換上/下一首,肯定在點擊listview的條目的時候把所有數據傳遞過來了。

        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Cursor cursor = adapter.getCursor();
                Bundle bundle = new Bundle();
                bundle.putInt("currentPosition", position);
                bundle.putSerializable("audioList", cursorToList(cursor));
                enterActivity(AudioPlayerActivity.class, bundle);
            }
        });
    
    private ArrayList<AudioItem> cursorToList(Cursor cursor){
        cursor.moveToPosition(-1);
        ArrayList<AudioItem> list = new ArrayList<AudioItem>();
        while (cursor.moveToNext()){
            AudioItem audioItem = AudioItem.fromCursor(cursor);
            list.add(audioItem);
        }
        return list;
    }
  2. 播放頁面佈局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/base_bg"
    android:orientation="vertical">
    
    <!--titleBar-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@mipmap/base_titlebar_bg"
        android:orientation="horizontal">
    
        <ImageView
            android:id="@+id/iv_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/selector_btn_back" />
    
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="@color/white"
            android:textSize="20sp" />
    </RelativeLayout>
    
    <!--音頻跳動的動畫和歌曲藝術家-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <ImageView
            android:id="@+id/iv_anim"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:background="@drawable/audio_player_anim" />
    
        <TextView
            android:id="@+id/tv_artist"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/iv_anim"
            android:layout_centerHorizontal="true"
            android:textColor="@color/white"
            android:textSize="16sp" />
    </RelativeLayout>
    
    <!--自定義歌詞-->
    <com.justforme.mobileplayer.ui.view.LyricView
        android:id="@+id/lyric_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">
    
        <!--歌曲進度條和歌曲時間-->
        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:textColor="@color/white"
            android:textSize="16sp" />
    
        <SeekBar
            android:id="@+id/sb_audio"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="3dp"
            android:progressDrawable="@drawable/audio_progress_drawable"
            android:thumb="@mipmap/audio_seek_thumb"
            android:thumbOffset="0dp" />
    
        <!--各種播放邏輯的控件佈局-->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp"
            android:layout_marginTop="5dp"
            android:gravity="center"
            android:orientation="horizontal">
    
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
    
                <ImageView
                    android:id="@+id/iv_play_mode"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_centerInParent="true"
                    android:background="@drawable/selector_btn_mode_order" />
            </RelativeLayout>
    
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
    
                <ImageView
                    android:id="@+id/iv_pre"
                    android:layout_width="45dp"
                    android:layout_height="45dp"
                    android:layout_centerInParent="true"
                    android:background="@drawable/selector_btn_audio_pre" />
            </RelativeLayout>
    
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
    
                <ImageView
                    android:id="@+id/iv_play"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_centerInParent="true"
                    android:background="@drawable/selector_btn_audio_pause" />
            </RelativeLayout>
    
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
    
                <ImageView
                    android:id="@+id/iv_next"
                    android:layout_width="45dp"
                    android:layout_height="45dp"
                    android:layout_centerInParent="true"
                    android:background="@drawable/selector_btn_audio_next" />
            </RelativeLayout>
    
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
    
                <ImageView
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_centerInParent="true"
                    android:background="@drawable/selector_btn_lyric" />
            </RelativeLayout>
        </LinearLayout>
    </LinearLayout>
    </LinearLayout>
  3. 音頻跳動的動畫效果 (準備9張圖片)

    
    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
        <item
            android:drawable="@mipmap/now_playing_matrix_01"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_02"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_03"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_04"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_05"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_06"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_07"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_08"
            android:duration="200" />
        <item
            android:drawable="@mipmap/now_playing_matrix_09"
            android:duration="200" />
    
    </animation-list>
       //播放頁面控件加載完成後,加載音頻跳動動畫
       AnimationDrawable animationDrawable = (AnimationDrawable) ivAnim.getBackground();
       animationDrawable.start();

Activity與Service交互

可以再看下上面的結構圖,播放音樂等相關功能是在服務中實現的,通過服務的中間人對象,實現交互。
下面這些方法是寫在服務中間人對象(AudioServiceBinder)中的。

  1. 播放音樂

    /**
     * 播放音樂
     */
    public void playAudio() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
        mediaPlayer = new MediaPlayer();
        AudioItem audioItem = audioList.get(currentPosition);
        try {
            mediaPlayer.setDataSource(AudioPlayerService.this, Uri.parse(audioItem.getPath()));
            mediaPlayer.setOnPreparedListener(onPreparedListener);
            mediaPlayer.setOnCompletionListener(onCompletionListener);
            mediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    
    }
  2. 判斷音樂是否正在播放

    public boolean isPlaying() {
         return mediaPlayer != null && mediaPlayer.isPlaying();
    }
  3. 暫停

    public void pause() {
        if (mediaPlayer != null) {
            mediaPlayer.pause();
        }
    }
  4. 播放

    public void start() {
        if (mediaPlayer != null) {
            mediaPlayer.start();
        }
    }
  5. 獲取歌曲總時間

    public long getDuration() {
        return mediaPlayer == null ? 0 : mediaPlayer.getDuration();
    }
  6. 獲取歌曲當前的時間

    public long getCurrentPosition() {
        return mediaPlayer == null ? 0 : mediaPlayer.getCurrentPosition();
    }
  7. 拖動歌曲到指定的時間

    public void seekTo(int progress) {
        if (mediaPlayer != null) {
            mediaPlayer.seekTo(progress);
        }
    }
  8. 播放上一首

    public void playPre() {
        if (currentPosition > 0) {
            currentPosition--;
            playAudio();
        }
    }
  9. 播放下一首

    public void playNext() {
        if (currentPosition < audioList.size() - 1) {
            currentPosition++;
            playAudio();
        }
    }
  10. 獲取當前正在播放的音樂的position

    public int getCurrentPlayingIndex() {
        return currentPosition;
    }
  11. 獲取音樂集合的大小

    public int getAudioListSize() {
        return audioList.size();
    }
    
  12. 切換播放模式

    public void switchPlayMode() {
    switch (playMode) {
        case MODE_ORDER://順序播放 --> 單曲循環
            playMode = MODE_SINGLE_REPEAT;
            break;
        case MODE_SINGLE_REPEAT://單曲循環 --> 循環播放
            playMode = MODE_ALL_REPEAT;
            break;
        case MODE_ALL_REPEAT://循環播放 --> 順序播放
            playMode = MODE_ORDER;
            break;
    }
        savePlayMode();
    }
  13. 保存播放模式

    private void savePlayMode() {
        SharedPreferenceUtils.putInt(AudioPlayerService.this, "playMode", playMode);
    }
  14. 獲取播放模式(讓外界獲取)

    public int getPlayMode() {
        return playMode;
    }
  15. 從SharedPreferences中獲取播放模式

    private int getPlayModeFromSp() {
        return SharedPreferenceUtils.getInt(AudioPlayerService.this, "playMode", MODE_ORDER);
    }
  16. 歌曲準備完成的監聽器

    private OnPreparedListener onPreparedListener = new OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            mediaPlayer.start();
            notifyPrepared();
        }
    };
    /**
    * 音樂準備完成時,發送廣播將數據傳給外界
    */
    private void notifyPrepared() {
        Intent intent = new Intent(ACTION_PREPARED);
        intent.putExtra("audioItem", audioList.get(currentPosition));
        sendBroadcast(intent);
    }
  17. 歌曲播放完成的監聽器

    private OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {
            notifyCompletion();
            autoPlayByPlayMode();
        }
    };
    private void notifyCompletion() {
            Intent intent = new Intent(ACTION_COMPLETION);
            intent.putExtra("audioItem", audioList.get(currentPosition));
            sendBroadcast(intent);
    }
    
  18. 根據播放模式自動播放

    private void autoPlayByPlayMode() {
        switch (playMode) {
            case MODE_ORDER:
                serviceBinder.playNext();
                break;
            case MODE_SINGLE_REPEAT:
                serviceBinder.playAudio();
                break;
            case MODE_ALL_REPEAT:
                if (currentPosition == audioList.size() - 1) {
                    currentPosition = 0;
                    serviceBinder.playAudio();
                } else {
                    serviceBinder.playNext();
                }
                break;
        }
    }

至此,service中的方法基本上寫好了。下面音樂播放的activity直接調用這些方法就可以了。

養成好習慣,服務銷燬的時候,釋放資源。

    @Override
    public void onDestroy() {
        super.onDestroy();
        mediaPlayer.release();
        mediaPlayer = null;
    }

AudioPlayerActivity的調用代碼

  1. 初始化數據

    @Override
    protected void initData() {
    
        registerAudioStateReceiver();
    
        Bundle bundle = new Bundle();
        //將音樂列表傳遞過來的數據(所有音樂的集合和當期音樂的position)傳遞給service
        bundle = getIntent().getExtras();
        Intent serviceIntent = new Intent(this, AudioPlayerService.class);
        serviceIntent.putExtras(bundle);
        bindService(serviceIntent, new AudioPlayerServiceConn(), Service.BIND_AUTO_CREATE);//爲了拿到serviceBinder對象
        startService(serviceIntent);//爲了傳遞數據
    }
    /**
     * 註冊一個音樂狀態的廣播接受者
     */
    private void registerAudioStateReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(AudioPlayerService.ACTION_PREPARED);
        filter.addAction(AudioPlayerService.ACTION_COMPLETION);
        audioServiceReceiver = new AudioServiceReceiver();
        registerReceiver(audioServiceReceiver, filter);
    }
    
    private class AudioServiceReceiver extends BroadcastReceiver {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(AudioPlayerService.ACTION_PREPARED)) {    //音樂準備完成
                AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
    
                tvTitle.setText(StringUtil.formatAudioName(audioItem.getTitle()));
                tvArtist.setText(audioItem.getArtist());
                tvTime.setText("00:00/" + StringUtil.formatVideoDuration(audioItem.getDuration()));
                sbAudio.setMax((int) audioItem.getDuration());
    
                updatePlayProgress();
                updatePlayModeBtnBg(false);
    
            } else if (action.equals(AudioPlayerService.ACTION_COMPLETION)) {    //音樂播放完成
                handler.removeMessages(UPDATE_PROGRESS);
                AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
                tvTime.setText(StringUtil.formatVideoDuration(audioItem.getDuration()) +
                        "/" + StringUtil.formatVideoDuration(audioItem.getDuration()));
                sbAudio.setProgress((int) audioItem.getDuration());
                ivPlay.setBackgroundResource(R.drawable.selector_btn_audio_play);
    
            }
        }
    }
    /**
     * 更新播放進度
     */
    private void updatePlayProgress() {
        long currentProgress = serviceBinder.getCurrentPosition();
        tvTime.setText(StringUtil.formatVideoDuration(currentProgress) + "/" + StringUtil.formatVideoDuration(serviceBinder.getDuration()));
        sbAudio.setProgress((int) currentProgress);
        handler.sendEmptyMessageDelayed(UPDATE_PROGRESS, 1000);
    
    }
  2. 控件的點擊事件

    @Override
    protected void processClick(View view) {
        switch (view.getId()) {
            case R.id.iv_back:
                finish();
                break;
            case R.id.iv_play_mode:
                serviceBinder.switchPlayMode();
                updatePlayModeBtnBg(true);
                break;
            case R.id.iv_pre:
                if (serviceBinder.getCurrentPlayingIndex() != 0) {
                    serviceBinder.playPre();
                } else {
                    ToastUtil.showShort(this, "已經是第一首了");
                }
                break;
            case R.id.iv_play:
                if (serviceBinder.isPlaying()) {
                    serviceBinder.pause();
                    handler.removeMessages(UPDATE_PROGRESS);
                    handler.removeMessages(UPDATE_LYRIC);
                } else {
                    serviceBinder.start();
                    handler.sendEmptyMessage(UPDATE_PROGRESS);
                    handler.sendEmptyMessage(UPDATE_LYRIC);
                }
                updatePlayBtnBg();
                break;
            case R.id.iv_next:
                if (serviceBinder.getCurrentPlayingIndex() != serviceBinder.getAudioListSize() - 1) {
                    serviceBinder.playNext();
                } else {
                    ToastUtil.showShort(this, "已經是最後一首了");
                }
                break;
        }
    }
    
    /**更新播放模式按鈕的背景
     * @param isShowTip 是否顯示建議(切換時的toast提醒)
     */
    private void updatePlayModeBtnBg(boolean isShowTip) {
        int playMode = serviceBinder.getPlayMode();
        switch (playMode) {
            case AudioPlayerService.MODE_ORDER:
                if (isShowTip) ToastUtil.showShort(this, "順序播放");
                ivPlayMode.setBackgroundResource(R.drawable.selector_btn_mode_order);
                break;
            case AudioPlayerService.MODE_SINGLE_REPEAT:
                if (isShowTip) ToastUtil.showShort(this, "單曲循環");
                ivPlayMode.setBackgroundResource(R.drawable.selector_btn_mode_single_repeat);
                break;
            case AudioPlayerService.MODE_ALL_REPEAT:
                if (isShowTip) ToastUtil.showShort(this, "循環播放");
                ivPlayMode.setBackgroundResource(R.drawable.selector_btn_mode_all_repeat);
                break;
        }
    
    }
    /**
     * 更新播放按鈕的背景
     */
    private void updatePlayBtnBg() {
        ivPlay.setBackgroundResource(serviceBinder.isPlaying() ?
                R.drawable.selector_btn_audio_pause :
                R.drawable.selector_btn_audio_play);
    }

基本功能完成了,記住在activity銷燬的時候,移除一些消息和廣播。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(audioServiceReceiver);
        handler.removeCallbacksAndMessages(null);
    }

好了,先整理到這裏。下一篇將整理音樂發送系統通知欄的相關知識,以及滾動歌詞的繪製

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