Android 实现简单音乐播放器(二)
在Android 实现简单音乐播放器(一)中,我介绍了MusicPlayer的页面设计。
现在,我简单总结一些功能实现过程中的要点和有趣的细节,结合MainActivity.java代码进行说明(写出来可能有点碎……一向不太会总结^·^)。
一、功能菜单
在MusicPlayer中,我添加了三个菜单:
search(搜索手机中的音乐文件,更新播放列表)、
clear(清除播放列表……这个功能是最初加进去的,后来改进之后,已经没什么实际意义)、
exit(退出)。
menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
<item android:id="@+id/action_search" android:title="search"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_clear" android:title="clear"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_exit" android:title="exit"
android:orderInCategory="100" app:showAsAction="never" />
</menu>
关于菜单功能,直接上代码,很简单,就不做说明啦。重要的在后面。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
} @Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId(); //noinspection SimplifiableIfStatement
if (id == R.id.action_search) {
progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true);
searchMusicFile();
return true;
}else if(id==R.id.action_clear){
list.clear();
listAdapter.notifyDataSetChanged();
return true;
}else if(id==R.id.action_exit){
flag=false;
mediaPlayer.stop();
mediaPlayer.release();
this.finish();
return true;
}
return super.onOptionsItemSelected(item);
}
二、搜索音乐文件——search的实现
先看一下相关的全局变量:
private ListView musicListView;
private SimpleAdapter listAdapter;
private List<HashMap<String,String>> list=new ArrayList<>();
为了播放音乐的便利,在播放器打开时,程序自动搜索音乐数据,将必要的信息保存在list中,并用ListView显示出来,以供用户进行选择。
而这个MusicPlayer用于播放手机外部存储设备(SD卡)的音乐,要搜索出SD卡中的全部音乐文件,主要有两种方法:1、直接遍历SD卡的File,判断文件名后缀,找到音乐文件。这种方法可以区别出一定格式的音乐文件,也可以找到对应的歌词文件,但是缺点是:遍历搜索,速度很慢。2、用Android提供的多媒体数据库MediaStore,直接用ContentResolver的query方法,就可以对MediaStore进行搜索啦,非常高效(果断选用这种方式~~),但是数据库里面没有歌词(泪目T_T~~~暂时放弃歌词播放的功能啦,以后要是想起来,再加上吧……)
private void searchMusicFile(){
// 如果list不是空的,就先清空
if(!list.isEmpty()){
list.clear();
}
ContentResolver contentResolver=getContentResolver();
//搜索SD卡里的music文件
Uri uri= MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection={
MediaStore.Audio.Media._ID, //根据_ID可以定位歌曲
MediaStore.Audio.Media.TITLE, //这个是歌曲名
MediaStore.Audio.Media.DISPLAY_NAME, //这个是文件名
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.IS_MUSIC,
MediaStore.Audio.Media.DATA
};
String where=MediaStore.Audio.Media.IS_MUSIC+">0";
Cursor cursor=contentResolver.query(uri,projection,where,null, MediaStore.Audio.Media.DATA);
while (cursor.moveToNext()){
//将歌曲的信息保存到list中
//其中,TITLE和ARTIST是用来显示到ListView中的
// _ID和DATA都可以用来播放音乐,其实保存任一个就可以
String songName=cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
String artistName=cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
String id=Integer.toString(cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media._ID)));
String data=Integer.toString(cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)));
HashMap<String,String> map=new HashMap<>();
map.put("name",songName);
map.put("artist",artistName);
map.put("id",id);
map.put("data",data);
list.add(map);
}
cursor.close();
//搜索完毕之后,发一个message给Handler,对ListView的显示内容进行更新
handler.sendEmptyMessage(SEARCH_MUSIC_SUCCESS);
}
搜索完了,要对ListView进行更新,这里的更新,在Handler中完成(也包括后面要讲到的播放时间的实时更新)。
这里用了两个常量来区别handler要处理的消息类别。
private static final int SEARCH_MUSIC_SUCCESS=0;
private static final int CURR_TIME_VALUE=1;
取名无能,其实感觉统一一下Message的命名可能对于理解的帮助会更好(比如MSG_MUSIC_SERCH,MSG_TIME_MODIFY),下次改正~
private Handler handler=new Handler(){
@Override
public void handleMessage(Message message){
switch (message.what){
//更新播放列表
case SEARCH_MUSIC_SUCCESS:
listAdapter=new SimpleAdapter(MainActivity.this,list,R.layout.musiclist,
new String[]{"name","artist"}, new int[]{R.id.songName,R.id.artistName});
MainActivity.this.setListAdapter(listAdapter);
Toast.makeText(MainActivity.this,"找到"+list.size()+"份音频文件",Toast.LENGTH_LONG).show();
progressDialog.dismiss();
break;
//更新当前歌曲的播放时间
case CURR_TIME_VALUE:
currtimeView.setText(message.obj.toString());
break;
default:
break;
}
}
};
仔细观察上面的handler以及搜索菜单中的动作,可以看到,在搜索音乐的过程中用到了一个进程对话框progressDialog,这是一个定义的全局变量,为了能随时启动和关闭。
private ProgressDialog progressDialog=null;
当搜索音乐的用时较长的时候,这个对话框就会显示一个一直在转的圆圈,并显示"正在搜索音乐"的字样,用来显示当前的进程。不过,不知道是由于我手机里面的音乐比较少(20多首),还是本身读取Android的MediaStore数据库的速度就很快,这个对话框存在的时间很短(几乎一闪而过,甚至闪都不闪)。虽然在这里,这个对话框实际意义并不大,还是把它的实现贴出来,备着以后用吧。
要显示这个对话框的时候,使用ProgressDialog的类方法show(),设置一些必要地参数,具体请参考Android的文档。
progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true);
要关闭这个对话框的时候,使用它的dismiss()方法.
progressDialog.dismiss();
三、选择歌曲
好了,现在我们已经有了播放列表,那么下一个步骤自然是选择要播放的歌曲咯。
首先,是下面代码中涉及的几个全局变量。
private int currState=IDLE;//当前播放器的状态
private int currPosition;//list的当前选中项的索引值(第一项对应0)
private String nameChecked;//当前选中的音乐名
private Uri uriChecked;//当前选中的音乐对应的Uri private AlwaysMarqueeTextView nameView;// 页面中用来显示当前选中音乐名的TextView
我们来看一下播放器的不同状态(currState可以取的几个值):
// 定义当前播放器的状态
private static final int IDLE=0; //空闲:没有播放音乐
private static final int PAUSE=1; //暂停:播放音乐时暂停
private static final int START=2; //正在播放音乐
选择歌曲,在IDLE状态下才有效。选中歌曲之后,要在具有跑马灯效果的TextView中显示歌名,并且更新播放总时长。
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
if(currState==IDLE) {
// 若在IDLE状态下,选中list中的item,则改变相应项目
HashMap<String, String> map = list.get(position);
nameChecked = map.get("name");
Long idChecked = Long.parseLong(map.get("id"));
//uriChecked:选中的歌曲相对应的Uri
uriChecked = Uri.parse(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + idChecked);
nameView.setText(nameChecked);
currPosition = position; //这个是歌曲在列表中的位置,“上一曲”“下一曲”功能将会用到
}
}
四、播放
有关播放的全局变量:
private MediaPlayer mediaPlayer;
private TextView currtimeView;
private TextView totaltimeView;
private SeekBar seekBar;
private AlwaysMarqueeTextView nameView;
private ImageButton playBtn;
这里的播放,指的是音乐播放器的播放按钮,它要实现的功能有两个:1、IDLE状态下,按下即开始播放;2、播放时,按下,暂停;再按下,继续播放(这两个状态分别对应两种按钮图片)。
ExecutorService executorService= Executors.newSingleThreadExecutor();
public void onPlayClick(View v){
switch (currState){
case IDLE:
start();
currState=START;
break;
case PAUSE:
mediaPlayer.start();
playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_pause));
currState=START;
break;
case START:
mediaPlayer.pause();
playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_play));
currState=PAUSE;
break;
}
}
private void start(){
if(uriChecked!=null){
mediaPlayer.reset();
try {
mediaPlayer.setDataSource(MainActivity.this,uriChecked);
mediaPlayer.prepare();
mediaPlayer.start();
initSeekBar();
nameView.setText(nameChecked);
playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_pause));
currState=START;
executorService.execute(new Runnable() {
@Override
public void run() {
flag=true;
while(flag){
if(mediaPlayer.getCurrentPosition()<seekBar.getMax()){
seekBar.setProgress(mediaPlayer.getCurrentPosition());
Message msg=handler.obtainMessage(CURR_TIME_VALUE,
toTime(mediaPlayer.getCurrentPosition()));
handler.sendMessage(msg);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
flag=false;
}
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}else{
Toast.makeText(this, "播放列表为空或尚未选中曲目", Toast.LENGTH_LONG).show();
}
}
在播放时,播放进度体现在当前播放时长和进度条的变化上。因此,按下播放键时,我们要对进度条进行初始化。
private void initSeekBar(){
int duration=mediaPlayer.getDuration();
seekBar.setMax(duration);
seekBar.setProgress(0);
if(duration>0){
totaltimeView.setText(toTime(duration));
}
}
播放过程中,实时更新播放时间和进度条的工作则用一个ExecutorService来完成。
把时长(毫秒数)转化为时间格式(00:00)的方法:
private String toTime(int duration){
Date date=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("mm:ss", Locale.getDefault());
sdf.setTimeZone(TimeZone.getTimeZone("GMT+0"));
date.setTime(duration);
return sdf.format(date);
}
补充说明,这里还有一个附加功能的实现,就是在播放音乐的过程中,用手去滑动进度条,改变进度时,音乐播放的进度也随之跳到相应地进度(相信这个功能也是音乐播放器必备的功能啦)。
具体实现,就是在OnCreate()中,给SeekBar增加一个OnSeekBarChangeListener(),代码如下:
seekBar=(SeekBar)findViewById(R.id.seekBar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(currState==START){
if(fromUser){ //如果是人为改变进度,则改变相应地显示时长
currtimeView.setText(toTime(progress));
}
}
} @Override
public void onStartTrackingTouch(SeekBar seekBar) {
//开始拖动进度条,将音乐播放器停止
mediaPlayer.pause();
} @Override
public void onStopTrackingTouch(SeekBar seekBar) {
//结束拖动进度条,按照新的进度继续播放音乐
if(currState==START){
mediaPlayer.seekTo(seekBar.getProgress());
mediaPlayer.start();
}
}
});
五、停止
1 private void stop() {
initState();
mediaPlayer.stop();
currState = IDLE;
}
停止功能很简单,注意在停止播放时,更新必要的信息(包括按钮、状态、进度条、时间等等),我就不赘述啦
在这里补充一下initState(),其实具体就是更新页面,使时间/进度条/按钮等等都恢复到歌曲尚未播放时的状态,具体代码如下:
private void initState(){
nameView.setText("");
currtimeView.setText("00:00");
totaltimeView.setText("00:00");
flag = false;
seekBar.setProgress(0);
playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_play));
}
六、上一曲/下一曲
这两个功能恰好对立,实现起来原理都是一样的。这里我就只贴出上一曲的程序咯。
按下上一曲的按钮,将在该按钮的动作响应函数里面进行动作响应。
public void onPreviousClick(View v){
previous();
}
具体previous()做了什么呢,主要是根据音乐列表的当前选中的索引值,使列表滑动到前一个列表项(currPosition-1)并进行点击(这里的点击是用ListView的performItemClick()方法来实现的,没有用到人的手指哟),并且根据当前的播放状态作出相对应的音乐控制,代码如下:
private void previous(){
if(musicListView.getCount()>0){
if(currPosition>0){
switch (currState){
case IDLE:
musicListView.smoothScrollToPosition(currPosition - 1);
musicListView.performItemClick(
musicListView.getAdapter().getView(currPosition-1,null,null),
currPosition-1,
musicListView.getItemIdAtPosition(currPosition-1));
break;
case START:
case PAUSE:
stop();
musicListView.smoothScrollToPosition(currPosition - 1);
musicListView.performItemClick(
musicListView.getAdapter().getView(currPosition - 1, null, null),
currPosition - 1,
musicListView.getItemIdAtPosition(currPosition-1));
break;
}
}else{
switch (currState) {
case IDLE:
musicListView.smoothScrollToPosition(musicListView.getCount() - 1);
musicListView.performItemClick(
musicListView.getAdapter().getView(musicListView.getCount()-1, null, null),
musicListView.getCount()-1,
musicListView.getItemIdAtPosition(musicListView.getCount()-1));
break;
case START:
case PAUSE:
stop();
musicListView.smoothScrollToPosition(musicListView.getCount() - 1);
musicListView.performItemClick(
musicListView.getAdapter().getView(musicListView.getCount()-1, null, null),
musicListView.getCount()-1,
musicListView.getItemIdAtPosition(musicListView.getCount()-1));
start();
break;
}
}
}
}
比较难的地方,就是如何在按下上一曲(或下一曲)的时候,实现出ListView的点击效果。
//使选中的歌曲滑动到页面显示范围内
musicListView.smoothScrollToPosition(currPosition - 1);
//单击ListView中的Item
musicListView.performItemClick( musicListView.getAdapter().getView(currPosition-1,null,null),currPosition-1,
musicListView.getItemIdAtPosition(currPosition-1));
七、退出时,释放MediaPlayer
@Override
protected void onDestroy() {
if(mediaPlayer!=null){
mediaPlayer.stop();
mediaPlayer.release();
}
super.onDestroy();
}
八、用户权限
由于要播放SD卡中的音乐,我们还要在AndroidManifest.xml中添加读外部存储的权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
好了,到现在,一个拥有基本功能的音乐播放器就完工啦。
(总算写完了~~~)
九、补充说明
由于前面是按照各个小功能的实现来讲的,比较碎,看上去很糊涂,也比较分化,当然,有点基础的朋友应该也能获取到自己想要的信息。
在这里我再补充,贴上程序的全局变量和OnCreate部分做的动作,前面看不懂的可以到这里找找。
private static final String TAG="yang";
private static final int SEARCH_MUSIC_SUCCESS=0;
private ProgressDialog progressDialog=null;
private ListView musicListView;
private SimpleAdapter listAdapter;
private List<HashMap<String,String>> list=new ArrayList<>(); private MediaPlayer mediaPlayer;
private TextView currtimeView;
private TextView totaltimeView;
private SeekBar seekBar;
private AlwaysMarqueeTextView nameView;
private ImageButton playBtn; private String nameChecked;
private Uri uriChecked; private int currPosition;//当前选中的list // 定义当前播放器的状态
private static final int IDLE=0; //空闲:没有播放音乐
private static final int PAUSE=1; //暂停:播放音乐时暂停
private static final int START=2; //正在播放音乐 private static final int CURR_TIME_VALUE=1; private int currState=IDLE;//当前播放器的状态
private boolean flag=false;//控制进度条的索引 ExecutorService executorService= Executors.newSingleThreadExecutor(); @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); musicListView=(ListView)findViewById(android.R.id.list);
currtimeView=(TextView)findViewById(R.id.currTime);
totaltimeView=(TextView)findViewById(R.id.totalTime);
seekBar=(SeekBar)findViewById(R.id.seekBar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(currState==START){
if(fromUser){
currtimeView.setText(toTime(progress));
}
}
} @Override
public void onStartTrackingTouch(SeekBar seekBar) {
mediaPlayer.pause();
} @Override
public void onStopTrackingTouch(SeekBar seekBar) {
if(currState==START){
mediaPlayer.seekTo(seekBar.getProgress());
mediaPlayer.start();
}
}
}); nameView=(AlwaysMarqueeTextView)findViewById(R.id.nameDisplay);
playBtn=(ImageButton)findViewById(R.id.play); mediaPlayer=new MediaPlayer();
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (musicListView.getCount() > 0) {
next();
} else {
Toast.makeText(MainActivity.this, "播放列表为空", Toast.LENGTH_LONG).show();
}
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
mediaPlayer.reset();
return false;
}
});
// 搜索MediaStore中的音频文件,填充文件列表
progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true);
searchMusicFile(); }
其实,onCreate主要是获取一些控件,然后就是给SeekBar和MusicPlayer添加必要的Listener。
前面没有提过的就是MusicPlayer的两个Listener,一个是OnCompletionListener,这个是监听音乐播放结束,我这里的实现也比较简单,当列表中的音乐超过1首,那就播放下一曲。另一个是OnErrorListener,这个是监听音乐播放出错,当出错的时候,我们就把MusicPlayer进行reset。关于MusicPlayer的使用,建议参考Android的文档。
Over!
Android 实现简单音乐播放器(二)的更多相关文章
- Android 实现简单音乐播放器(一)
今天掐指一算,学习Android长达近两个月了,今天开始,对过去一段时间的学习收获以及遇到的疑难杂症做一些总结. 简单音乐播放器是我自己完成的第一个功能较为完整的APP,可以说是我的Android学习 ...
- Android实现简单音乐播放器(MediaPlayer)
Android实现简单音乐播放器(MediaPlayer) 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 工程内容 实现一个简单的音乐播放器,要求功能 ...
- Android实现简单音乐播放器(startService和bindService后台运行程序)
Android实现简单音乐播放器(MediaPlayer) 开发工具:Andorid Studio 1.3运行环境:Android 4.4 KitKat 工程内容 实现一个简单的音乐播放器,要求功能有 ...
- Android——简单音乐播放器
使用MediaPlayer做的简单音乐播放器,更多内容请到百度经验查看 http://jingyan.baidu.com/article/60ccbceb63452364cab197f1.html ...
- html5 简单音乐播放器
html5 简单音乐播放器 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> < ...
- iOS之基于FreeStreamer的简单音乐播放器(模仿QQ音乐)
代码地址如下:http://www.demodashi.com/demo/11944.html 天道酬勤 前言 作为一名iOS开发者,每当使用APP的时候,总难免会情不自禁的去想想,这个怎么做的?该怎 ...
- Android开发6:Service的使用(简单音乐播放器的实现)
前言 啦啦啦~各位好久不见啦~博主最近比较忙,而且最近一次实验也是刚刚结束~ 好了不废话了,直接进入我们这次的内容~ 在这篇博文里我们将学习Service(服务)的相关知识,学会使用 Service ...
- Android开发学习之路--MediaPlayer之简单音乐播放器初体验
很多时候我们都会用手机来播放音乐,播放视频,那么具体地要怎么实现呢,其实主要是MediaPlayer类来完成的.下面通过简单的例子来实现一首歌曲的播放吧.新建工程MediaPlayerStudy,这里 ...
- Android开发实战之简单音乐播放器
最近开始学习音频相关.所以,很想自己做一个音乐播放器,于是,花了一天学习,将播放器的基本功能实现了出来.我觉得学习知识点还是蛮多的,所以写篇博客总结一下关于一个音乐播放器实现的逻辑.希望这篇博文对你的 ...
随机推荐
- ACM程序对拍
有时候在OJ刷题目的时候,总是会遇到不知名bug,题目总不能AC,自己测试的一些数据又都能得出正确的结果,又或是直接暴力会TLE,改了算法,但是仍然WA,这时候进行程序对拍测试数据不失为一个好办法.程 ...
- oracle修改表字段名时报错:ORA-00054:资源正忙,但指定以NOWAIT方式获取资源,或者超时失效的问题
打开sql plus select session_id from v$locked_object;查询出oracle锁定的会话ID SELECT sid, serial#, username, os ...
- Altium Designer 15 --- Nets Update
Now I want to introduce the use of 'Configure Physical Nets', as follows: If you has finished the PC ...
- linux中权限的修改
修改访问权限的linux名是:Linux访问权限的问题是这样子的:比如 d rwx rwx rwx ,d是文件所在的文件,后面有9位,分别代表不同者的权限.第一个rwx代表这文件的所有者的权限,r是r ...
- host,nslookup,dig 命令安装
host,nslookup,dig依赖bind包,所以先看一下系统有没有bind包 命令如下:rpm -qa |grep bind 如果没有或者版本太低请升级安装 命令是:yum install bi ...
- Java关键字——throws和throw
throws关键字 在定义一个方法时,可以使用throws关键字声明,使用throws声明的方法表示此方法不处理异常,而交给方法的调用处进行处理. 使用了throws关键字,表示不管是否会有异常,在调 ...
- JavaWeb学习笔记——Tomcat配置
使用的Tomcat版本是apache-tomcat-6.0.20 详细的环境变量配置参考<windows 7系统安装与配置Tomcat服务器环境> 网址为http://jingyan.ba ...
- Maven概览
Maven的核心思想,约定由于配置 1 Maven坐标 1.1 本项目的坐标 groupId: 必须.项目组名称,定义当前Maven项目所隶属的实际项目,通常与域名反向一一对应,与Java包名表示方式 ...
- Euclidean Space
http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
- ecshop商品-》购买记录
注意事项:{insert name='bought_notes' id=$id} 有了产品ID一切都好说 html代码 <!-- #BeginLibraryItem "/libra ...