一、音乐播放器常见的操作
/*变量声明*/
private ImageButton playBtn = null;//播放、暂停
private ImageButton latestBtn = null;//上一首
private ImageButton nextButton = null;//下一首
private ImageButton forwardBtn = null;//快进
private ImageButton rewindBtn = null;//快退
private TextView playtime = null;//已播放时间
private TextView durationTime = null;//歌曲时间
private SeekBar seekbar = null;//歌曲进度
private Handler handler = null;//用于进度条
private Handler fHandler = null;//用于快进
private int currentPosition;//当前播放位置
/*获得列表传过来的数据*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.play);
Intent intent = this.getIntent();
Bundle bundle = intent.getExtras();
_ids = bundle.getIntArray("_ids"); //获得保存音乐文件_ID的数组
position = bundle.getInt("position"); //获得应该播放的音乐的号数,既播放第几首
//代码未完,见下面的代码
}
/*初始化控件*/
playtime = (TextView)findViewById(R.id.playtime); //显示已经播放的时间
durationTime = (TextView)findViewById(R.id.duration); //显示歌曲总时间
playBtn = (ImageButton)findViewById(R.id.playBtn); //开始播放、暂停播放按钮
latestBtn = (ImageButton)findViewById(R.id.latestBtn); //播放上一首按钮
nextButton = (ImageButton)findViewById(R.id.nextBtn); //播放下一首按钮
forwardBtn = (ImageButton)findViewById(R.id.forwardBtn); //快进按钮
rewindBtn = (ImageButton)findViewById(R.id.rewindBtn); //快退按钮
seekbar = (SeekBar)findViewById(R.id.seekbar); //播放进度条
/*定义各控件的回调函数*/
playBtn.setOnClickListener(
new
View.OnClickListener() {
//点击“播放、暂停”按钮时回调
@Override
public
void
onClick(View v) {
if
(mp.isPlaying()){
//如果正在播放则暂停
pause();
playBtn.setBackgroundResource(
R.drawable.play_selecor);
//更改按键状态图标
}
else
{
//如果没有播放则恢复播放
play();
playBtn.setBackgroundResource(
R.drawable.pause_selecor);
//更改按键状态图标
}
}
});
latestBtn.setOnClickListener(
new
View.OnClickListener() {
//点击“播放上一首”按钮时回调
@Override
public
void
onClick(View v) {
int
num = _ids.length;
//获得音乐的数目
if
(position==
0
){
//如果已经时第一首则播放最后一首
position=num-
1
;
}
else
{
//否则播放上一首
position-=
1
;
}
int
pos = _ids[position];
//得到将要播放的音乐的_ID
setup();
//做播放前的准备工作
play();
//开始播放
}
});
nextButton.setOnClickListener(
new
View.OnClickListener(){
//点击“播放下一首”按钮时回调
@Override
public
void
onClick(View v) {
int
num = _ids.length;
//获得音乐的数目
if
(position==num-
1
){
//如果已经是最后一首,则播放第一首
position=
0
;
}
else
{
position+=
1
;
//否则播放下一首
}
int
pos = _ids[position];
//得到将要播放的音乐的_ID
setup();
//做播放前的准备工作
play();
//开始播放
}
});
forwardBtn.setOnTouchListener(
new
OnTouchListener() {
//点击“快进”按钮时回调
@Override
public
boolean
onTouch(View v, MotionEvent event) {
switch
(event.getAction()) {
case
MotionEvent.ACTION_DOWN:
fHandler.post(forward);
//此处使用handler对象更新进度条
mp.pause();
//点击快进按钮时,音乐暂停播放
break
;
case
MotionEvent.ACTION_UP:
fHandler.removeCallbacks(forward);
mp.start();
//松开快进按钮时,音乐暂恢复播放
playBtn.setBackgroundResource(
R.drawable.pause_selecor);
break
;
}
return
false
;
}
});
rewindBtn.setOnTouchListener(
new
OnTouchListener() {
//点击“快退”按钮时回调
@Override
public
boolean
onTouch(View v, MotionEvent event) {
switch
(event.getAction()) {
case
MotionEvent.ACTION_DOWN:
fHandler.post(rewind);
mp.pause();
//点击快退按钮时,音乐暂暂停播放
break
;
case
MotionEvent.ACTION_UP:
fHandler.removeCallbacks(rewind);
mp.start();
//松开快退按钮时,音乐暂恢复播放
playBtn.setBackgroundResource(
R.drawable.pause_selecor);
break
;
}
return
false
;
}
});
seekbar.setOnSeekBarChangeListener(
new
OnSeekBarChangeListener() {
@Override
public
void
onStopTrackingTouch(SeekBar seekBar) {
mp.start();
//停止拖动进度条时,音乐开始播放
}
@Override
public
void
onStartTrackingTouch(SeekBar seekBar) {
mp.pause();
//开始拖动进度条时,音乐暂停播放
}
@Override
public
void
onProgressChanged(SeekBar seekBar,
int
progress,
boolean
fromUser) {
if
(fromUser){
mp.seekTo(progress);
//当进度条的值改变时,音乐播放器从新的位置开始播放
}
}
});
最近做了一个android音乐播放器,个人感觉最难的就是“后台播放”以及有关“播放列表”的部分,但是总算是找到了实现的方式。不同的人实现 的方式可能不一样,这里我就分享一下自己对“播放列表”这个模块的一些实现方法,“后台播放”会在下一篇博文中进行介绍,希望大家也能分享一下自己的一些 思路。
android使用ContentProvider来支持不同应用程序的数据共享,为了方便其他应用程序对sdcard中的数据进行操作,sdcard也 提供了ContentProvider接口,这里就以访问音频文件为例,视频以及图片的操作也类似,这里就不在赘述。
访问sdcard中的音频文件的URI为MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,为了使播 放列表显示所以音乐文件的信息,这里需要查询sdcard里的音频文件,并把查询到的信息保存在Cursor中,具体代码如下:
<pre>Cursor c = this .getContentResolver().</pre>
<pre>query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,</pre>
/*这个字符串数组表示要查询的列*/
|
new String[]{MediaStore.Video.Media.TITLE, //音乐名
MediaStore.Audio.Media.DURATION, //音乐的总时间
MediaStore.Audio.Media.ARTIST, //艺术家
MediaStore.Audio.Media._ID, //id号
MediaStore.Audio.Media.DISPLAY_NAME, //音乐文件名
MediaStore.Audio.Media.DATA //音乐文件的路径
},
null , //查询条件,相当于sql中的where语句
null , //查询条件中使用到的数据
null ); //查询结果的排序方式
|
通过MediaStore.Audio.Media.XXX来访问音乐文件的一些信息,这里只列出了一部分,可以根据需要进行增添和删除。
至此,Cursor c就已经保存了sdcard内所以音频文件的信息,下面的操作就是围绕这个Cursor展开的。
首先定义三个数组:
private
int
[] _ids;
//存放音乐文件的id数组
private
String[] _titles;
//存放音乐文件的标题数组
private
String[] _path;
//存放音乐文件的路径
_ids保存了所有音乐文件的_ID,用来确定到底要播放哪一首歌曲,_titles存放音乐名,用来显示在播放界面,而_path存放音乐文件的路径(删除文件时会用到)。
接下来再定义一个变量,用来定位选择的是哪一首音乐:
private int pos;
接下来将音乐文件的信息存放在相应的数组中:
c.moveToFirst();
_ids = new int [c.getCount()];
_titles = new String[c.getCount()];
_path = new String[c.getCount()];
for ( int i=0;i<c.getCount();i++){
_ids[i] = c.getInt(3);
_titles[i] = c.getString(0);
_path[i] = c.getString(5).substring(4);
c.moveToNext();
}
|
有人可能会问为什么获取路径的格式是_path[i]=c.geString(5).substring(4)?因为MediaStore.Audio.Media.DATA
得到的内容格式为/mnt/sdcard/[子文件夹名/]音乐文件名,而我们想要得到的是/sdcard/[子文件夹名]音乐文件名,
所以要做相应的裁剪操作。
接下来把Cursor中的信息显示到listview中:
MusicListAdapter adapter = new MusicListAdapter( this , c);
listview.setAdapter(adapter);
|
MusicListAdapter是一个自定义的Adapter,继承自BaseAdapter,这里只贴出代码,不做讲解。
package com.alex.video;
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
public class MusicListAdapter extends BaseAdapter{
private Context myCon;
private Cursor myCur;
private int pos=-1;
public MusicListAdapter(Context con,Cursor cur){
this .myCon = con;
this .myCur = cur;
}
@Override
public int getCount() {
return this .myCur.getCount();
}
@Override
public Object getItem( int position) {
return position;
}
@Override
public long getItemId( int position) {
return position;
}
@Override
public View getView( int position, View convertView, ViewGroup parent) {
convertView = LayoutInflater.from(myCon).inflate(R.layout.musiclist,
null );
myCur.moveToPosition(position);
TextView videoTitle = (TextView)convertView.findViewById(R.id.musictitle);
if (myCur.getString(0).length()>24){
try {
String musicTitle = bSubstring(myCur.getString(0).trim(),24);
videoTitle.setText(musicTitle);
} catch (Exception e) {
e.printStackTrace();
}
} else {
videoTitle.setText(myCur.getString(0).trim());
}
TextView videoArtist = (TextView)convertView.findViewById(R.id.musicartist);
if (myCur.getString(2).equals( "<unknown>" )){
videoArtist.setText( "未知艺术家" );
} else {
videoArtist.setText(myCur.getString(2));
}
TextView videoTime = (TextView)convertView.findViewById(R.id.musictime);
videoTime.setText(toTime(myCur.getInt(1)));
ImageView videoItem = (ImageView)convertView.findViewById(R.id.musicitem);
videoItem.setImageResource(R.drawable.item);
return convertView;
}
/*时间格式转换*/
public String toTime( int time) {
time /= 1000;
int minute = time / 60;
int hour = minute / 60;
int second = time % 60;
minute %= 60;
return String.format( "%02d:%02d" , minute, second);
}
/*字符串裁剪*/
public static String bSubstring(String s, int length) throws Exception
{
byte [] bytes = s.getBytes( "Unicode" );
int n = 0; // 表示当前的字节数
int i = 2; // 要截取的字节数,从第3个字节开始
for (; i < bytes.length && n < length; i++)
{
// 奇数位置,如3、5、7等,为UCS2编码中两个字节的第二个字节
if (i % 2 == 1)
{
n++; // 在UCS2第二个字节时n加1
}
else
{
// 当UCS2编码的第一个字节不等于0时,该UCS2字符为汉字,一个汉字算两个字节
if (bytes[i] != 0)
{
n++;
}
}
}
// 如果i为奇数时,处理成偶数
if (i % 2 == 1)
{
// 该UCS2字符是汉字时,去掉这个截一半的汉字
if (bytes[i - 1] != 0)
i = i - 1;
// 该UCS2字符是字母或数字,则保留该字符
else
i = i + 1;
}
return new String(bytes, 0, i, "Unicode" );
}
}
|
在上一篇随笔中,我介绍了如何在程序中查询sdcard内的多媒体文件,并且显示到播放列表中,但是,如果在sdcard内删除、增加一些多媒体文件,如何让播放列表也更新呢,这里我分享一下自己在项目中的一些解决方法,希望对大家有所帮助。
首先,我简单介绍一下android是如何扫描sdcard内的多媒体信息的,详细请阅读stay的博文:http://www.cnblogs.com/stay/articles/1957571.html
private void scanSdCard(){
IntentFilter intentfilter = new IntentFilter( Intent.ACTION_MEDIA_SCANNER_STARTED);
intentfilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
intentfilter.addDataScheme("file");
scanSdReceiver = new ScanSdReceiver();
registerReceiver(scanSdReceiver, intentfilter);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + Environment.getExternalStorageDirectory().getAbsolutePath()))); }
其中ScanSdReceiver是一个自定义的广播接收器,继承自 BroadCastReceiver,因为android系统开始扫描sdcard以及扫描完毕时都会发送一个系统广播来表示当前扫描的状态,这样我们就 可以很方便通过判断当前的扫描状态加一些自己的逻辑操作,
ScanSdReceiver的代码如下:
其中ScanSdReceiver是一个自定义的广播接收器,继承自 BroadCastReceiver,因为android系统开始扫描sdcard以及扫描完毕时都会发送一个系统广播来表示当前扫描的状态,这样我们就 可以很方便通过判断当前的扫描状态加一些自己的逻辑操作,ScanSdReceiver的代码如下:
public class ScanSdReceiver extends BroadcastReceiver {
private AlertDialog.Builder builder = null ;
private AlertDialog ad = null ;
private int count1;
private int count2;
private int count;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_MEDIA_SCANNER_STARTED.equals(action)){
Cursor c1 = context.getContentResolver()
.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME },
null , null , null );
count1 = c1.getCount();
System. out .println( "count:" +count);
builder = new AlertDialog.Builder(context);
builder.setMessage( "正在扫描存储卡..." );
ad = builder.create();
ad.show();
} else if (Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)){
Cursor c2 = context.getContentResolver()
.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME },
null , null , null );
count2 = c2.getCount();
count = count2-count1;
ad.cancel();
if (count>=0){
Toast.makeText(context, "共增加" +
count + "首歌曲" , Toast.LENGTH_LONG).show();
} else {
Toast.makeText(context, "共减少" +
count + "首歌曲" , Toast.LENGTH_LONG).show();
}
}
}
}
|
private void deleteMusic( int position){
this .getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media._ID + "=" + _ids[position],
null );
}
|
其中“多媒体文件的ID”可以从_ids数组中取得(关于_ids数组请 参考第一张的内容)。这样,我们是否就已经从SDCARD中删除了指定_ID的多媒体文件了呢? 其实当你打开FileExplorer时会发现,原来的文件并没有被删除,— —!杯具,搞了半天文件还在,我第一次遇到这个问题的时候也是纠结了老半天。。。
private void deleteMusicFile( int position){
File file = new File(_path[position]);
file.delete();
}
|
android自带播放器支持“边下载边播放”的功能,当你使用系统浏览器点击一个“MP3的下载链接”时,它就会自动播放这首歌曲并保存到本地(不知道用第三方浏览器是否也如此,笔者认为应该是系统浏览器会自动识别MP3下载链接,并调用系统播放器来播放)。
与这个过程类似,在笔者做的音乐播放器中,当用户选择“歌曲下载”时,会转到一个webview中,这里我将webview的初始url定向 到"htpp://www.top100.cn"(巨鲸音乐),当点击MP3的下载链接时,就会将音乐下载到sdcard的根目录。webview所在 activity的代码如下:
setContentView(R.layout.web);
web = (WebView)findViewById(R.id.web);
web.setWebViewClient( new DownLoadWebViewClient( this ));
WebSettings s = web.getSettings();
s.setSaveFormData( false );
s.setSavePassword( false );
s.setUseWideViewPort( true );
s.setJavaScriptEnabled( true );
s.setLightTouchEnabled( true );
web.setWebChromeClient( new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
//Activity和Webview根据加载程度决定进度条的进度大小
//当加载到100%的时候 进度条自动消失
context.setProgress(progress * 100 );
}
});
web.loadUrl( "http://www.top100.cn/" );
|
|
web.setWebViewClient(new DownLoadWebViewClient(this));其中DownLoadWebViewClient就使我们下载MP3文件的关键,它继承自 WebViewClient,这里我们Override它的public boolean shouldOverrideUrlLoading(WebView view, String url)方法,在方法中我们判断点击的链接时否为“下载MP3文件”,如果成立,则启动一个service来下载mp3文件,代码如下:
public class DownLoadWebViewClient extends WebViewClient {
private Context context;
public DownLoadWebViewClient(Context context){
this .context = context;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.i( "info" , "open an url" );
String urlStr = "" ; //存放解码后的url
//如果是utf8编码
if (isUtf8Url(url)){
urlStr = Utf8URLdecode(url);
//如果不是utf8编码
} else {
urlStr = URLDecoder.decode(url);
}
//如果链接是下载top100.cn中的mp3文件
if (url.substring(url.length()- 4 ).equals( ".mp3" )&&url.substring( 7 , 10 ).equals( "221" )){
Log.i( "info" , "mp3 file" );
String ss[] = urlStr.split( "/" );
String musicName = ss[ss.length- 1 ]; //得到音乐文件的全名(包括后缀)
Log.i( "info" , "musicfile: " + musicName);
//将下载链接和文件名传递给下载模块
Intent intent = new Intent(context,DownLoadService. class );
intent.putExtra( "url" , url);
intent.putExtra( "musicName" , musicName);
context.startService(intent);
}
return super .shouldOverrideUrlLoading(view, url);
}
|
这里略去了url解码的相关方法。其中DownLoadService用于下载MP3文件并在,它接收DownLoadWebViewClient传递来的url和音乐文件名,代码如下:
public
class DownLoadService extends Service implements Runnable{ //实现Runable接口
private String URL_str; //网络歌曲的路径
private File download_file; //下载的文件
private int total_read = 0 ; //已经下载文件的长度(以字节为单位)
private int readLength = 0 ; //一次性下载的长度(以字节为单位)
private int music_length = 0 ; //音乐文件的长度(以字节为单位)
private boolean flag = false ; //是否停止下载,停止下载为true
private Thread downThread; //下载线程
private String musicName; //下载的文件名
@Override
public IBinder onBind(Intent intent) {
return null ;
}
@Override
public void onCreate() {
downThread = new Thread( this ); //初始化下载线程
downThread.start();
}
@Override
public void onStart(Intent intent, int startId) {
URL_str = intent.getExtras().getString( "url" ); //获取下载链接的url
musicName = intent.getExtras().getString( "musicName" ); //获取下载的文件名
}
@Override
public void onDestroy() {
flag = true ; //停止下载
}
//实现Run方法,进行歌曲的下载
@Override
public void run() {
FileOutputStream fos = null ; //文件输出流
FileInputStream fis = null ; //文件输出流
InputStream is = null ; //网络文件输入流
URL url = null ;
try {
url = new URL(URL_str); //网络歌曲的url
HttpURLConnection httpConnection = null ;
httpConnection = (HttpURLConnection)
url.openConnection(); //打开网络连接
download_file = new File(Environment. //在sdcard根目录建立一个与要下载的文件的名字相同的文件
getExternalStorageDirectory()
+ "/" + musicName);
fos = new FileOutputStream(download_file, true ); //初始化文件输出流
fis = new FileInputStream(download_file); //初始化文件输入流
total_read = fis.available(); //初始化“已下载部分”的长度,此处应为0
music_length = httpConnection.getContentLength(); //要下载的文件的总长度
if (is == null ) { //如果下载失败则打印日志,并返回
Log.i( "info" , "donload failed..." );
return ;
}
byte buf[] = new byte [ 1024 ]; //定义下载缓冲区
readLength = 0 ; //一次性下载的长度
Log.i( "info" , "download start..." );
//向前台发送开始下载广播
Intent startIntent = new Intent();
startIntent.setAction( "com.alex.downloadstart" );
sendBroadcast(startIntent);
//如果读取网络文件的数据流成功,且用户没有选择停止下载,则开始下载文件
while (readLength != - 1 && !flag) {
if ((readLength = is.read(buf))> 0 ){
fos.write(buf, 0 , readLength);
total_read += readLength; //已下载文件的长度增加
}
if (total_read == music_length) { //当已下载的长度等于网络文件的长度,则下载完成
flag = false ;
Log.i( "info" , "download complete..." );
//向前台发送下载完成广播
Intent completeIntent = new Intent();
completeIntent.setAction( "com.alex.downloadcompleted" );
sendBroadcast(completeIntent);
//关闭输入输出流
fos.close();
is.close();
fis.close();
}
Thread.sleep( 50 ); //当前现在休眠50毫秒
Log.i( "info" , "download process : " //打印下载进度
+ ((total_read+ 0.0 )/music_length* 100 + "" ).substring( 0 , 4 )+ "%" );
}
} catch (Exception e) {
Intent errorIntent = new Intent();
errorIntent.setAction( "com.alex.downloaderror" );
sendBroadcast(errorIntent);
e.printStackTrace();
}
}
}
|
这里有个小bug,如果下载同一首歌曲多次,程序不会多次新建文件,而将数据写入与之同名的文件中。