手把手教你做音乐播放器(三)获取音乐信息

第3节 获取音乐信息

在“视频播放器”的开发过程当中,我们已经学会了如何获取视频文件的信息:

  1. 定义一个视频信息的数据结构VideoItem
  2. 自定义一个AnsycTask,在它的工作线程中查询视频;
  3. 在工作线程中,访问MediaProvider,获取视频数据;
  4. 将视频数据显示到自定义的列表项中;

这里列举音乐文件,也采用类似的方式:

  1. 定义一个音乐信息的数据结构MusicItem
  2. 自定义一个AnsycTask,在它的工作线程中查询音乐;
  3. 在工作线程中,访问MediaProvider,获取音乐数据;
  4. 将音乐数据显示到自定义的列表项中;

3.1 音乐数据的获取原理

与获取视频信息的方式几乎一样,获取音乐信息也是通过系统自带的MediaProviderMediaProvider里面存储了所有的多媒体数据信息,不仅有视频,还包括各种音频文件。

  1. Media Provider发出查询请求的地址-uri,它就像访问网站时,要输入的网址一样。系统提供了两个位置的uri,一个是指向内部存储的uri,一个是指向外部存储的uri。我们要查询的音乐文件都是存放在外部存储地址上的;

    Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
  2. 确定要请求的视频文件信息。在视频列表中,我们需要展示视频的标题、创建时间,还需要播放它时使用的文件所在地址。这些信息在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 -->对应文件的播放时长
    };
  3. 确定查询的条件。我们之前假设过:只关心那些包含了music关键字的目录。因此我们要确定的只是查询到的文件路径中,包含有music这个字段。

    String where = MediaStore.Audio.Media.DATA + " like \"%"+"/music"+"%\"";

    这个条件参数的写法就和SQL数据库语言的语法一样。这里我们不打算讲SQL语法,只要知道在我们这个例子中这样使用就好了;

  4. 设定查询结果的排序方式,使用默认的排序方式就可以了,

    String sortOrder = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
  5. 获取ContentResolver对象,让它使用前面的参数向Media Provider发起查询请求;查询的结果存放在Cursor--指标当中;

    ContentResolver resolver = getContentResolver();
    Cursor cursor = resolver.query(
                    uri,
                    searchKey, 
                    where, 
                    null, 
                    sortOrder);
  6. 遍历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);

    获取音乐的封面,

    1. 需要知道它所属的专辑idalbumId

      int albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ID));
    2. 根据这个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);
    3. 通过下面的方式,创建出封面图片,

      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;
        }
    }
  7. 存放获取的视频文件信息,创建一个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);                 
  8. 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 在工作线程中获取音乐信息

主线程中不能做耗费时间到事情,如果要进行耗时的操作需要开启一个工作线程,把耗时操作交给工作线程处理。查询音乐的信息就是一个耗时的操作(准确的说是不知道什么时候能查询完,如果设备上的音乐文件很少,那么也许很快就完成了,但如果音乐文件很多,也会会花上很多很多时间)。

为此,我们还是采用“视频播放器”中的设计,创建一个叫做MusicUpdateTaskAsyncTask,在它的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];

       //这是主线程,在这里把要显示的音乐添加到音乐的展示列表当中。

   }
}

与“视频播放器”的设计不同,这里我们简化了设计,只用到了AsyncTaskonProgressUpdate()方法和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 音乐列表展示

我们给音乐列表设计了如下的界面,

手把手教你做音乐播放器(三)获取音乐信息_第1张图片

我们已经知道了如何实现一个自定义的列表,

  1. 为每一条列表项定义它的外貌-布局文件;
  2. 自定义一个Adapter,将要展示的数据放入其中;
  3. 在主界面的布局文件中放入ListView控件,将Adapter设置到ListView中,让ListView展示所有数据;

3.3.1 音乐项的界面布局

每个数据项,要展示音乐的封面、名称、播放时长,

手把手教你做音乐播放器(三)获取音乐信息_第2张图片

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

手把手教你做音乐播放器(三)获取音乐信息_第3张图片
  1. ImageView用来放置歌曲封面,给它指定一个图片的大小,150dp x 100dp;背景颜色采用主题属性colorPrimary的颜色;

  2. 封面的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>

3.3.2 自定义Adapter

这里自定义MusicItemAdapter的方法,与自定义VideoItemAdapter的方法几乎一模一样,只是,

  1. Adapter保存的数据类型不同,从VideoItem变成了MusicItem

  2. 时间格式我们将显示成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;
    }

}

3.3.3 通过ListView使用Adapter

有了Adapter和数据,就要把它们结合起来,放到ListView中,以列表的形式展示出来。

  1. 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>
  2. 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);
    }
  3. 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目录下的音乐都被显示出来了。

手把手教你做音乐播放器(三)获取音乐信息_第4张图片

3.4 测试音乐播放

音乐列表准备好了,我们来试试播放音乐吧。
前面我们介绍了如何使用MediaPlayer来播放音乐,这里我们让用户点击音乐列表上的特定音乐后,就开始播放它,

  1. 为音乐列表的音乐项设置点击的监听,

    @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) {
            //添加播放音乐的代码
    
        }
    };
  2. 在点击响应处,添加播放音乐的代码,

    //定义监听器
    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编程中购买相关硬件。同时也感谢大家对我们这些码农的支持。

*最后再次感谢各位读者对安豆的支持,谢谢:)
/*******************************************************************/

你可能感兴趣的:(音乐播放器,手把手教你做安卓应用开发)