Android音乐播放器开发(5)—播放界面

Android音乐播放器开发(5)—播放界面

1. 说明

本音乐播放器基于Android开发,原为我和另外两个小伙伴在上学期间一起做的一个小项目,近来有时间整理一下。之前我有文章已经介绍了播放界面的功能实现(Android音乐播放器开发),但介绍的比较粗糙,接下来会做更细致化的整理。源码已同步到Gitee仓库,以后也会放到GitHub仓库,觉得还不错的话帮忙点个“star”吧,非常感谢。

当初代码写的很随意,目的只为实现功能。现在更倾向于代码可读性和简洁性,因此会在原来的程序基础上做一些小修改。也有可能不会一步到位,计划慢慢修改,以增强自己的理解。

服务端使用的是比较传统的servlet和jdbc传递数据,整理完之后,新版本会修改为SSM框架,更加简洁高效。安卓端使用的也都是基础的工具,比如音乐播放功能的实现也是借助于入门级的MediaPlayer类,目前关于安卓端没有什么更改的想法。

服务端:Android音乐播放器开发–服务端

登录:Android音乐播放器开发–登录

注册:Android音乐播放器开发–注册

修改密码:Android音乐播放器开发–修改密码

(适用于平时做个小课设的小伙伴们)

2. 界面设计

首先为播放器设计一个播放界面

播放界面设计到的功能包括:

image-20201009234316706

其中功能按钮除去上述介绍的,后续调试中需要添加一个关闭服务的按钮,暂且也将其放在该界面

使用xml文件进行界面设计,命名为activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:padding="16dp" android:layout_height="match_parent" android:gravity="center_horizontal" android:background="@drawable/background" >
    <Button android:id="@+id/quit_btn" android:layout_gravity="left" android:background="@drawable/kaiguan" android:layout_width="25dp" android:layout_height="25dp"/>
    <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="4" >

        <com.loopj.android.image.SmartImageView android:layout_width="260dp" android:layout_height="260dp" android:id="@+id/siv_icon" android:src="@drawable/default_record_album" android:layout_centerInParent="true"/>

    </RelativeLayout>

    <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1">

        <TextView android:id="@+id/text_view_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="8dp" android:textSize="26dp" android:textColor="#FFFFFF" android:text="歌名" />


        <TextView android:id="@+id/text_view_artist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_below="@id/text_view_name" android:text="演唱者" android:textColor="#FFFFFF" android:textSize="20dp" />

    </RelativeLayout>

    <LinearLayout android:id="@+id/layout_progress" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="40dp" android:gravity="center_vertical" >
        <SeekBar android:layout_width="match_parent" android:id="@+id/seek_bar" android:max="100" style="@style/Widget.AppCompat.SeekBar" android:layout_height="wrap_content" />
    </LinearLayout>
    <LinearLayout android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginBottom="8dp" android:showDividers="middle" android:gravity="center">

        <Button android:id="@+id/play_way_btn" android:layout_width="36dp" android:background="@drawable/xunhuanbofang" android:layout_marginRight="16dp" android:layout_height="36dp" />
        <Button android:id="@+id/play_last_btn" android:layout_width="40dp" android:layout_marginRight="16dp" android:background="@drawable/last" android:layout_height="40dp" />
        <Button android:id="@+id/play_or_pause_btn" android:layout_width="55dp" android:gravity="center" android:background="@drawable/bofang" android:layout_height="55dp" />
        <Button android:id="@+id/play_next_btn" android:layout_width="40dp" android:layout_marginLeft="16dp" android:background="@drawable/next" android:layout_height="40dp" />
        <Button android:id="@+id/play_menu_btn" android:layout_width="40dp" android:layout_marginLeft="16dp" android:background="@drawable/menu" android:layout_height="40dp" />

    </LinearLayout>
</LinearLayout>

图标全部来自阿里巴巴图标矢量库

布局文件比较简单,这里就不做过多介绍了,大致界面和布局如下图所示:

image-20201009234821964

3. 播放器功能实现

3.1 两个接口

首先梳理一下整体思路,由于这里的功能非常多,涉及到后台逻辑和界面的切换,以及进度条的更新,在这里设置了两个接口,用于分离逻辑层表现层。逻辑层的方法主要有播放上一首playLast(),播放/暂停playOrPause(),播放下一首playNext(),停止播放stopPlay(),设置播放进度seekTo();表现层方法主要有播放状态的通知onPlayerStateChange()和播放进度的改变onSeekChange(),用于更新UI。

image-20201028211642621

image-20201028211813209

PlayerControl.java

public interface PlayerControl {
   
    /* *播放 */
    void playOrPause();

    /* 播放上一首 */
    void play_last();

    /* 播放下一首 */
    void play_next();

    /* 停止播放 */
    void stopPlay();

    /* 设置播放进度 */
    void seekTo(int seek);
}

PlayerViewControl.java

public interface PlayerViewControl {
   
    /* 播放状态的通知 */
    void onPlayerStateChange(int state);

    /* 播放进度的改变 */
    void onSeekChange(int seek);
}

PlayerControl接口的功能由PlayerPresenter实现。

有了两个接口,先不着急实现。现在可以写初始化播放界面的一些内容了。

3.2 初始化用户信息

在初始化播放界面之前,先要对用户信息进行初始化,因为界面的初始化依赖于用户信息的歌曲id和播放模式。

private String account;    //账户
private int musicId;   //歌曲id
public int playPattern;  //播放模式

//初始化用户信息
private void initUserData(){
   
    Intent intent = getIntent();

    String userStr = intent.getStringExtra("result");
    JSONObject userData = RequestServlet.getJSON(userStr);
    account = userData.optString("account");
    musicId = userData.optInt("music_id");
    playPattern = userData.optInt("pattern");
}

3.3 初始化播放界面

private SeekBar mSeekBar;  //进度条
private Button mPlayOrPause;
private Button mPlayPattern;
private Button mPlayLast;
private Button mPlayNext;
private Button mPlayMenu;
private Button mQuit;
private TextView mMusicName;
private TextView mMusicArtist;
private SmartImageView mMusicPic;

public final int PLAY_IN_ORDER = 0;   //顺序播放
public final int PLAY_RANDOM = 1;    //随机播放
public final int PLAY_SINGLE = 2;    //单曲循环

//初始化界面
private void initView(){
   
    mSeekBar = (SeekBar) this.findViewById(R.id.seek_bar);
    mPlayOrPause = (Button) this.findViewById(R.id.play_or_pause_btn);
    mPlayPattern = (Button) this.findViewById(R.id.play_way_btn);
    mPlayLast= (Button) this.findViewById(R.id.play_last_btn);
    mPlayNext = (Button) this.findViewById(R.id.play_next_btn);
    mPlayMenu = (Button) this.findViewById(R.id.play_menu_btn);
    mQuit=(Button) this.findViewById(R.id.quit_btn);
    mMusicName = (TextView) this.findViewById(R.id.text_view_name);
    mMusicArtist = (TextView) this.findViewById(R.id.text_view_artist);
    mMusicPic = (SmartImageView) this.findViewById(R.id.siv_icon);

    //模式转换
    if (playPattern==PLAY_IN_ORDER) {
   
        mPlayPattern.setBackgroundResource(R.drawable.xunhuanbofang);
    }else if(playPattern==PLAY_RANDOM){
   
        mPlayPattern.setBackgroundResource(R.drawable.suijibofang);
    } else if (playPattern==PLAY_SINGLE) {
   
        mPlayPattern.setBackgroundResource(R.drawable.danquxunhuan);
    }

    //获取音乐列表
    getMusicListThread();
}

初始化播放界面包含三个方面:

  • 绑定xml文件中的所有控件
  • 根据在数据库中解析出的用户播放模式更换界面中播放模式按钮的图标,这里还无法加载歌曲信息,因为还未获取到歌曲资源
  • 从数据库中获取音乐列表,并根据在用户信息中解析出的musicId,将歌曲信息也初始化到界面中,这里使用了一个子线程getMusicListThread

getMusicListThread

获取音乐列表需要在服务端获取数据,需要开启一个子线程。这里调用了RequestServlet类中的getMusicList方法(这里可以在RequestServlet类中新建这个方法,后续再进行实现)

public static JSONArray sMusicList;   //歌曲列表
public int songNum = 0;  //歌曲总数

//获取音乐列表
private void getMusicListThread(){
   
    new Thread(){
   
        @Override
        public void run() {
   
            try{
   
                JSONArray result = RequestServlet.getMusicList();
                Message msg = new Message();
                msg.what = 2;
                msg.obj = result;
                handler2.sendMessage(msg);
            }
            catch (Exception e){
   
                e.printStackTrace();
            }
        }
    }.start();
}

private Handler handler2 = new Handler(){
   
    public void handleMessage(android.os.Message msg) {
   
        try {
   
            if (msg.what == 2) {
   
                sMusicList = (JSONArray) msg.obj;
                songNum = sMusicList.length();

                //根据用户数据和歌曲列表初始化有关歌曲的界面
                setMusicView(IsPlay.notPlay);
            }
        }catch (Exception e) {
   
            e.printStackTrace();
        }
    }
};

子线程在服务端获取音乐列表,将其传递到主线程,主线程调用**setMusicView()**方法初始化歌曲相关的界面

setMusicView

在初始化歌曲信息之前,我们已经拿到了用户信息和歌曲列表,现在可以根据在用户信息中解析出的musicId在歌曲列表中获取单条歌曲信息,然后将该歌曲信息初始化到界面中。

在正式介绍setMusicView()方法之前,可以看到上面在调用该方法之前传递了一个参数,这里使用了一个枚举类型,用于区分是否需要播放。像现在初始化界面,我们是不需要进行歌曲播放的,而在切换上/下一首时,除了更换歌曲信息外,我们还需要对歌曲进行播放。

public enum IsPlay{
   
    play, notPlay
}
public String playAddress;  //音乐文件地址
public static final String IMG = "http://10.0.2.2:8080/musicplayer/image/";    //音乐图片的通用地址

//设置有关歌曲的界面
public void setMusicView(IsPlay playState){
   
    try {
   
        JSONObject musicInfo = (JSONObject) sMusicList.get(musicId);
        String name = musicInfo.optString("name");
        String author = musicInfo.optString("author");
        String img = musicInfo.optString("img");
        playAddress=musicInfo.optString("address");
        mMusicPic.setImageUrl(IMG+img,R.mipmap.ic_launcher,R.mipmap.ic_launcher);   //设置界面上的歌曲封面
        mMusicName.setText(name);   //设置界面上的歌曲名
        mMusicArtist.setText(author);   //设置界面上的演唱者
    } catch (Exception e) {
   
        e.printStackTrace();
    }
    if(playState == IsPlay.play){
   
        if ( mPlayerControl != null) {
   
            mPlayerControl.stopPlay();
        }
        mPlayerControl.playOrPause(playState);
    }
}

可以看到,如果需要播放的话,需要将play参数传递到该方法内,方法内判断信息,首先调用了**stopPlay()方法,再调用playOrPause()**方法。为什么先要停止播放?是因为mediaplayer的一个特性,如果需要切换歌曲的话,首先要释放掉mediaplayer资源,再实例化一个对象来加载新的资源才可以。

3.4 初始化事件

涉及到几个按钮的点击事件

private PlayerControl playerControl = new PlayerPresenter(this);

//初始化事件
private void initEvent(){
   

    //播放/暂停按钮
    mPlayOrPause.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            if(mPlayerControl!=null){
   
                mPlayerControl.playOrPause(IsPlay.notPlay);
            }
        }
    });

    //播放上一首
    mPlayLast.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            if(mPlayerControl!=null){
   
                mPlayerControl.playLast();
            }
        }
    });

    //播放下一首
    mPlayNext.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            if(mPlayerControl!=null){
   
                mPlayerControl.playNext();
            }
        }
    });

    //播放模式
    mPlayPattern.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            playPattern = (playPattern+1)%3;

            if (playPattern==PLAY_IN_ORDER) {
   
                mPlayPattern.setBackgroundResource(R.drawable.xunhuanbofang);
            }else if(playPattern==PLAY_RANDOM){
   
                mPlayPattern.setBackgroundResource(R.drawable.suijibofang);
            } else if (playPattern==PLAY_SINGLE) {
   
                mPlayPattern.setBackgroundResource(R.drawable.danquxunhuan);
            }
        }
    });

    //音乐列表
    mPlayMenu.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            Intent intent = new Intent(MainActivity.this,MusicListActivity.class);
            startActivity(intent);
        }
    });

    //退出按钮
    mQuit.setOnClickListener(new View.OnClickListener() {
   
        @Override
        public void onClick(View view) {
   
            Toast.makeText(MainActivity.this, "正在保存信息…", Toast.LENGTH_SHORT).show();
            saveDataToDB();
        }
    });

    //进度条
    mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
   
            //进度条发生改变
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
   
            //手已经触摸上去了拖动
            isUserTouchProgressBar=true;
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
   

            int touchProgress=seekBar.getProgress();
            //停止拖动
            if ( mPlayerControl != null) {
   
                mPlayerControl.seekTo(touchProgress);
            }
            isUserTouchProgressBar=false;
        }
    });
}

PlayerControl就是逻辑层的接口,PlayerPresenter是实现该接口功能的实现类,这里将mainactivity作为参数进行传递,方便调用mainactivity的一些参数和方法。

播放/暂停、播放上一首、播放下一首按钮的点击,直接交给PlayerControl接口处理即可。

播放模式按钮被点击,按照顺序播放、随机播放、单曲循环的顺序切换播放模式,这里做了个求余操作,使playPattern的值始终保持在0、1、2之间。根据playPattern数值的不同,更改播放模式按钮的图标。

public final int PLAY_IN_ORDER = 0;   //顺序播放
public final int PLAY_RANDOM = 1;    //随机播放
public final int PLAY_SINGLE = 2;    //单曲循环


mPlayPattern.setOnClickListener(new View.OnClickListener() {
   
    @Override
    public void onClick(View view) {
   
        playPattern = (playPattern+1)%3;

        if (playPattern==PLAY_IN_ORDER) {
   
            mPlayPattern.setBackgroundResource(R.drawable.xunhuanbofang);
        }else if(playPattern==PLAY_RANDOM){
   
            mPlayPattern.setBackgroundResource(R.drawable.suijibofang);
        } else if (playPattern==PLAY_SINGLE) {
   
            mPlayPattern.setBackgroundResource(R.drawable.danquxunhuan);
        }
    }
});

退出按钮被点击,需要保存当前用户信息(播放模式、所播歌曲id)到数据库中,因此开启了子线程实现。

mQuit.setOnClickListener(new View.OnClickListener() {
   
    @Override
    public void onClick(View view) {
   
        Toast.makeText(MainActivity.this, "正在保存信息…", Toast.LENGTH_SHORT).show();
        saveDataToDB();
    }
});

拖动进度条的事件监听需要实现SeekBar.OnSeekBarChangeListener接口,调用SeekBar的setOnSeekBarChangeListener把该事件监听对象传递进去进行事件监听。接口内有三个重要的方法:

  1. onProgressChanged,进度条发生改变时使用;
  2. onStartTrackingTouch,进度条开始被拖动时使用;
  3. onStopTrackingTouch,进度条停止被拖动时使用。

这里使用了第三个方法,当停止拖动进度条时,调用playerControl接口的seekTo方法。

private boolean isUserTouchProgressBar = false;   //判断手是否触摸进度条的状态

mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
   
        //进度条发生改变
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
   
        //手已经触摸上去了拖动
        isUserTouchProgressBar=true;
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
   

        int touchProgress=seekBar.getProgress();
        //停止拖动
        if ( playerControl != null) {
   
            playerControl.seekTo(touchProgress);
        }
        isUserTouchProgressBar=false;
    }
});

saveDataToDB

前面挖了很多坑,很多程序都先搁置了,具体的有两个接口实现、获取歌曲信息、保存用户信息等等。

saveDataToDB是在点击退出按钮时用来实现保存用户信息到数据库的方法。

private void saveDataToDB(){
   
    new Thread() {
   
        public void run () {
   
            try {
   
                JSONObject result = RequestServlet.savePlayerInformation(account, musicId, playPattern);
                Message msg = new Message();
                msg.what = 1;
                msg.obj = result;
                handler1.sendMessage(msg);
            } catch (Exception e) {
   
                e.printStackTrace();
            }
        }
    }.start();
}

该方法调用了RequestServlet类中的savePlayerInformation方法,要保存的内容有歌曲id播放模式

Handler handler1 = new Handler(){
   
    public void handleMessage(android.os.Message msg) {
   
        try {
   
            if (msg.what == 1) {
   
                JSONObject result = (JSONObject) msg.obj;
                MainActivity.this.finish();
                Toast.makeText(MainActivity.this, "已退出", Toast.LENGTH_SHORT).show();
            }
        }catch (Exception e) {
   
            e.printStackTrace();
        }
    }
};

RequestServlet.savePlayerInformation()

savePlayerInformation方法与类中其它方法都比较相似(既然有那么多重复的部分,就可以把重复的部分拎出来单独写一个方法)

private static final String SAVE_USER_INFO ="http://192.168.43.xxx:8080/musicplayer/SaveMusic";

public static JSONObject savePlayerInformation(String account,int musicId,int playPattern){
   
    JSONObject result = null;

    String path = SAVE_USER_INFO+"?account="+account+"&musicId="+musicId+"&pattern="+playPattern;

    HttpURLConnection conn;

    try {
   
        conn = getConn(path);
        int code = conn.getResponseCode();    //http相应状态吗,200代表相应成功
        if (code == 200){
   
            InputStream stream = conn.getInputStream();
            String str = streamToString(stream);
            result = getJSON(str);
            conn.disconnect();
        }
    }catch (Exception e){
   
        e.printStackTrace();
    }

    return result;
}

RequestServlet.getMusicList()

获取音乐列表不需要向服务端传递参数信息,直接调用对应的servlet即可。

private static final String GET_MUSIC_LIST = "http://192.168.43.xxx:8080/musicplayer/GetMusicList";

//获取歌曲列表
public static JSONArray getMusicList(){
   
    JSONArray result = null;

    String path = GET_MUSIC_LIST;
    HttpURLConnection conn;

    try {
   
        conn = getConn(path);
        int code = conn.getResponseCode();
        if (code == 200){
   
            InputStream jsonArray = conn.getInputStream();
            String str = streamToString(jsonArray);
            result = getJsonArray(str);
            conn.disconnect();
        }else {
   
            return null;
        }
    }catch (Exception e){
   
        e.printStackTrace();
    }

    return result;
}

3.5 PlayerControl接口实现

PlayerControl接口功能交给PlayerPresenter实现

PlayerControl接口处理逻辑层,涉及到了播放器的音乐控制等内容。

Android有很多处理多媒体的API,MediaPlayer就是很基础一种,这里借助了MediaPlayer工具实现音乐播放功能。

private MediaPlayer mMediaPlayer=null;
  1. 定义全局变量和常量
private MediaPlayer mMediaPlayer = null;

private static final String ADDRESS = "http://192.168.43.xxx:8080/musicplayer/music/";
private PlayerViewControl mViewController = null;    //表现层
private MainActivity mMainActivity = null; 

//播放状态
public final int PLAY_STATE_PLAY=1;   //在播
public final int PLAY_STATE_PAUSE=2;  //暂停
public final int PLAY_STATE_STOP=3;   //未播

public int mCurrentState = PLAY_STATE_STOP;   //默认状态是停止播放

private Timer mTimer;
private SeekTimeTask mTimeTask;

//有参构造,接收MainActivity
public PlayerPresenter(MainActivity activity){
   
    mMainActivity = activity;
}
  1. 播放/暂停
@Override
public void playOrPause(MainActivity.IsPlay playState) {
   
    if(mViewController == null){
   
        this.mViewController = mMainActivity.mPlayerViewControl;
    }

    if (mCurrentState == PLAY_STATE_STOP || playState == MainActivity.IsPlay.play) {
   
        try {
   
            mMediaPlayer = new MediaPlayer();
            //指定播放路径
            mMediaPlayer.setDataSource(ADDRESS + mMainActivity.playAddress);
            //准备播放
            mMediaPlayer.prepareAsync();
            //播放
            mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
   
                @Override
                public void onPrepared(MediaPlayer mp) {
   
                    mMediaPlayer.start();
                }
            });
            mCurrentState = PLAY_STATE_PLAY;
            startTimer();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
    } else if (mCurrentState == PLAY_STATE_PLAY) {
   
        //如果当前的状态为播放,那么就暂停
        if (mMediaPlayer != null) {
   
            mMediaPlayer.pause();
            mCurrentState = PLAY_STATE_PAUSE;
            stopTimer();
        }
    } else if (mCurrentState == PLAY_STATE_PAUSE) {
   
        //如果当前的状态为暂停,那么继续播放
        if (mMediaPlayer != null) {
   
            mMediaPlayer.start();
            mCurrentState = PLAY_STATE_PLAY;
            startTimer();
        }
    }

    mViewController.onPlayerStateChange(mCurrentState);
}

播放或者暂停会涉及到界面的变化,所以这里就需要绑定表现层而表现层接口的实现就写在了MainActivity内,这里直接调用。

if(mViewController == null){
   
    this.mViewController = mMainActivity.mPlayerViewControl;
}

如果播放状态为停止或者传进来的参数为播放时,才会进行播放。

  • 实例化MediaPlayer,加载歌曲资源,准备播放
  • 调用**start()**进行播放
  • 修改播放状态为播放中
  • 开始计时
if (mCurrentState == PLAY_STATE_STOP || playState == MainActivity.IsPlay.play) {
   
    try {
   
        mMediaPlayer = new MediaPlayer();
        //指定播放路径
        mMediaPlayer.setDataSource(ADDRESS + mMainActivity.playAddress);
        //准备播放
        mMediaPlayer.prepareAsync();
        //播放
        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
   
            @Override
            public void onPrepared(MediaPlayer mp) {
   
                mMediaPlayer.start();
            }
        });
        mCurrentState = PLAY_STATE_PLAY;
        startTimer();
    } catch (IOException e) {
   
        e.printStackTrace();
    }
}

如果当前播放状态为播放中,调用该方法是为了暂停播放,调用pause()暂停播放,修改播放状态为播放暂停,停止计时

else if (mCurrentState == PLAY_STATE_PLAY) {
   
    //如果当前的状态为播放,那么就暂停
    if (mMediaPlayer != null) {
   
        mMediaPlayer.pause();
        mCurrentState = PLAY_STATE_PAUSE;
        stopTimer();
    }
}

如果当前播放状态为播放暂停,调用该方法是为了继续播放

else if (mCurrentState == PLAY_STATE_PAUSE) {
   
    //如果当前的状态为暂停,那么继续播放
    if (mMediaPlayer != null) {
   
        mMediaPlayer.start();
        mCurrentState = PLAY_STATE_PLAY;
        startTimer();
    }
}

当然,调用一次该方法,界面就需要根据播放状态做一次变化(参见3.6)

mViewController.onPlayerStateChange(mCurrentState);
  1. 计时

上面再播放/暂停切换时,使用到了计时功能,这个功能主要是为了根据播放时间不断更新进度条

private void startTimer() {
   
    if (mTimer == null) {
   
        mTimer=new Timer();
    }
    if (mTimeTask == null) {
   
        mTimeTask = new SeekTimeTask();
    }
    mTimer.schedule(mTimeTask,0,500);
}
private void stopTimer() {
   
    if (mTimeTask != null) {
   
        mTimeTask.cancel();
        mTimeTask=null;
    }
    if (mTimer != null) {
   
        mTimer.cancel();
        mTimer=null;
    }
}

Timer是一个普通的类,而TimerTask则是一个抽象类,TimerTask有一个抽象方法run(),我们可以每隔一段时间调用run方法去实现一些界面的改变。

Timer类中的schedule方法有三个参数,第一个参数就是TimerTask对象,第二个参数表示多长时间后执行,第三个参数表示间隔时间,单位是毫秒(ms),我这里设置了500毫秒(略长)。这样计时启动后,每隔500毫秒调用一次run方法。

而run方法,根据当前播放的时长和歌曲总时长计算一个百分比,再交到表现层去更新进度条。(参见3.6)

private class SeekTimeTask extends TimerTask {
   

    @Override
    public void run() {
   
        //获取当前的播放进度
        if (mMediaPlayer != null && mViewController!=null) {
   
            int currentPosition = mMediaPlayer.getCurrentPosition();
            //记录百分比
            int curPosition=(int)(currentPosition*1.0f/mMediaPlayer.getDuration()*100);
            if(curPosition<=100) {
   
                mViewController.onSeekChange(curPosition);
            }
        }
    }
}
  1. 播放上一首

切换歌曲首先需要判断一下用户当前使用的播放模式,根据播放模式的不同(顺序、随机、单曲)进行歌曲切换。

@Override
public void playLast() {
   
    // 顺序播放
    if (mMainActivity.playPattern == mMainActivity.PLAY_IN_ORDER) {
   
        if (mMainActivity.musicId == 0) {
   
            mMainActivity.musicId = mMainActivity.songNum-1;
            mMainActivity.setMusicView(MainActivity.IsPlay.play);
        } else {
   
            mMainActivity.musicId = mMainActivity.musicId - 1;
            mMainActivity.setMusicView(MainActivity.IsPlay.play);
        }
    }

    //随机播放
    else if (mMainActivity.playPattern == mMainActivity.PLAY_RANDOM) {
   
        mMainActivity.musicId = ( mMainActivity.musicId+(int)(1+Math.random()*(20-1+1))) % mMainActivity.songNum ;
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    }
    //单曲循环
    else if(mMainActivity.musicId==mMainActivity.PLAY_SINGLE){
   
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    }
}

如果是顺序播放,还需要对当前所播歌曲的id进行判断,如果当前id为0,那么上一首id应该是总歌曲量-1(id从0开始计算,最后一首歌歌曲id为歌曲总数-1),如果当前歌曲id不为0,直接减1就是变换之后的歌曲id。然后再调用setMusicView方法,传递参数为play(播放)

// 顺序播放
if (mMainActivity.playPattern == mMainActivity.PLAY_IN_ORDER) {
   
    if (mMainActivity.musicId == 0) {
   
        mMainActivity.musicId = mMainActivity.songNum-1;
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    } else {
   
        mMainActivity.musicId = mMainActivity.musicId - 1;
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    }
}

如果是随机播放,那么使用当前歌曲id+一个合理的随机整数作为切换后的歌曲id

//随机播放
else if (mMainActivity.playPattern == mMainActivity.PLAY_RANDOM) {
   
    mMainActivity.musicId = ( mMainActivity.musicId+(int)(1+Math.random()*(20-1+1))) % mMainActivity.songNum ;
    mMainActivity.setMusicView(MainActivity.IsPlay.play);
}

如果是单曲循环,那么将会重新播放该歌曲

//单曲循环
else if(mMainActivity.musicId==mMainActivity.PLAY_SINGLE){
   
    mMainActivity.setMusicView(MainActivity.IsPlay.play);
}
  1. 播放下一首

与播放上一首逻辑相似,这里不再赘述

@Override
public void playNext() {
   
    // 顺序播放
    if (mMainActivity.playPattern == mMainActivity.PLAY_IN_ORDER) {
   

        mMainActivity.musicId = (mMainActivity.musicId + 1) % mMainActivity.songNum;
        mMainActivity.setMusicView(MainActivity.IsPlay.play);

    }
    //随机播放
    else if (mMainActivity.playPattern == mMainActivity.PLAY_RANDOM) {
   
        mMainActivity.musicId = (mMainActivity.musicId+(int)(1+Math.random()*(20-1+1))) % mMainActivity.songNum ;
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    }
    //单曲循环
    else if(mMainActivity.playPattern == mMainActivity.PLAY_SINGLE){
   
        mMainActivity.setMusicView(MainActivity.IsPlay.play);
    }
}
  1. 停止播放

setMusicView方法中调用了该方法,在切换歌曲之前,需要释放掉MediaPlayer,否则会造成程序崩溃或者会出现同时播放几首歌的情况

@Override
public void stopPlay() {
   
    if (mMediaPlayer != null ) {
   
        mMediaPlayer.stop();
        mCurrentState= PLAY_STATE_STOP;
        stopTimer();
        //更新播放状态
        if (mViewController != null) {
   
            mViewController.onPlayerStateChange(mCurrentState);
        }
        mMediaPlayer.release();//释放资源
        mMediaPlayer=null;
    }
}
  1. seekTo

这里是在拖动进度条之后进行调用。停止拖动进度条后,会传进一个代表了百分比的参数,再使用MediaPlayer类中的seekTo方法直接跳转到计算出的音乐时长

@Override
public void seekTo(int seek) {
   
    //0~100之间
    //需要做一个转换,得到的seek其实是一个百分比
    if (mMediaPlayer != null) {
   
        //getDuration()获取音频时长
        int tarSeek=(int)(seek*1f/100*mMediaPlayer.getDuration());
        mMediaPlayer.seekTo(tarSeek);
    }
}

3.6 PlayerViewControl接口实现

考虑到需要使用到mainactivity内绑定的控件,因此直接放到了mainactivity内去实现PlayerViewControl接口

public PlayerViewControl mPlayerViewControl = new PlayerViewControl() {
   
    @Override
    public void onPlayerStateChange(int state) {
   
        //根据播放状态来修改UI
        switch (state) {
   
            case PLAY_STATE_PLAY:
                //播放中的话,我们要修改按钮显示为暂停
                mPlayOrPause.setBackgroundResource(R.drawable.bofangb);
                break;
            case PLAY_STATE_PAUSE:
            case PLAY_STATE_STOP:
                mPlayOrPause.setBackgroundResource(R.drawable.bofang);
                break;
        }
    }

    @Override
    public void onSeekChange(final int seek) {
   
        //改变播放进度,有一个条件:当用户的手触摸到进度条的时候,就不更新。
        runOnUiThread(new Runnable() {
   
            @Override
            public void run() {
   
                if (!isUserTouchProgressBar) {
   
                    mSeekBar.setProgress(seek);
                    if(seek==100) {
   
                        mPlayerControl.playNext();
                    }
                }
            }
        });
    }
};
  1. 根据播放状态进行图标的替换,这个很容易理解
@Override
public void onPlayerStateChange(int state) {
   
    //根据播放状态来修改UI
    switch (state) {
   
        case PLAY_STATE_PLAY:
            //播放中的话,我们要修改按钮显示为暂停
            mPlayOrPause.setBackgroundResource(R.drawable.bofangb);
            break;
        case PLAY_STATE_PAUSE:
        case PLAY_STATE_STOP:
            mPlayOrPause.setBackgroundResource(R.drawable.bofang);
            break;
    }
}
  1. 进度条更新

在计时功能内进行了调用这个方法,现在定义的是每隔500ms更新一次进度条。这里使用了一个子线程。另外,这里有个限制,当用户在按压进度条时,便不再自动更新进度条。

@Override
public void onSeekChange(final int seek) {
   
    //改变播放进度,有一个条件:当用户的手触摸到进度条的时候,就不更新。
    runOnUiThread(new Runnable() {
   
        @Override
        public void run() {
   
            if (!isUserTouchProgressBar) {
   
                mSeekBar.setProgress(seek);
                if(seek==100) {
   
                    mPlayerControl.playNext();
                }
            }
        }
    });
}

4. 测试

现在基本功能都已实现,后续会继续完善功能。

测试环境:Android 10,局域网

准备工作:打开tomcat,打开USB调试

4.1 添加数据库数据

由于数据库内没有歌曲数据,现在添加几首歌曲

INSERT INTO `music`(name, author, address, img, create_time)
VALUES ('光年之外', '邓紫棋', 'guangnian.mp3', 'guangnian.jpg', now()),
('再见', '邓紫棋', 'zaijian.mp3', 'zaijian.jpg', now()),
('一曲相思', '半阳', 'yiqu.mp3', 'yiqu.jpg', now()),
('小半', '陈粒', 'xiaoban.mp3', 'xiaoban.jpg', now()),
('稻香', '周杰伦', 'daoxiang.mp3', 'daoxiang.jpg', now()),
('你要的全拿走', '胡彦斌', 'niyao.mp3', 'niyao.jpg', now()),
('盗将行', '花粥,马雨阳', 'dao.mp3', 'dao.jpg', now()),
('Strongest', 'Alan worker', 'Strongest.mp3', 'Strongest.jpg', now());

4.2 添加资源文件

在服务端webapp目录下新建两个文件夹,一个文件夹内放图片文件,另一个放歌曲文件,这样Android端就可以获取到这些资源了。(注意名称要与数据库内的信息一致)

image-20201112232402438

4.3 登录

现在使用“cun”这个账户进行登录。查看数据库可以看到歌曲id为1,播放模式为0(顺序播放)

image-20201112232835222

为了参考方便,歌曲信息如下图

image-20201112233024389

输入账号和密码,点击登录按钮。由下图可以看出登录成功!初始化的歌曲信息和播放模式都正确。

image-20201112233326864

4.4 播放/暂停

播放功能正常

image-20201112233647081

暂停功能正常

image-20201112233703820

测试拖拽进度条也正常

4.5 上一首/下一首

上一首/下一首功能正常

image-20201112234021815

歌曲播放完成后,歌曲自动切换到下一首播放正常。

播放状态为随机播放,上一首/下一首功能正常(不过目前有较大概率重复一首播放,后续改进)

image-20201112234035180

4.6 退出

点击退出按钮,用户信息保存到服务端正常

image-20201112234259624

5. 后续

考虑到播放列表功能的实现,现针对播放功能的实现做出部分改动。

因为不同Activity之间互相调用内部的方法比较复杂,现在将可以复用的部分程序拿出来构建一个工具类(作为桥梁的功能)

新建一个工具类,命名为MusicPlayUtil

构建单例模式,保证其它类拿到的对象只有一个。

//这里私有化了无参构造,其它类不可以new该对象
private  MusicPlayUtil(){
   
}

public static MusicPlayUtil musicPlayUtil = new MusicPlayUtil();

public static MusicPlayUtil getInstance(){
   
    return musicPlayUtil;
}

绑定MainActivityPlayerControl,因为需要使用到它们其中的变量和方法。

private MainActivity mainActivity = null;
private PlayerControl mPlayerControl = null;

//绑定MainActivity
public void setMainActivity(MainActivity activity){
   
    this.mainActivity = activity;
    mPlayerControl = mainActivity.mPlayerControl;   //MainActivity已经调用了PlayerControl,这里直接使用
}

MainActivity中显示音乐界面的方法提取到了工具类里。

这一部分内容在前文里已经做了说明,这里再简单介绍一下。歌曲播放界面初始化时,除了初始化功能按钮等内容,还需要初始化有关歌曲的元素,所谓’有关歌曲’,是因为不同的歌曲所展示的内容是不同的,包括歌曲封面、歌曲名称和演唱者等信息。因此在播放器初始化和后面的切换歌曲时都需要重新初始化有关歌曲的部分界面。我们还可以看到,方法传递了一个参数playState,这个参数相当于在询问播放器“是否需要播放?”,因为我们不希望用户刚打开界面就已经在播放歌曲了,没有哪个播放器是这么做的,而在切换歌曲的时候需要自动播放,这里做了个区分。而这个参数是静态的(static),所以可以直接调用。

//设置有关歌曲的界面
public void setMusicView(MainActivity.IsPlay playState){
   
    try {
   
        JSONObject musicInfo = (JSONObject) mainActivity.sMusicList.get(mainActivity.musicId);
        String name = musicInfo.optString("name");
        String author = musicInfo.optString("author");
        String img = musicInfo.optString("img");
        mainActivity.playAddress=musicInfo.optString("address");
        mainActivity.mMusicPic.setImageUrl(IMG+img, R.mipmap.ic_launcher,R.mipmap.ic_launcher);
        mainActivity.mMusicName.setText(name);
        mainActivity.mMusicArtist.setText(author);
    } catch (Exception e) {
   
        e.printStackTrace();
    }
    if(playState == MainActivity.IsPlay.play){
   
        if ( mPlayerControl != null) {
   
            mPlayerControl.stopPlay();
        }
        mPlayerControl.playOrPause(playState);
    }
}

然后就是在播放界面设置和获取部分变量

//获取歌曲列表
public JSONArray getMusicList(){
   
    return mainActivity.sMusicList;
}

//获取歌曲id
public int getMusicId(){
   
    return mainActivity.musicId;
}

//获取歌曲总数
public int getMusicNum(){
   
    return mainActivity.songNum;
}

//设置歌曲id
public void setMusicId(int id){
   
    mainActivity.musicId = id;
}

工具类的内容就这些,那么怎么用呢?

  1. 获取工具类对象
private MusicPlayUtil musicPlayUtil = MusicPlayUtil.getInstance();   //获取工具类实例化的对象
  1. MainActivity中,初始化时需要将自身这个对象作为参数传递给工具类,保证工具类也可以修改部分UI
musicPlayUtil.setMainActivity(this);   
  1. 另外在初始化或者切换歌曲时调用工具类的setMusicView方法(将自己原有的这个方法删掉)
musicPlayUtil.setMusicView(IsPlay.notPlay);

6. 改进

6.1 退出

原来的程序里,在播放状态下,返回主界面,然后再打开软件需要重新登录。

现在对用户的返回进行监听,并将home界面当做一个Activity,退出后再次打开软件,不会重启软件

//MainActivity

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
   
    if (keyCode == KeyEvent.KEYCODE_BACK) {
   
        Intent home = new Intent(Intent.ACTION_MAIN);
        home.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        home.addCategory(Intent.CATEGORY_HOME);
        startActivity(home);
        return true;
    }
    return super.onKeyDown(keyCode, event);
}
「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论