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);
    }

好了,先整理到这里。下一篇将整理音乐发送系统通知栏的相关知识,以及滚动歌词的绘制

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