第3节 获取音乐信息
在“视频播放器”的开发过程当中,我们已经学会了如何获取视频文件的信息:
- 定义一个视频信息的数据结构
VideoItem
; - 自定义一个
AnsycTask
,在它的工作线程中查询视频; - 在工作线程中,访问
MediaProvider
,获取视频数据; - 将视频数据显示到自定义的列表项中;
这里列举音乐文件,也采用类似的方式:
- 定义一个音乐信息的数据结构
MusicItem
; - 自定义一个
AnsycTask
,在它的工作线程中查询音乐; - 在工作线程中,访问
MediaProvider
,获取音乐数据; - 将音乐数据显示到自定义的列表项中;
3.1 音乐数据的获取原理
与获取视频信息的方式几乎一样,获取音乐信息也是通过系统自带的MediaProvider
。MediaProvider
里面存储了所有的多媒体数据信息,不仅有视频,还包括各种音频文件。
向
Media Provider
发出查询请求的地址-uri,它就像访问网站时,要输入的网址一样。系统提供了两个位置的uri,一个是指向内部存储的uri,一个是指向外部存储的uri。我们要查询的音乐文件都是存放在外部存储地址上的;Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
确定要请求的视频文件信息。在视频列表中,我们需要展示视频的标题、创建时间,还需要播放它时使用的文件所在地址。这些信息在
Media Provider
中都对应着查询它们使用的字段名称;String[] searchKey = new String[] { MediaStore.Audio.Media._ID,-->对应文件在数据库中的检索ID MediaStore.Audio.Media.TITLE, -->对应文件的标题 MediaStore.Audio.Albums.ALBUM_ID,-->对应文件所在的专辑ID,在后面获取封面图片时会用到 MediaStore.Audio.Media.DATA, -->对应文件的存放位置 MediaStore.Audio.Media.DURATION -->对应文件的播放时长 };
确定查询的条件。我们之前假设过:只关心那些包含了
music
关键字的目录。因此我们要确定的只是查询到的文件路径中,包含有music
这个字段。String where = MediaStore.Audio.Media.DATA + " like \"%"+"/music"+"%\"";
这个条件参数的写法就和
SQL
数据库语言的语法一样。这里我们不打算讲SQL
语法,只要知道在我们这个例子中这样使用就好了;设定查询结果的排序方式,使用默认的排序方式就可以了,
String sortOrder = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
获取ContentResolver对象,让它使用前面的参数向
Media Provider
发起查询请求;查询的结果存放在Cursor
--指标当中;ContentResolver resolver = getContentResolver(); Cursor cursor = resolver.query( uri, searchKey, where, null, sortOrder);
遍历
Cursor
,得到它指向的每一条查询到的信息;当Cursor
指向某条数据的时候,我们就获取它携带的每个字段的值;while(cursor.moveToNext()) { //获取音乐的路径 String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)); //获取音乐的ID String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)); //通过URI和ID,组合出改音乐特有的Uri地址 Uri musicUri = Uri.withAppendedPath(uri, id); //获取音乐的名称 String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)); //获取音乐的时长,单位是毫秒 long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)); //获取该音乐所在专辑的id int albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ID)); //再通过AlbumId组合出专辑的Uri地址 Uri albumUri = ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId); ...... }
每首音乐都有一个全局唯一的URI地址,操作某首具体的音乐就可以通过这个地址来完成。而
id
就是用来获取该音乐的URI地址的,将id
与音频的URI地址组合一下就能得到特定某首音乐的URI地址,它的形式就像content://media/external/audio/media/20310
,Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Uri musicUri = Uri.withAppendedPath(uri, id);
获取音乐的封面,
需要知道它所属的
专辑id
-albumId
,int albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ID));
根据这个id与专辑封面的uri(
content://media/external/audio/albumart
)组合,得到专辑所在的Uri,它的形式就像content://media/external/audio/albumart/4
Uri albumUri = ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId);
通过下面的方式,创建出封面图片,
public Bitmap createThumbFromUir(ContentResolver res, Uri albumUri) { InputStream in = null; Bitmap bmp = null; try { in = res.openInputStream(albumUri); BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options(); bmp = BitmapFactory.decodeStream(in, null, sBitmapOptions); in.close(); } catch (FileNotFoundException e) { } catch (IOException e) { e.printStackTrace(); } return bmp; }
这是一个很多模块都可能用到的功能,我们将它做成一个函数,放到
Utils.java
文件中,便于其它模块使用,
class Utils { static public Bitmap createThumbFromUir(ContentResolver res, Uri albumUri) { InputStream in = null; Bitmap bmp = null; try { in = res.openInputStream(albumUri); BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options(); bmp = BitmapFactory.decodeStream(in, null, sBitmapOptions); in.close(); } catch (FileNotFoundException e) { } catch (IOException e) { e.printStackTrace(); } return bmp; } }
存放获取的视频文件信息,创建一个
MusicItem类
,public class MusicItem { String name; --存储音乐的名字 Uri songUri; --存储音乐的Uri地址 Uri albumUri;--存储音乐封面的Uri地址 Bitmap thumb;--存储封面图片 long duration;--存储音乐的播放时长,单位是毫秒 MusicItem(Uri songUri, Uri albumUri, String strName, long duration) { this.name = strName; this.songUri = songUri; this.duration = duration; this.albumUri = albumUri; } ...... }
创建封面图片的方法上面已经介绍过了。一开始,我们可以暂时不用创建出封面图片,只保留
albumId
,在需要的时候再去创建出封面,一旦创建出封面图片,就把它保存在thumb
这个成员变量当中。创建一个
MusicItem
,MusicItem data = new MusicItem(musicUri, albumUri, name, duration);
Cursor
使用完了之后要把它关闭掉,cursor.close();
整理一下前面的各个步骤,获取外部存储上music目录
中所有音频文件的方式如下,
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {
MediaStore.Audio.Media._ID,-->对应文件在数据库中的检索ID
MediaStore.Audio.Media.TITLE, -->对应文件的标题
MediaStore.Audio.Albums.ALBUM_ID,-->对应文件所在的专辑ID,在后面获取封面图片时会用到
MediaStore.Audio.Media.DATA, -->对应文件的存放位置
MediaStore.Audio.Media.DURATION -->对应文件的播放时长
};
String where = MediaStore.Audio.Media.DATA + " like \"%"+"/music"+"%\"";
String sortOrder = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(
uri,
searchKey,
where,
null,
sortOrder);
while(cursor.moveToNext())
{
//获取音乐的路径,这个参数我们实际上不会用到,不过在调试程序的时候可以方便我们看到音乐的真实路径,确定寻找的文件的确就在我们规定的目录当中
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
//获取音乐的ID
String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
//通过URI和ID,组合出改音乐特有的Uri地址
Uri musicUri = Uri.withAppendedPath(uri, id);
//获取音乐的名称
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
//获取音乐的时长,单位是毫秒
long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
//获取该音乐所在专辑的id
int albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ID));
//再通过AlbumId组合出专辑的Uri地址
Uri albumUri = ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId);
//创建一个MusicItem
MusicItem data = new MusicItem(musicUri, albumUri, name, duration);
}
cursor.close();
最后一点千万不要忘记,要在应用的AndroidManifest.xml
文件中,添加读取外部存储器的权限,
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.anddle.anddleplayer">
<!--添加读取存储器的权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
......
</manifest>
3.2 在工作线程中获取音乐信息
主线程中不能做耗费时间到事情,如果要进行耗时的操作需要开启一个工作线程,把耗时操作交给工作线程处理。查询音乐的信息就是一个耗时的操作(准确的说是不知道什么时候能查询完,如果设备上的音乐文件很少,那么也许很快就完成了,但如果音乐文件很多,也会会花上很多很多时间)。
为此,我们还是采用“视频播放器”中的设计,创建一个叫做MusicUpdateTask
的AsyncTask
,在它的doInBackground()
方法中去进行查询操作。
AsyncTask
的原理和用法,我们已经在“视频播放器”的部分详细讲过了,这里就假设大家对它的使用已经比较熟悉了。
private class MusicUpdateTask extends AsyncTask<Object, MusicItem, Void> {
List<MusicItem> mDataList = new ArrayList<MusicItem>();
@Override
protected Void doInBackground(Object... params) {
//这里是工作线程,处理耗时的查询音乐的操作
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Albums.ALBUM_ID,
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.DURATION
};
String where = MediaStore.Audio.Media.DATA + " like \"%"+getString(R.string.search_path)+"%\"";
String [] keywords = null;
String sortOrder = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(uri, searchKey, where, keywords, sortOrder);
if(cursor != null)
{
while(cursor.moveToNext() && ! isCancelled())
{
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
Uri musicUri = Uri.withAppendedPath(uri, id);
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
int albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ID));
Uri albumUri = ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId);
MusicItem data = new MusicItem(musicUri, albumUri, name, duration, 0/*, false*/);
if (uri != null) {
ContentResolver res = getContentResolver();
data.thumb = Utils.createThumbFromUir(res, albumUri);
}
Log.d(TAG, "real music found: " + path);
publishProgress(data);
}
cursor.close();
}
return null;
}
@Override
protected void onProgressUpdate(MusicItem... values) {
MusicItem data = values[0];
//这是主线程,在这里把要显示的音乐添加到音乐的展示列表当中。
}
}
与“视频播放器”的设计不同,这里我们简化了设计,只用到了AsyncTask
的onProgressUpdate()
方法和doInBackground()
方法。
另外,在列表加载的过程中,也没有让用户手动选择停止加载列表的功能。如果你觉得这个功能很重要,可以模仿“视频播放器”的设计,在这里添加上。我们在这里取消了这个设计,主要的目的只是为了让大家快速的梳理一下曾经具有的开发经验,节省时间,从而尽快的将我们带入带新的开发知识上去。
在使用MusicUpdateTask
的时候,在主界面所在的MusicListActivity
创建和销毁时,分别启动和取消MusicUpdateTask
的运行,
public class MusicListActivity extends AppCompatActivity {
private MusicUpdateTask mMusicUpdateTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_list);
......
mMusicUpdateTask = new mMusicUpdateTask();
mMusicUpdateTask.execute();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mMusicUpdateTask != null && mMusicUpdateTask.getStatus() == AsyncTask.Status.RUNNING) {
mMusicUpdateTask.cancel(true);
}
mMusicUpdateTask = null;
}
}
3.3 音乐列表展示
我们给音乐列表设计了如下的界面,

我们已经知道了如何实现一个自定义的列表,
- 为每一条列表项定义它的外貌-布局文件;
- 自定义一个
Adapter
,将要展示的数据放入其中; - 在主界面的布局文件中放入
ListView
控件,将Adapter
设置到ListView
中,让ListView
展示所有数据;
3.3.1 音乐项的界面布局
每个数据项,要展示音乐的封面、名称、播放时长,

为此,我们给它定义个布局music_item.xml
,将界面区域这样分割:

ImageView
用来放置歌曲封面,给它指定一个图片的大小,150dp x 100dp
;背景颜色采用主题属性colorPrimary
的颜色;封面的id设置成
music_thumb
,音乐名称的id设置成music_title
,播放时长的id设置成music_duration
;
整个布局方式如下,
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/music_thumb"
android:layout_width="150dp"
android:layout_height="100dp"
android:scaleType="center"
android:padding="5dp"
android:layout_margin="5dp"
android:background="@color/colorPrimary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/music_title"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_vertical"
android:layout_margin="2dp"
style="?android:attr/textAppearanceMedium"
android:lines="2"
android:layout_weight="2"/>
<TextView
android:id="@+id/music_duration"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_vertical"
android:layout_margin="2dp"
style="?android:attr/textAppearanceSmall"
android:singleLine="true"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>
3.3.2 自定义Adapter
这里自定义MusicItemAdapter
的方法,与自定义VideoItemAdapter
的方法几乎一模一样,只是,
Adapter
保存的数据类型不同,从VideoItem
变成了MusicItem
;时间格式我们将显示成
32:21
这种形式,所以可以采用这样的实现,public String convertMSecendToTime(long time) { SimpleDateFormat mSDF = new SimpleDateFormat("mm:ss"); Date date = new Date(time); String times= mSDF.format(date); return times; }
这是一个很多模块都可能用到的功能,我们将它做成一个函数,放到
Utils.java
文件中,便于其它模块使用,class Utils { static public String convertMSecendToTime(long time) { SimpleDateFormat mSDF = new SimpleDateFormat("mm:ss"); Date date = new Date(time); String times= mSDF.format(date); return times; } ...... }
MusicItemAdapter
的实现如下,
public class MusicItemAdapter extends BaseAdapter {
private List<MusicItem> mData;
private final LayoutInflater mInflater;
private final int mResource;
private Context mContext;
public MusicItemAdapter(Context context, int resId, List<MusicItem> data)
{
mContext = context;
mData = data;
mInflater = LayoutInflater.from(context);
mResource = resId;
}
@Override
public int getCount() {
return mData != null ? mData.size() : 0;
}
@Override
public Object getItem(int position) {
return mData != null ? mData.get(position): null ;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(mResource, parent, false);
}
MusicItem item = mData.get(position);
TextView title = (TextView) convertView.findViewById(R.id.music_title);
title.setText(item.name);
TextView createTime = (TextView) convertView.findViewById(R.id.music_duration);
//调用辅助函数转换时间格式
String times = Utils.convertMSecendToTime(item.duration);
times = String.format(mContext.getString(R.string.duration), times);
createTime.setText(times);
ImageView thumb = (ImageView) convertView.findViewById(R.id.music_thumb);
if(thumb != null) {
if (item.thumb != null) {
thumb.setImageBitmap(item.thumb);
} else {
thumb.setImageResource(R.mipmap.default_cover);
}
}
return convertView;
}
}
3.3.3 通过ListView使用Adapter
有了Adapter和数据,就要把它们结合起来,放到ListView
中,以列表的形式展示出来。
在
MusicListActivity
的布局activity_music_list.xml
当中,加入列表,<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:id="@+id/music_list" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
在
onCreate()
中,创建Adapter,并设置给ListView
,private List<MusicItem> mMusicList; private ListView mMusicListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_music_list); mMusicList = new ArrayList<MusicItem>(); mMusicListView = (ListView) findViewById(R.id.music_list); MusicItemAdapter adapter = new MusicItemAdapter(this, R.layout.music_item, mMusicList); mMusicListView.setAdapter(adapter); }
将
MusicUpdateTask
获取的音乐数据放入到ListView
当中,需要完善MusicUpdateTask
的()
方法,@Override protected void onProgressUpdate(MusicItem... values) { MusicItem data = values[0]; //这是主线程,在这里把要显示的音乐添加到音乐的展示列表当中。 mMusicList.add(data); MusicItemAdapter adapter = (MusicItemAdapter) mMusicListView.getAdapter(); adapter.notifyDataSetChanged(); }
因为列表中显示封面的时候,创建了不少图片,在Activity退出的时候,我们要手动的回收这些图片占用的空间,
@Override
protected void onDestroy() {
super.onDestroy();
if(mMusicUpdateTask != null && mMusicUpdateTask.getStatus() == AsyncTask.Status.RUNNING) {
mMusicUpdateTask.cancel(true);
}
mMusicUpdateTask = null;
//手动回收使用的图片资源
for(MusicItem item : mMusicList) {
if( item.thumb != null ) {
item.thumb.recycle();
item.thumb = null;
}
}
mMusicList.clear();
}
好了,当我们运行应用的时候,就会看到设备上music
目录下的音乐都被显示出来了。

3.4 测试音乐播放
音乐列表准备好了,我们来试试播放音乐吧。
前面我们介绍了如何使用MediaPlayer
来播放音乐,这里我们让用户点击音乐列表上的特定音乐后,就开始播放它,
为音乐列表的音乐项设置点击的监听,
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_music_list); ...... //设置监听器 mMusicListView.setOnItemClickListener(mOnMusicItemClickListener); } //定义监听器 private AdapterView.OnItemClickListener mOnMusicItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //添加播放音乐的代码 } };
在点击响应处,添加播放音乐的代码,
//定义监听器 private AdapterView.OnItemClickListener mOnMusicItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //添加播放音乐的代码 MusicItem item = mMusicList.get(position); try { mMusicPlayer.reset(); mMusicPlayer.setDataSource(MusicListActivity.this, item.songUri); mMusicPlayer.prepare(); mMusicPlayer.start(); } catch (IOException e) { e.printStackTrace(); } } };
每次点击前都调用一次
mMusicPlayer.reset()
,可以清除以前播放器的状态。
这样一来,用户点击音乐的时候,就可以播放制定的音乐了。
**注意,这里只是为了让我们直观的感受到音乐列表的完成,并能够播放音乐,但是程序的框架并没有按照我们之前设计那样,所以只能算是效果的验证。
在后面的章节中,我们将修改这里的设计,按照正确的设计框架来实现音乐播放。**
所以最后,让我们清除这段测试播放音乐用的代码吧。
/*******************************************************************/
* 版权声明
* 本教程只在优快云和安豆网发布,其他网站出现本教程均属侵权。
*另外,我们还推出了Arduino智能硬件相关的教程,您可以在我们的网店跟我学Arduino编程中购买相关硬件。同时也感谢大家对我们这些码农的支持。
*最后再次感谢各位读者对安豆
的支持,谢谢:)
/*******************************************************************/