Android | 一个随机播放网络音乐的小 Demo

3,235 阅读4分钟

前言

是这样,前几天接触到一个可以随机获取网络音乐及其热评的 API(关于该API:github.com/isecret/yun… ),于是乎就想着要做一个小 demo 来练练手吧!

目前的效果就是上面那个样子。

我目前有打算把这个 demo 长期维护下去,后面会加入更多功能,例如收藏、下载等。

GitHub 地址:github.com/MzoneCL/Onl…

需求

需求很简单,就是通过 API 随机获取一首在线音乐及其某一条热评,实现音乐的后台播放、暂停、随机切换,显示热评及其点赞数。

API 返回的数据示例如下:

{
  "song_id": 400162138,
  "title": "海阔天空",
  "images": "https://p1.music.126.net/a9oLdcFPhqQyuouJzG2mAQ==/3273246124149810.jpg",
  "author": "Beyond",
  "album": "华纳23周年纪念精选系列",
  "description": "歌手:Beyond。所属专辑:华纳23周年纪念精选系列。",
  "mp3_url": "https://api.comments.hk/music/400162138",
  "pub_date": "2001-08-31 16:00:00",
  "comment_id": 168923809,
  "comment_user_id": 6942157,
  "comment_nickname": "斑马斑斑",
  "comment_avatar_url": "https://p1.music.126.net/O-z-71Ffl1VimPDElVDKcQ==/6057209557649645.jpg",
  "comment_liked_count": 105599,
  "comment_content": "如果家驹没走,现在是什么样的存在?",
  "comment_pub_date": "2016-06-14 12:26:33"
}

参数释义:

用到的库

网络请求:OkHttp - github.com/square/okht…

json 解析:Gson - github.com/google/gson

图片加载:Glide - github.com/bumptech/gl…

背景虚化:Glide-transformations - github.com/wasabeef/gl…

OK,那接下来就看看是如何一点一点做出来的吧!

代码实现

音乐播放 - Service + MediaPlayer

public class MusicPlayService extends Service {
    private static String TAG = "MusicPlayService";

    MediaPlayer mediaPlayer;

    boolean firstTimePlay;

    public MusicPlayService(){}

    @Override
    public void onCreate() {
        super.onCreate();
        if (mediaPlayer == null){
            mediaPlayer = new MediaPlayer();
            firstTimePlay = true;
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new MyMusicPlayBinder();
    }

    public class MyMusicPlayBinder extends Binder {
        public void playMusic(){
            if (!mediaPlayer.isPlaying())
                mediaPlayer.start();
        }

        public void pauseMusic(){
            if (mediaPlayer.isPlaying())
                mediaPlayer.pause();
        }

        public void playRandomMusic(String url, final OnNetworkMusicPreparedListener onNetworkMusicPreparedListener) {
            try {
                mediaPlayer.stop();
                mediaPlayer.reset();
                mediaPlayer.setDataSource(url);
                mediaPlayer.prepareAsync();
                mediaPlayer.setLooping(true); // 循环播放
                mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(MediaPlayer mp) {
                        mediaPlayer.start();
                        onNetworkMusicPreparedListener.onPrepared();
                        firstTimePlay = false;
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public boolean isPlaying(){
            return mediaPlayer.isPlaying();
        }

        public int getMusicDuration(){
            return mediaPlayer.getDuration();
        }

        // 获取当前播放进度
        public int getCurPosition(){
            return mediaPlayer.getCurrentPosition();
        }

        public boolean isFirstTimePlay(){
            return firstTimePlay;
        }
    }

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

分析一下主要代码。

首先声明一个了 MediaPlayer 对象(第 4 行),用于音乐的播放。随后在 Service 被创建的时候对其进行初始化(第 14 行)。

定义了一个内部类 MyMusicPlayBinder(29 - 76 行),用以和 Activity 通信。在该类中,定义了一系列方法(播放、暂停、获取进度等等)供 Activity 调用。并且在 onBind() 方法中返回了一个 MyMusicPlayBinder 实例(26 行)。

这其中注意一下 playRandomMusic(40 - 58 行) 方法,由于我们播放的是网络音乐,所以要调用的是 mediaPlayer.prepareAsync(),即异步准备,然后必须要设置回调,即 mediaPlayer.setOnPreparedListener(),该回调会在 mediaPlayer 准备好之后被调用,我们应该在该回调中开始播放(调用 mediaPlayer.start())。

界面实现

接下来看看主界面的实现。

还是先看代码吧:

(为了避免代码看起来过长,省略了一些不重要的代码)

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static String TAG = "MainActivity";
    private int IMAGE_SOURCE_PLAY = R.drawable.play_white;
    private int IMAGE_SOURCE_PAUSE = R.drawable.pause_white;

    /*
    控件声明 省略
    */

    private MusicPlayService.MyMusicPlayBinder musicController;

    ObjectAnimator objectAnimator; // 图片旋转动画
    SeekBar seekBar; // 进度条
    private ServiceConnection serviceConnection = new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            musicController = (MusicPlayService.MyMusicPlayBinder) service;
            if (musicController.isPlaying())
                btn_play_or_pause.setImageResource(IMAGE_SOURCE_PAUSE);
            else
                btn_play_or_pause.setImageResource(IMAGE_SOURCE_PLAY);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {}
    };

    Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initService();

        if (savedInstanceState != null)
            tv_music_title.setText(savedInstanceState.getString("musicTitle"));
    }
    
    private void initView(){
        /*
        各个控件的初始化 省略
        */

        seekBar = (SeekBar) findViewById(R.id.music_progress_seek_bar);

        // 禁止拖动 点击
        seekBar.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true;
            }
        });

        objectAnimator = ObjectAnimator.ofFloat(image_view_music, "rotation", 0, 360);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.setDuration(20 * 1000);
        objectAnimator.setRepeatCount(ValueAnimator.INFINITE);//Animation.INFINITE 表示重复多次
        objectAnimator.setRepeatMode(ValueAnimator.RESTART);//RESTART表示从头开始,REVERSE表示从末尾倒播

        btn_play_or_pause.setOnClickListener(this);
        btn_next_random.setOnClickListener(this);
    }

    private void initService(){
        Intent intent = new Intent(this, MusicPlayService.class);
        startService(intent);
        bindService(intent, serviceConnection, BIND_AUTO_CREATE);
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_play_or_pause:
                playOrPauseMusic();
                break;
            case R.id.next_random:
                nextRandomMusic();
            default:
                break;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void playMusic(){
        musicController.playMusic();
        objectAnimator.resume();
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void pauseMusic(){
        musicController.pauseMusic();
        objectAnimator.pause();
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void playOrPauseMusic(){

        if (musicController.isFirstTimePlay()){
            Toast.makeText(MainActivity.this, "没有正在播放的音乐!", Toast.LENGTH_SHORT).show();
            return;
        }

        if (musicController.isPlaying()){
            pauseMusic();
            btn_play_or_pause.setImageResource(IMAGE_SOURCE_PLAY);
        }else {
            playMusic();
            btn_play_or_pause.setImageResource(IMAGE_SOURCE_PAUSE);
        }
    }

    private void nextRandomMusic(){
        btn_play_or_pause.setImageResource(IMAGE_SOURCE_PLAY);
        MusicModel.getRandomMusic(new GetRandomMusicListener() {
            @Override
            public void onSuccess(final Music music) {
                LogUtil.e(TAG, music.getDescription());
                musicController.playRandomMusic(music.getMp3_url(), new OnNetworkMusicPreparedListener() {
                    @Override
                    public void onPrepared() {
                        btn_play_or_pause.setImageResource(IMAGE_SOURCE_PAUSE);
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                tv_music_title.setText(music.getTitle());
                                tv_music_desc.setText(music.getDescription().replaceAll("。", "  "));
                                tv_comment.setText(music.getComment_content());
                                tv_comment_time.setText(music.getComment_pub_date());
                                tv_comment_username.setText(music.getComment_nickname());
                                tv_liked_count.setText(music.getComment_liked_count() + "");

                                seekBar.setMax(musicController.getMusicDuration());
                                new UpdateProgressThread().start();
                                tv_music_total_time.setText(TimeTool.format(musicController.getMusicDuration()));

                                Glide.with(MainActivity.this).load(music.getImages()).into(image_view_music);
                                Glide.with(MainActivity.this).load(music.getImages()).
                                        apply(RequestOptions.bitmapTransform(new BlurTransformation(50,10))).into(image_view_bg);
                                Glide.with(MainActivity.this).load(music.getComment_avatar_url()).placeholder(R.drawable.placeholder).into(image_view_user_avatar);
                                objectAnimator.start();
                            }
                        });
                    }
                });
            }

            @Override
            public void onFailed() {
                LogUtil.e(TAG, "获取音乐失败!");
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);
    }

    @Override
    public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
        super.onSaveInstanceState(outState, outPersistentState);
        outState.putString("musicTitle", tv_music_title.getText().toString());
    }

    class UpdateProgressThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (seekBar.getProgress() < seekBar.getMax()){
                final int curPosition = musicController.getCurPosition();
                seekBar.setProgress(curPosition);
                SystemClock.sleep(1000);
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_music_cur_time.setText(TimeTool.format(curPosition));
                    }
                });
            }
        }
    }
}

简单分析下代码。首先第 14 - 25 行,定义了一个 serviceConnection,用于 Activity 和 Service 建立连接。27 行定义一个 Handler 用于界面的更新(切换线程更新界面)。第 56 - 60 行初始化 objectAnimator,并将其与 image_view_music 绑定,该动画用于歌曲图片的旋转。169 - 185 行,定义了一个内部类 UpdateProgressThread,这个线程类是用来 SeekBar 的更新的,也就是实时更新播放进度条,由于不能在子线程更新 UI,所以必须要是用 mHandler.post() 切换到主线程去更新。

主要的代码差不多就这些了。

琐碎

  1. 在使用 Okhttp 的时候,连续多次调用 response.body().string() 会导致 java.lang.IllegalStateException: closed 错误。

  2. Service 的绑定(bindService())实际上是一个异步的过程。

  3. 使用 bindService() 绑定服务就一定要调用 unbindService() 解绑,要记住了啊!