实验目的
掌握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 控件则按照如下代码所示进行设置。
Copy <?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 ,用于提供对音乐播放控制的图标;
Copy <?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 中也对这些控件绑定了对应的控件类型对象,具体代码如下所示。
Copy 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 对象;
或
调用setDataSource() 设置MediaPlayer 对象数据源;
调用prepareAsync() 或prepare() 方法进行MediaPlayer 对象初始化准备工作;
调用start() 方法启动MediaPlayer 对象;
停止MediaPlayer对象后再启动的方法调用顺序:
调用prepareAsync() 或prepare() 方法重新准备MediaPlayer 对象;
调用start() 方法启动MediaPlayer 对象;
1. 在MainActivity活动管理MediaPlayer对象
首先在MainActivity 活动中定义MediaPlayer 私有成员变量。由于MediaPlayer 对象使用时耗费系统资源,所以 应该在活动的生命周期中合理的管理MediaPlayer 对象资源。本项目中当活动被创建时在onStart方法中对MediaPlayer 进行初始化,在onStop() 方法中则释放其对象,具体代码如下所示。
Copy 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();
}
}
注: 上述做法,在App被放入后台运行或MainActivity 活动发生跳转时,音乐播放均会被停止,我们在下一个项目将使用Service 来解决这一问题。
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 ,多媒体音乐实际存储的路径;
Copy 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 状态,具体代码如下所示。
Copy 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 图形加载库的配置见下一步骤。
Copy 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 下载相应版本的依赖库。
Copy ...
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 的支持,我们将在下一个项目中介绍此概念。
实验小结
通过本次实验,你应该掌握了如下知识内容:
使用ContentResovler 查询媒体库音频信息、专辑信息;