Android | 一個隨機播放網絡音樂的小 Demo

前言

是這樣,前幾天接觸到一個能夠隨機獲取網絡音樂及其熱評的 API(關於該API:github.com/isecret/yun… ),因而乎就想着要作一個小 demo 來練練手吧!java

目前的效果就是上面那個樣子。git

我目前有打算把這個 demo 長期維護下去,後面會加入更多功能,例如收藏、下載等。github

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

需求

需求很簡單,就是經過 API 隨機獲取一首在線音樂及其某一條熱評,實現音樂的後臺播放、暫停、隨機切換,顯示熱評及其點贊數。api

API 返回的數據示例以下:bash

{
  "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…app

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

圖片加載: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() 解綁,要記住了啊!

相關文章
相關標籤/搜索