在“视频播放器”的开发过程当中,我们已经学会了如何获取视频文件的信息:
VideoItem
;AnsycTask
,在它的工作线程中查询视频;MediaProvider
,获取视频数据;这里列举音乐文件,也采用类似的方式:
MusicItem
;AnsycTask
,在它的工作线程中查询音乐;MediaProvider
,获取音乐数据;与获取视频信息的方式几乎一样,获取音乐信息也是通过系统自带的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>
主线程中不能做耗费时间到事情,如果要进行耗时的操作需要开启一个工作线程,把耗时操作交给工作线程处理。查询音乐的信息就是一个耗时的操作(准确的说是不知道什么时候能查询完,如果设备上的音乐文件很少,那么也许很快就完成了,但如果音乐文件很多,也会会花上很多很多时间)。
为此,我们还是采用“视频播放器”中的设计,创建一个叫做MusicUpdateTask
的AsyncTask
,在它的doInBackground()
方法中去进行查询操作。
AsyncTask
的原理和用法,我们已经在“视频播放器”的部分详细讲过了,这里就假设大家对它的使用已经比较熟悉了。
private class MusicUpdateTask extends AsyncTask<Object, MusicItem, Void> {
List mDataList = new ArrayList();
@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;
}
}
我们给音乐列表设计了如下的界面,
我们已经知道了如何实现一个自定义的列表,
Adapter
,将要展示的数据放入其中;ListView
控件,将Adapter
设置到ListView
中,让ListView
展示所有数据;每个数据项,要展示音乐的封面、名称、播放时长,
为此,我们给它定义个布局music_item.xml
,将界面区域这样分割:
ImageView
用来放置歌曲封面,给它指定一个图片的大小,150dp x 100dp
;背景颜色采用主题属性colorPrimary
的颜色;
封面的id设置成music_thumb
,音乐名称的id设置成music_title
,播放时长的id设置成music_duration
;
整个布局方式如下,
<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>
这里自定义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 mData;
private final LayoutInflater mInflater;
private final int mResource;
private Context mContext;
public MusicItemAdapter(Context context, int resId, List 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;
}
}
有了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 mMusicList;
private ListView mMusicListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_list);
mMusicList = new ArrayList();
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
目录下的音乐都被显示出来了。
音乐列表准备好了,我们来试试播放音乐吧。
前面我们介绍了如何使用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()
,可以清除以前播放器的状态。
这样一来,用户点击音乐的时候,就可以播放制定的音乐了。
**注意,这里只是为了让我们直观的感受到音乐列表的完成,并能够播放音乐,但是程序的框架并没有按照我们之前设计那样,所以只能算是效果的验证。
在后面的章节中,我们将修改这里的设计,按照正确的设计框架来实现音乐播放。**
所以最后,让我们清除这段测试播放音乐用的代码吧。
/*******************************************************************/
* 版权声明
* 本教程只在CSDN和安豆网发布,其他网站出现本教程均属侵权。
*另外,我们还推出了Arduino智能硬件相关的教程,您可以在我们的网店跟我学Arduino编程中购买相关硬件。同时也感谢大家对我们这些码农的支持。
*最后再次感谢各位读者对安豆
的支持,谢谢:)
/*******************************************************************/