Android音乐播放器开发(5)

您所在的位置:网站首页 播放页设计 Android音乐播放器开发(5)

Android音乐播放器开发(5)

#Android音乐播放器开发(5)| 来源: 网络整理| 查看: 265

1. 说明

本音乐播放器基于Android开发,原为我和另外两个小伙伴在上学期间一起做的一个小项目,近来有时间整理一下。之前我有文章已经介绍了播放界面的功能实现(Android音乐播放器开发),但介绍的比较粗糙,接下来会做更细致化的整理。内容已同步到Gitee仓库,GitHub仓库。

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

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

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

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

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

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

播放界面:Android音乐播放器开发--播放界面

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

2. 界面设计

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

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

image-20201009234316706

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

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

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

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

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把该事件监听对象传递进去进行事件监听。接口内有三个重要的方法:

onProgressChanged,进度条发生改变时使用; onStartTrackingTouch,进度条开始被拖动时使用; 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; 定义全局变量和常量 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; }

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

实例化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); 计时

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

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


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3