第十一个项目——音乐播放器2
实验目的
掌握MediaPlayer进行音频及视频播放的方法;
掌握MediaPlayer操作状态图及各状态的含义;
实验要求
使用MediaPlayer控制音乐播放;
使用BottomNavigationView显示底部操作;
使用ListView的onItemClick实现音乐切换;
实验内容
在第十个Android项目的基础上,新增BottomNavigationView用于展示当前正在播放的音乐信息及控制播放状态,并通过MediaPlayer进行多媒体音乐的播放,项目最终运行效果如图1. 音乐播放器2运行效果所示。

步骤一,打开GGMusic项目
打开第十个Android项目GGMusic,在此项目的基础上完成本次实验的内容。
1. 修改activity_main.xml布局
首先修改activity_main.xml布局,为其新增一个BottomNavigationView用于显示当前播放的音乐信息及控制音乐播放。 原布局中仅有一个ListView控件,由于加入一个BottomNavigationView控件后,需要调整ListView控件布局参数。使用ConstraintLayout将ListView置于其中,ListView的布局参数保持不变。设置ListView控件的父容器ConstraintLayout属性,主要设置其app:layout_constraintBottom_toTopOf属性为@id/navigation。
BottomNavigationView控件则按照如下代码所示进行设置。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<android.support.constraint.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ListView
... />
</android.support.constraint.ConstraintLayout>
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
</android.support.constraint.ConstraintLayout>
2. 定义音乐显示控制栏的布局bottom_media_toolbar.xml
在activity_main.xml中使用BottomNavigationView主要是借助于该控件在不同设备像素密度下适配底部导航高度的作用。 而实际的布局则通过一个名为bottom_media_toolbar.xml的文件进行定义。
在bottom_media_toolbar.xml文件中定义了音乐显示控制栏的布局,主要包含以下几个控件:
ProgressBar,@+id/progress,用于显示当前音乐播放的进度;
ImageView,@+id/iv_thumbnail,用于显示当前音乐所属的专辑封面图;
TextView,@+id/tv_bottom_title,用于显示当前音乐的歌曲名;
TextView,@+id/tv_bottom_artist,用于显示当前音乐的歌手名;
ImageView,@+id/iv_play,用于提供对音乐播放控制的图标;
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ProgressBar
android:id="@+id/progress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:progress="28"
android:progressBackgroundTint="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@id/tv_bottom_title"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/iv_thumbnail"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/tv_bottom_title"
android:text="Title"
android:textSize="14sp"
android:textColor="@color/colorPrimary"
android:layout_marginTop="4dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="56dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_thumbnail"
/>
<TextView
android:id="@+id/tv_bottom_artist"
android:text="Artist name"
android:textSize="12sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
app:layout_constraintTop_toBottomOf="@id/tv_bottom_title"
app:layout_constraintStart_toStartOf="@id/tv_bottom_title"/>
<ImageView
android:id="@+id/iv_play"
android:src="@drawable/ic_pause_circle_outline_black_24dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:clickable="true"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</android.support.constraint.ConstraintLayout>
3. 在BottomNavigationView中加载bottom_media_toolbar布局
通过在MainActivity的onCreate方法中首先绑定BottomNavigationView控件,然后通过LayoutInflater类的inflate()方法将bottom_media_toolbar加载至BottomNavigationView中。此外由于bottom_media_toolbar中的几个控件需要在切换音乐时更新其相应的属性,因此在MainActivity中也对这些控件绑定了对应的控件类型对象,具体代码如下所示。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
...
private BottomNavigationView navigation;
private TextView tvBottomTitle;
private TextView tvBottomArtist;
private ImageView ivAlbumThumbnail;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
navigation = findViewById(R.id.navigation);
LayoutInflater.from(MainActivity.this)
.inflate(R.layout.bottom_media_toolbar,
navigation,
true);
ivPlay = navigation.findViewById(R.id.iv_play);
tvBottomTitle = navigation.findViewById(R.id.tv_bottom_title);
tvBottomArtist = navigation.findViewById(R.id.tv_bottom_artist);
ivAlbumThumbnail = navigation.findViewById(R.id.iv_thumbnail);
if (ivPlay != null) {
ivPlay.setOnClickListener(MainActivity.this);
}
navigation.setVisibility(View.GONE);
...
}
步骤二,使用MediaPlayer进行多媒体音乐播放
使用MediaPlayer进行多媒体音乐播放前,需了解MediaPlayer操作状态图,如图2. MediaPlayer操作状态图所示。 对MediaPlayer对象进行操作时需严格遵守该状态图,确保MediaPlayer对象操作正确,否则MediaPlayer会抛出相应的异常,例如:

启动MediaPlayer对象的方法调用顺序:
调用MediaPlayer.create(Context context, int id)创建MediaPlayer对象;
调用start()方法启动MediaPlayer对象;
或
实例化MediaPlayer对象;
调用setDataSource()设置MediaPlayer对象数据源;
调用prepareAsync()或prepare()方法进行MediaPlayer对象初始化准备工作;
调用start()方法启动MediaPlayer对象;
停止MediaPlayer对象后再启动的方法调用顺序:
调用stop()方法停止MediaPlayer;
调用prepareAsync()或prepare()方法重新准备MediaPlayer对象;
调用start()方法启动MediaPlayer对象;
1. 在MainActivity活动管理MediaPlayer对象
首先在MainActivity活动中定义MediaPlayer私有成员变量。由于MediaPlayer对象使用时耗费系统资源,所以 应该在活动的生命周期中合理的管理MediaPlayer对象资源。本项目中当活动被创建时在onStart方法中对MediaPlayer 进行初始化,在onStop()方法中则释放其对象,具体代码如下所示。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
...
private MediaPlayer mMediaPlayer = null;
...
@Override
protected void onStart() {
super.onStart();
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
}
}
@Override
protected void onStop() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
Log.d(TAG, "onStop invoked!");
}
super.onStop();
}
}
2. 添加ListView控件onItemClick()事件处理函数
用户点击ListView显示的音乐列表项时实现音乐切换播放操作,这需要我们为ListView添加onItemClick()事件处理函数。 在事件处理函数中,需要获得单击的音乐文件存储路径,并通过MediaPlayer对象播放该音乐,此外还需要将单击的音乐信息展示 在底部音乐显示控制栏(bottom_media_toolbar)上。
A. 首先添加ListView的onItemClick()事件处理函数,在MainActivity中定义ListView.OnItemClickListener()事件侦听函数接口对象,并通过匿名类形式实现该接口。在该匿名类接口中,需要重写onItemClick()方法,该方法有4个参数,其含义表示:
AdapterView<?> adapterView,表示ListView对象;
View view,在ListView中被点击的Item对应的布局控件对象;
int i,被点击的布局控件对象view在Adapter中的序号;
long l, 被点击的Item在ListView中的序号;
首先可以通过mCursorAdapter对象的getCursor()方法获得绑定的Cursor对象,通过onItemClick()方法的第三个参数i可知道 所需获取的多媒体音乐信息在Cursor对象中的位置,因此通过cursor.moveToPosition(i)方法,将游标cursor移动到对应的查询结果集记录上,即可通过Cursor类的getColumnIndex()、getString()等方式获取对应的多媒体音乐的信息。在这里,我们需要获取的多媒体音乐信息包括:
MediaStore.Audio.Media.TITLE,多媒体音乐的歌曲名;
MediaStore.Audio.Media.ARTIST,多媒体音乐的歌手名;
MediaStore.Audio.Media.ALBUM_ID,多媒体音乐所在的专辑ID,通过该ID可以查询专辑信息(主要是专辑的封面图);
MediaStore.Aduio.Media.DATA,多媒体音乐实际存储的路径;
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
...
private ListView.OnItemClickListener itemClickListener
= new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int i, long l) {
Cursor cursor = mCursorAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(i)) {
int titleIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.TITLE);
int artistIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.ARTIST);
int albumIdIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.ALBUM_ID);
int dataIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.DATA);
String title = cursor.getString(titleIndex);
String artist = cursor.getString(artistIndex);
Long albumId = cursor.getLong(albumIdIndex);
String data = cursor.getString(dataIndex);
...
}
}
};
2. 获取多媒体音乐文件存储路径并使用MediaPlayer播放
通过获取到的MediaStore.Audio.Media.DATA字段构造对应的Uri,使用MediaPlayer进行播放。
进行多媒体音乐播放前,需要判断MediaPlayer对象是否为空,在此基础上进行MediaPlayer状态控制。 无论当前MediaPlayer处于何种状态(Prepared、Started、Paused、Stopped),要实现切换音乐均需要调用 setDataSource()方法。调用该方法前,MediaPlayer必须处于Idle状态,因此可以首先调用reset()方法,将MediaPlayer重置为Idle状态,具体代码如下所示。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
...
private ListView.OnItemClickListener itemClickListener
= new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int i, long l) {
Cursor cursor = mCursorAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(i)) {
...
int dataIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.DATA);
...
String data = cursor.getString(dataIndex);
Uri dataUri = Uri.parse(data);
if (mMediaPlayer != null) {
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(
MainActivity.this, dataUri);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
};
3. 更新音乐播放控制栏信息
更新音乐播放控制栏信息,需要将歌曲名,歌手名,歌曲专辑图等信息设置到bottom_media_toolbar对应的控件上。 其中歌曲名,歌手名已经在onItemClick()方法中获取到,还需获取歌曲专辑图。专辑信息的获取需要通过MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI加专辑ID(ALBUM_ID)进行查询。
albumUri通过ContentUri.withAppendedId()方法从MediaStore.Audio .Albums.EXTERNAL_CONTENT_URI及albumId进行构造。 再通过ContentResolver进行查询得到albumCursor游标对象。专辑封面图则由MediaStore.Audio .Albums.ALBUM_ART字段指明,项目中通过Glide图形加载库进行加载,关于Glide图形加载库的配置见下一步骤。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, ... {
...
private ListView.OnItemClickListener itemClickListener
= new ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int i, long l) {
Cursor cursor = mCursorAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(i)) {
....
int albumIdIndex = cursor.getColumnIndex(
MediaStore.Audio.Media.ALBUM_ID);
Long albumId = cursor.getLong(albumIdIndex);
....
navigation.setVisibility(View.VISIBLE);
if (tvBottomTitle != null) {
tvBottomTitle.setText(title);
}
if (tvBottomArtist != null) {
tvBottomArtist.setText(artist);
}
Uri albumUri = ContentUris.withAppendedId(
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
albumId);
Cursor albumCursor = mContentResolver.query(
albumUri,
null,
null,
null,
null);
if (albumCursor != null && albumCursor.getCount() > 0) {
alumbCursor.moveToFirst();
int albumArtIndex = albumCursor.getColumnIndex(
MediaStore.Audio.Albums.ALBUM_ART);
String albumArt = albumCursor.getString(
albumArtIndex);
Glide.with(MainActivity.this)
.load(albumArt)
.into(ivAlbumThumbnail);
albumCursor.close();
}
}
}
};
4. 配置Glide图形加载库
Glide图形加载库支持拉取,解码和展示视频快照,图片,和GIF动画,更详细的介绍可参考Glide官网:http://bumptech.github.io/glide。
使用Glide图形加载库对Android SDK的要求:
Min Sdk Version,大于 API Level 14;
Compile Sdk Version,使用 API Level 27或更高版本进行编译;
Support Library Version,Glide使用的支持库版本为27。
打开app模块的build.gradle文件,添加如下代码所示的依赖信息,点击编辑窗口中的sync按钮,确保Android Studio根据修改后的build.gradle下载相应版本的依赖库。
...
allprojects {
repositories {
jcenter()
maven() { url "https://maven.google.com"}
}
}
dependencies {
...
implementation 'com.android.support:appcompat-v7:27.1.0'
implementation 'com.android.support:design:27.1.0'
implementation 'com.github.bumptech.glide:glide:4.6.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'
...
}
步骤三,编译GGMusic
完成上述步骤后,编译GGMusic项目,并发布至模拟器或真机上运行,查看App的运行结果。现在你点击ListView中的某一多媒体音乐项时,MediaPlayer将自动播放该多媒体音乐,并在音乐控制工具栏中刷新当前播放的音乐信息。
如果你将App切换至后台运行,你会发现音乐播放被停止。这是因为App切换至后台时,MainActivity活动的onStop()方法被调用,从而执行了MediaPlayer对象的释放操作。如何实现音乐的后台播放,这需要Service的支持,我们将在下一个项目中介绍此概念。
实验小结
通过本次实验,你应该掌握了如下知识内容:
使用MediaPlayer进行多媒体音乐播放;
掌握MediaPlayer状态图及相关操作函数;
使用ContentResovler查询媒体库音频信息、专辑信息;
Last updated
Was this helpful?