本音乐播放器基于Android开发,原为我和另外两个小伙伴在上学期间一起做的一个小项目,近来有时间整理一下。之前我有文章已经介绍了播放界面的功能实现(Android音乐播放器开发),但介绍的比较粗糙,接下来会做更细致化的整理。源码已同步到Gitee仓库,以后也会放到GitHub仓库,觉得还不错的话帮忙点个“star”吧,非常感谢。
当初代码写的很随意,目的只为实现功能。现在更倾向于代码可读性和简洁性,因此会在原来的程序基础上做一些小修改。也有可能不会一步到位,计划慢慢修改,以增强自己的理解。
服务端使用的是比较传统的servlet和jdbc传递数据,整理完之后,新版本会修改为SSM框架,更加简洁高效。安卓端使用的也都是基础的工具,比如音乐播放功能的实现也是借助于入门级的MediaPlayer类,目前关于安卓端没有什么更改的想法。
服务端:Android音乐播放器开发–服务端
登录:Android音乐播放器开发–登录
注册:Android音乐播放器开发–注册
修改密码:Android音乐播放器开发–修改密码
(适用于平时做个小课设的小伙伴们)
首先为播放器设计一个播放界面
播放界面设计到的功能包括:
其中功能按钮除去上述介绍的,后续调试中需要添加一个关闭服务的按钮,暂且也将其放在该界面
使用xml文件进行界面设计,命名为activity_main.xml
<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>
图标全部来自阿里巴巴图标矢量库
布局文件比较简单,这里就不做过多介绍了,大致界面和布局如下图所示:
首先梳理一下整体思路,由于这里的功能非常多,涉及到后台逻辑和界面的切换,以及进度条的更新,在这里设置了两个接口,用于分离逻辑层和表现层。逻辑层的方法主要有播放上一首playLast(),播放/暂停playOrPause(),播放下一首playNext(),停止播放stopPlay(),设置播放进度seekTo();表现层方法主要有播放状态的通知onPlayerStateChange()和播放进度的改变onSeekChange(),用于更新UI。
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实现。
有了两个接口,先不着急实现。现在可以写初始化播放界面的一些内容了。
在初始化播放界面之前,先要对用户信息进行初始化,因为界面的初始化依赖于用户信息的歌曲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");
}
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();
}
初始化播放界面包含三个方面:
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资源,再实例化一个对象来加载新的资源才可以。
涉及到几个按钮的点击事件
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把该事件监听对象传递进去进行事件监听。接口内有三个重要的方法:
这里使用了第三个方法,当停止拖动进度条时,调用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;
}
PlayerControl接口功能交给PlayerPresenter实现
PlayerControl接口处理逻辑层,涉及到了播放器的音乐控制等内容。
Android有很多处理多媒体的API,MediaPlayer就是很基础一种,这里借助了MediaPlayer工具实现音乐播放功能。
private MediaPlayer mMediaPlayer=null;
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;
}
@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;
}
如果播放状态为停止或者传进来的参数为播放时,才会进行播放。
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);
上面再播放/暂停切换时,使用到了计时功能,这个功能主要是为了根据播放时间不断更新进度条
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);
}
}
}
}
切换歌曲首先需要判断一下用户当前使用的播放模式,根据播放模式的不同(顺序、随机、单曲)进行歌曲切换。
@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);
}
与播放上一首逻辑相似,这里不再赘述
@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);
}
}
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;
}
}
这里是在拖动进度条之后进行调用。停止拖动进度条后,会传进一个代表了百分比的参数,再使用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);
}
}
考虑到需要使用到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();
}
}
}
});
}
};
@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;
}
}
在计时功能内进行了调用这个方法,现在定义的是每隔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();
}
}
}
});
}
现在基本功能都已实现,后续会继续完善功能。
测试环境:Android 10,局域网
准备工作:打开tomcat,打开USB调试
由于数据库内没有歌曲数据,现在添加几首歌曲
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());
在服务端webapp目录下新建两个文件夹,一个文件夹内放图片文件,另一个放歌曲文件,这样Android端就可以获取到这些资源了。(注意名称要与数据库内的信息一致)
现在使用“cun”这个账户进行登录。查看数据库可以看到歌曲id为1,播放模式为0(顺序播放)
为了参考方便,歌曲信息如下图
输入账号和密码,点击登录按钮。由下图可以看出登录成功!初始化的歌曲信息和播放模式都正确。
播放功能正常
暂停功能正常
测试拖拽进度条也正常
上一首/下一首功能正常
歌曲播放完成后,歌曲自动切换到下一首播放正常。
播放状态为随机播放,上一首/下一首功能正常(不过目前有较大概率重复一首播放,后续改进)
点击退出按钮,用户信息保存到服务端正常
考虑到播放列表功能的实现,现针对播放功能的实现做出部分改动。
因为不同Activity之间互相调用内部的方法比较复杂,现在将可以复用的部分程序拿出来构建一个工具类(作为桥梁的功能)
新建一个工具类,命名为MusicPlayUtil
构建单例模式,保证其它类拿到的对象只有一个。
//这里私有化了无参构造,其它类不可以new该对象
private MusicPlayUtil(){
}
public static MusicPlayUtil musicPlayUtil = new MusicPlayUtil();
public static MusicPlayUtil getInstance(){
return musicPlayUtil;
}
绑定MainActivity和PlayerControl,因为需要使用到它们其中的变量和方法。
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;
}
工具类的内容就这些,那么怎么用呢?
private MusicPlayUtil musicPlayUtil = MusicPlayUtil.getInstance(); //获取工具类实例化的对象
musicPlayUtil.setMainActivity(this);
setMusicView
方法(将自己原有的这个方法删掉)musicPlayUtil.setMusicView(IsPlay.notPlay);
原来的程序里,在播放状态下,返回主界面,然后再打开软件需要重新登录。
现在对用户的返回进行监听,并将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);
}