本文介绍音乐播放器的音乐播放功能实现。
音乐播放功能最初的形态是根据B站大佬的视频学着做的,现在应该还能搜索到他的视频。当然他只做了一个雏形,印象中他只做了一个播放按钮和进度条。然后我在他的基础上逐渐完善各个功能。
感谢这位老哥
一、布局播放界面的背景自行挑选,(个人感觉我选的背景还是蛮好看的)。图标都来自 阿里巴巴矢量图标库,内容丰富。
由上至下的控件分别为:
Button控件,用于关闭服务和关闭界面。(这个控件是最后加上的,虽然摆在这里不好看,但是当时没有别的好办法,就暂时放在这里了。) 相对布局,在布局内添加SmartImageView控件,用于加载服务端的歌曲图片(非常好用); 两个TextView控件,第1个用于显示在播歌曲名称,第2个用于显示歌手姓名; 线性布局,在布局内添加SeekBar控件,用于显示歌曲播放进度、用户拖动可以改变进度; 线性布局,在布局内添加5个Button控件,从左到右依次为播放模式、上一首、播放/暂停、下一首、歌曲播放列表。播放模式包括顺序播放、随机播放和单曲循环。 二、功能实现首先设置两个接口,分别命名为PlayerControl.java和PlayerViewControl.java,用于分离逻辑层和表现层。前者包含的方法有播放上一首play_last()、播放/暂停playOrPause()、播放下一首play_next()、停止播放stopPlay()、设置播放进度seekTo(),并由类PlayerPresenter继承并改写,用于处理程序的内在逻辑;后者包含播放状态的通知onPlayerStateChange()和播放进度的改变onSeekChange(),用于更新UI。
2.1 获取用户信息在音乐播放器初始化之前,首先应该获取用户信息数据,根据用户上次关闭播放器之前保留的记录重新展示。在用户登录时,登录程序(暂时还没写,后续更新)将用户信息使用Intent方法进行传递。在本程序中,获取登录账户对应的全部用户信息,并从中解析出用户的账号account、所听歌曲music_id和播放模式pattern。
每个歌曲都有对应的歌曲id,播放时也使用此id。播放模式是int类型,0代表循环播放,1代表随机播放,2代表单曲循环。
Intent intent=getIntent();//获取到Intent对象,包含了用户的整个信息
String user = intent.getStringExtra("result");
JSONObject user_info = (JSONObject) RequestServlet.getJSON(user);
music_id=user_info.optInt("music_id"); //歌曲
account=user_info.optString("account"); //账户
pattern=user_info.optInt("pattern"); //播放模式
上述程序使用的getJSON函数用于将String转换为json,方便提取数据。用于经常用到此方法,因此将其写为了函数。
public static JSONObject getJSON(String json){
try {
JSONobj = new JSONObject(json);
}catch (Exception e){
e.printStackTrace();
}
return JSONobj;
2.2 初始化播放界面
使用findViewById方法绑定布局中的各个控件。
mSeekBar = (SeekBar) this.findViewById(R.id.seek_bar);//进度条
mPlayOrPause = (Button) this.findViewById(R.id.play_or_pause_btn);//播放
mPlay_way = (Button) this.findViewById(R.id.play_way_btn);//播放模式
mPlay_last= (Button) this.findViewById(R.id.play_last_btn);//上一首
mPlay_next = (Button) this.findViewById(R.id.play_next_btn);//下一首
mPlay_menu = (Button) this.findViewById(R.id.play_menu_btn);//歌曲列表
mQuit=(Button) this.findViewById(R.id.quit_btn);//退出
text_view_name = (TextView) this.findViewById(R.id.text_view_name);//歌名
text_view_artist = (TextView) this.findViewById(R.id.text_view_artist);//歌手
siv = (SmartImageView) this.findViewById(R.id.siv_icon);//图片
根据播放模式pattern更改播放界面的播放模式UI
if (pattern==0) {
mPlay_way.setBackgroundResource(R.drawable.xunhuanbofang);
}else if(pattern==1){
mPlay_way.setBackgroundResource(R.drawable.suijibofang);
} else if (pattern==2) {
mPlay_way.setBackgroundResource(R.drawable.danquxunhuan);
}
开启子线程,在服务端获取整个音乐播放列表,并获取歌曲总数song_num,使用Message方法将获取到的音乐列表传递到主线程。
new Thread(){
public void run(){
try {
JSONArray result = RequestServlet.getMusicList();
song_num=result.length();
Message msg = new Message();
msg.what=1;
msg.obj = result;
handler1.sendMessage(msg);
}catch (Exception e){
e.printStackTrace();
}
}
}.start();
获取歌单使用的getMusicList函数如下(实际上这属于客户端与服务端的交互部分的程序,若有不解,待后续给出解释)
//获取歌单
public static JSONArray getMusicList(){
String path = SELECT_SERVLET+"?table=music&all=Y";
HttpURLConnection conn;
JSONArray result;
try {
conn = getConn(path);
int code = conn.getResponseCode();
if (code == 200){
InputStream jsonArray = conn.getInputStream();
String sjsonArray = streamToString(jsonArray);
result = getJsonArray(sjsonArray);
conn.disconnect();
return result;
}else {
return null;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
在主线程中使用Handler方法获取子线程中的音乐列表
private Handler handler1 = new Handler() {
public void handleMessage(android.os.Message msg) {
try {
if (msg.what == 1) {
musicList = (JSONArray) msg.obj;
add_view(0);
}
}catch (Exception e) {
e.printStackTrace();
}
}
其中add_view函数用于初始化界面信息,由于很多地方应用到了此功能,故将其写为函数,方便调用。
add_view()方法,首先根据music_id在音乐列表中获取用户所听歌曲的全部信息,并从中解析出歌曲的名称、演唱者、歌曲封面存储路径和歌曲所在路径,使用set方法将以上信息全部加载到播放页面,至此,界面初始化完成。然后在该方法中添加一个判断语句,在调用该方法是需要传入一个int型参数,如果该参数等于特定值时(如:1),则执行音乐播放。而初始化时并不需要播放。
public static void add_view(int a) {
try {
JSONObject A_music = (JSONObject) musicList.get(music_id); //获取歌曲信息
String name = A_music.optString("name");
String author = A_music.optString("author");
String img = A_music.optString("img");
address=A_music.optString("address");
siv.setImageUrl(MusiclistActivity.IMG+img,R.mipmap.ic_launcher,R.mipmap.ic_launcher); //此处在博客中缩进有问题,自行更正
text_view_name.setText(name);
text_view_artist.setText(author);
//如果a=1,则播放
if (a == 1) {
if ( mplayerControl != null) {
mplayerControl.stopPlay();
}
mplayerControl.playOrPause();
}
} catch (Exception e) {
e.printStackTrace();
}
}
2.3 播放/暂停
系统默认播放状态为STOP
对播放按钮设置监听,按钮被点击后,调用逻辑层接口中的playOrPause()方法,创建一个MediaPlayer播放器,指定播放文件为音频。(MediaPlayer类在Android系统中是用于播放音频和视频的,它支持多种格式音频文件,并提供了非常全面的控制方法,从而使播放音频的工作变得十分简单。)
private void initPlayer() {
if (mMediaPlayer == null) {
mMediaPlayer=new MediaPlayer();
//指定参数为音频文件
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
}
MediaPlayer播放器按照初始化中解析出的歌曲所在路径获取歌曲文件,使用start方法开始播放音乐,同时修改播放状态为PLAY,开始计时。
if (mCurrentState == PLAY_STATE_STOP) {
//创建播放器
initPlayer();
try {
//指定播放路径
mMediaPlayer.setDataSource(MainActivity.ADDRESS + MainActivity.address);
//准备播放
mMediaPlayer.prepareAsync();
//播放
mMediaPlayer.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
mCurrentState = PLAY_STATE_PLAY;
startTimer();
播放状态更新后,通知表现层接口的onPlayerStateChange()方法,进行UI更新。
private 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;
}
}
再次点击播放按钮,音乐暂停播放,更改状态为PAUSE,停止计时,并更新UI。
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();
}
}
2.4 进度条更新
歌曲播放时,系统创建一个Timer()方法,设置该方法每隔500ms访问一次run()方法。run()方法获取歌曲播放进度,将其转化为百分比,当歌曲进度小于100%时,通知表现层进行进度条更新。
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);
}
}
}
表现层获取歌曲播放百分比,使用setProgress进行进度条更新,当歌曲播放进度为100%时,执行播放下一首操作。
public void onSeekChange(final int seek) {
//改变播放进度,有一个条件:当用户的手触摸到进度条的时候,就不更新。
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isUserTouchProgressBar) {
mSeekBar.setProgress(seek);
if(seek==100) {
mplayerControl.play_next();
}
}
}
});
}
2.5 拖拽进度条改变播放进度
对进度条改变设置监听。
用户在歌曲播放时拖动进度条,停止拖动时,获取进度条当前值所占百分比,传入逻辑层的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);
}
}
2.6 更改播放模式
将播放模式pattern声明为int型公共变量,供所有类调用。
对播放模式按钮设置监听。
对播放模式进行求余操作,确保值不会“溢出”。
用户点击一次按钮,更改一次pattern值,并根据pattern值更改UI。
mPlay_way.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
if( mplayerControl != null) {
pattern=(pattern+1) % 3;
if (pattern==0) {
mPlay_way.setBackgroundResource(R.drawable.xunhuanbofang);
}else if(pattern==1){
mPlay_way.setBackgroundResource(R.drawable.suijibofang);
} else if (pattern==2) {
mPlay_way.setBackgroundResource(R.drawable.danquxunhuan);
}
}
}
}
2.7 播放上一首
对播放上一首按钮设置监听。
用户点击按钮后,调用逻辑层的play_last()方法。
根据播放模式的不同,在不同条件语句下对music_id进行减法操作。
调用add_view()方法,传入参数1,播放新歌曲。
public void play_last() {
// 顺序播放
if (MainActivity.pattern == 0) {
if (MainActivity.music_id == 0) {
MainActivity.music_id = MainActivity.song_num;
MainActivity.add_view(1);
} else {
MainActivity.music_id = MainActivity.music_id - 1;
MainActivity.add_view(1);
}
}
//随机播放
else if (MainActivity.pattern == 1) {
MainActivity.music_id=(MainActivity.music_id+(int)(1+Math.random()*(20-1+1))) % MainActivity.song_num ;
MainActivity.add_view(1);
}
//单曲循环
else if(MainActivity.pattern==2){
MainActivity.add_view(1);
}
}
2.8 播放下一首
与播放上一首功能类似,对music_id进行加法操作。
2.9 停止播放
用于切换歌曲,在切换歌曲前,应该停止播放当前歌曲。
更改播放状态。
停止计时。
释放MediaPlayer的资源。(因为播放新的歌曲会创建新的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;
}
}
2.10 打开音乐列表
对按钮设置监听。
按钮被点击,打开音乐列表。
mPlay_menu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ( mplayerControl != null) {
Intent intent = new Intent(MainActivity.this,MusiclistActivity.class);
startActivity(intent);
}
}
}
2.11 Service(服务)
服务可以长期运行在后台,没有用户界面,可以用来播放歌曲;
关闭按钮被点击时,服务才被销毁。
2.12 onKeyDown()方法
用于监听用户是否点击手机的返回键
该方法将home界面当做一个Activity,再次打开软件,不会重启软件
@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);
}
2.13 关闭按钮
设置监听。
按钮被点击时,调用stopPlay()方法,停止播放歌曲,释放掉MediaPlayer资源。
开启子线程,保存用户当前所听歌曲ID和播放模式到服务端对应账户中。
private void write() {
new Thread() {
public void run () {
try {
JSONObject result = RequestServlet.SavePlayerInformation(account, music_id, pattern, 0);
Message msg = new Message();
msg.what = 1;
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 == 1) {
JSONObject result = (JSONObject) msg.obj;
stop();
MainActivity.this.finish();
Toast.makeText(MainActivity.this, "已退出", Toast.LENGTH_SHORT).show();
}
}catch (Exception e) {
e.printStackTrace();
}
}
};
至此,音乐播放界面的功能已基本实现。
作者:sjcup