> For the complete documentation index, see [llms.txt](https://xxgqin.gitbook.io/android/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xxgqin.gitbook.io/android/ch06/ch06-1.md).

# 第十一个项目——音乐播放器2

## 实验目的

* 掌握**MediaPlayer**进行音频及视频播放的方法；
* 掌握**MediaPlayer**操作状态图及各状态的含义；

## 实验要求

* 使用**MediaPlayer**控制音乐播放；
* 使用**BottomNavigationView**显示底部操作；
* 使用**ListView**的**onItemClick**实现音乐切换；

## 实验内容

在第十个**Android**项目的基础上，新增**BottomNavigationView**用于展示当前正在播放的音乐信息及控制播放状态，并通过**MediaPlayer**进行多媒体音乐的播放，项目最终运行效果如[图1. 音乐播放器2运行效果](/android/ch06/ch06-1.md#code11_screenshot)所示。

![图1. 音乐播放器2运行效果](http://www.funnycode.net/guet/img/ch06/code11_screenshot.png)

### 步骤一，打开GGMusic项目

打开第十个**Android**项目**GGMusic**，在此项目的基础上完成本次实验的内容。

**1. 修改activity\_main.xml布局**

首先修改**activity\_main.xml**布局，为其新增一个**BottomNavigationView**用于显示当前播放的音乐信息及控制音乐播放。 原布局中仅有一个**ListView**控件，由于加入一个**BottomNavigationView**控件后，需要调整**ListView**控件布局参数。使用**ConstraintLayout**将**ListView**置于其中，**ListView**的布局参数保持不变。设置**ListView**控件的父容器**ConstraintLayout**属性，主要设置其**app:layout\_constraintBottom\_toTopOf**属性&#x4E3A;*@id/navigation*。

**BottomNavigationView**控件则按照如下代码所示进行设置。

```markup
<?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**，用于提供对音乐播放控制的图标；

```markup
<?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()**&#x65B9;法将**bottom\_media\_toolbar**加载至**BottomNavigationView**中。此外由于**bottom\_media\_toolbar**中的几个控件需要在切换音乐时更新其相应的属性，因此在**MainActivity**中也对这些控件绑定了对应的控件类型对象，具体代码如下所示。

```java
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操作状态图](/android/ch06/ch06-1.md#mediaplayer-sd)所示。 对**MediaPlayer**对象进行操作时需严格遵守该状态图，确保**MediaPlayer**对象操作正确，否则**MediaPlayer**会抛出相应的异常，例如：

![图2. MediaPlayer操作状态图](http://www.funnycode.net/guet/img/ch06/mediaplayer_state_diagram.png)

* **启动MediaPlayer对象的方法调用顺序：**
  * 调用**MediaPlayer.create(Context context, int id)**&#x521B;建**MediaPlayer**对象；
  * 调用**start()**&#x65B9;法启动**MediaPlayer**对象；

    或
  * 实例化**MediaPlayer**对象；
  * 调用**setDataSource()**&#x8BBE;置**MediaPlayer**对象数据源；
  * 调用**prepareAsync()**&#x6216;**prepare()**&#x65B9;法进行**MediaPlayer**对象初始化准备工作；
  * 调用**start()**&#x65B9;法启动**MediaPlayer**对象；
* **停止MediaPlayer对象后再启动的方法调用顺序：**
  * 调用**stop()**&#x65B9;法停止**MediaPlayer**；
  * 调用**prepareAsync()**&#x6216;**prepare()**&#x65B9;法重新准备**MediaPlayer**对象；
  * 调用**start()**&#x65B9;法启动**MediaPlayer**对象；

**1. 在MainActivity活动管理MediaPlayer对象**

首先在**MainActivity**活动中定义**MediaPlayer**私有成员变量。由于**MediaPlayer**对象使用时耗费系统资源，所以 应该在活动的生命周期中合理的管理**MediaPlayer**对象资源。本项目中当活动被创建时在onStart方法中对**MediaPlayer** 进行初始化，在**onStop()**&#x65B9;法中则释放其对象，具体代码如下所示。

```java
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();
    }
}
```

{% hint style="info" %}
注： 上述做法，在App被放入后台运行或**MainActivity**活动发生跳转时，音乐播放均会被停止，我们在下一个项目将使用**Service**来解决这一问题。
{% endhint %}

**2. 添加ListView控件onItemClick()事件处理函数**

用户点击**ListView**显示的音乐列表项时实现音乐切换播放操作，这需要我们为**ListView**添加**onItemClick()**&#x4E8B;件处理函数。 在事件处理函数中，需要获得单击的音乐文件存储路径，并通过**MediaPlayer**对象播放该音乐，此外还需要将单击的音乐信息展示 在底部音乐显示控制栏(**bottom\_media\_toolbar**)上。

**A.** 首先添加**ListView**的**onItemClick()**&#x4E8B;件处理函数，在**MainActivity**中定义**ListView\.OnItemClickListener()**&#x4E8B;件侦听函数接口对象，并通过匿名类形式实现该接口。在该匿名类接口中，需要重写**onItemClick()**&#x65B9;法，该方法有4个参数，其含义表示：

* *AdapterView\<?> adapterView*，表示**ListView**对象；
* *View view*，在**ListView**中被点击的**Item**对应的布局控件对象；
* *int i*，被点击的布局控件对象*view*在**Adapter**中的序号；
* *long l*, 被点击的**Item**在**ListView**中的序号；

首先可以通过**mCursorAdapter**对象的**getCursor()**&#x65B9;法获得绑定的*Cursor*对象，通过**onItemClick()**&#x65B9;法的第三个参数*i*可知道 所需获取的多媒体音乐信息在*Cursor*对象中的位置，因此通过**cursor.moveToPosition(i)**&#x65B9;法，将游标*cursor*移动到对应的查询结果集记录上，即可通过**Cursor**类的**getColumnIndex()**、**getString()**&#x7B49;方式获取对应的多媒体音乐的信息。在这里，我们需要获取的多媒体音乐信息包括：

* *MediaStore.Audio.Media.TITLE*，多媒体音乐的歌曲名；
* *MediaStore.Audio.Media.ARTIST*，多媒体音乐的歌手名；
* *MediaStore.Audio.Media.ALBUM\_ID*，多媒体音乐所在的专辑ID，通过该ID可以查询专辑信息（主要是专辑的封面图）；
* *MediaStore.Aduio.Media.DATA*，多媒体音乐实际存储的路径；

```java
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()**&#x65B9;法。调用该方法前，**MediaPlayer**必须处于**Idle**状态，因此可以首先调用**reset()**&#x65B9;法，将**MediaPlayer**重置为**Idle**状态，具体代码如下所示。

```java
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()**&#x65B9;法中获取到，还需获取歌曲专辑图。专辑信息的获取需要通过*MediaStore.Audio.Albums.EXTERNAL\_CONTENT\_URI*加专辑ID(ALBUM\_ID)进行查询。

*albumUri*通过**ContentUri.withAppendedId()**&#x65B9;法从*MediaStore.Audio .Albums.EXTERNAL\_CONTENT\_URI*及*albumId*进行构造。 再通过**ContentResolver**进行查询得到*albumCursor*游标对象。专辑封面图则由*MediaStore.Audio .Albums.ALBUM\_ART*字段指明，项目中通过**Glide**图形加载库进行加载，关于**Glide**图形加载库的配置见下一步骤。

```java
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**下载相应版本的依赖库。

```java
...

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()**&#x65B9;法被调用，从而执行了**MediaPlayer**对象的释放操作。如何实现音乐的后台播放，这需要**Service**的支持，我们将在下一个项目中介绍此概念。

## 实验小结

通过本次实验，你应该掌握了如下知识内容：

* 使用**MediaPlayer**进行多媒体音乐播放；
* 掌握**MediaPlayer**状态图及相关操作函数；
* 使用**ContentResovler**查询媒体库音频信息、专辑信息；
