MP3播放器(安卓应用开发)

如果我们有着自己的MP3音乐库,在Android设备中使用商用媒体播放器进行播放,对于应用的权限和广告等问题,相信感觉不会太好。

好消息是,开发一个自己的安卓版MP3播放器并不是一项太难的工作。

准备工作和开发约定

我们使用的是开发环境是Android Studio,可以在https://developer.android.google.cn/studio下载最新的版本。

创建一个新的项目,并指定类型为空白的Android应用项目,如下图。

创建新Android项目

本文中,将使用Java开发语言和Android 9平台,项目名称定义为AudioPlayer,项目包的名称使用com.caohuayu.audioplayer,其中,com.caohuayu是作者域名的反向定义,如果大家有自己的域名,也可以使用相应的反向域名。新建项目参数设置如下图,项目保存位置(Save location)可以根据自己电脑的实际情况决定。

项目参数

由于应用的功能比较简单,我们只需要在AndroidManifest.xml配置文件中声明所需的存储读写权限,如下面的代码。

XML
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

实际上,本文中应用功能只需要读取文件的权限,即读取文件并实现基本的播放和循环功能。如果扩展功能以便保存一些数据时就需要使用写入权限,比如保存播放清单、当前播放的曲目,以及播放曲目的进度等。

布局设计

本应用的功能只限于播放设备中指定位置存放的全部MP3文件,可以执行播放/停止操作,以及全部循环、单曲循环和随机播放功能。

应用的功能比较简单,只需要一个界面,我们使用默认的Activity,默认的布局文件为activity_main.xml,定义内容如下。

XML
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="50dp" >

        <Button android:id="@+id/btnSingle"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="@string/single"
            android:textAllCaps="false"
            android:layout_width="wrap_content"></Button>
        <Button android:id="@+id/btnRandom"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="@string/random"
            android:textAllCaps="false"
            android:layout_width="wrap_content"></Button>
        <Button android:id="@+id/btnRepeat"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="@string/repeat"
            android:textAllCaps="false"
            android:layout_width="wrap_content"></Button>
        <Button android:id="@+id/btnPlay"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="@string/play"
            android:textAllCaps="false"
            android:layout_width="wrap_content"></Button>
    </LinearLayout>

    <TextView android:id="@+id/txtMsg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAlignment="center"></TextView>


    <ListView android:id="@+id/lst1"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:listSelector="#CCCCCC"></ListView>

</LinearLayout>

在MainActivity.java代码文件中加载此布局,默认的界面如下图。

英文版界面

在中文版的真实的安卓设备中显示如下图。

中文版界面

四个按钮(Button)分别是:

  • Single(单曲),单曲循环按钮。
  • Random(随机),随机播放按钮。
  • Repeat(循环),全部循环按钮。
  • Play(播放/停止),播放与停止按钮。

TextView控件的ID为txtMsg,用于显示必要的提示信息。

ListView控件的ID为lst1,用于显示播放列表。

功能实现

主界面的功能实现代码位于MainActivity.java文件,首先来看一些初始代码。

Java
package com.caohuayu.audioplayer;

import androidx.appcompat.app.AppCompatActivity;

import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.util.ArrayList;
import java.util.Random;

public class MainActivity extends AppCompatActivity
implements View.OnClickListener, AdapterView.OnItemClickListener,
        MediaPlayer.OnCompletionListener{

    // 控件对象
    private Button btnSingle;
    private Button btnRandom;
    private Button btnRepeat;
    private Button btnPlay;
    private ListView lst1;
    private TextView txtMsg;
    // 完整的文件名数组
    private String[] filenames;
    // 播放列表显示名称,只显示文件基本名称
    private String[] shownames;
    // 播放组件
    private MediaPlayer player = new MediaPlayer();
    // 当前播放索引值
    private int currentIndex=-1;
    // 循环方法,1单曲,2随机,3全部循环
    private int repeatState=3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 对象初始化
        btnSingle=(Button)findViewById(R.id.btnSingle);
        btnRandom=(Button)findViewById(R.id.btnRandom);
        btnRepeat=(Button)findViewById(R.id.btnRepeat);
        btnPlay=(Button)findViewById(R.id.btnPlay);
        lst1=(ListView)findViewById(R.id.lst1);
        txtMsg=(TextView)findViewById(R.id.txtMsg);
        txtMsg.setText(R.string.stop);
        // 设置事件侦听
        btnSingle.setOnClickListener(this);
        btnRandom.setOnClickListener(this);
        btnRepeat.setOnClickListener(this);
        btnPlay.setOnClickListener(this);
        lst1.setOnItemClickListener(this);
        player.setOnCompletionListener(this);
        // 循环状态按钮颜色
        setButtonColor(btnSingle,false);
        setButtonColor(btnRandom,false);
        setButtonColor(btnRepeat,true);
        // 载入播放列表
        loadPlayList();
    }

    @Override
    protected void onDestroy(){
        player.stop();
        player.release();
        player=null;
        filenames=null;
        shownames=null;
        super.onDestroy();
    }
    // 其它代码…
}

代码中,MainActivity类实现了View.OnClickListener、AdapterView.OnItemClickListener和MediaPlayer.OnCompletionListener事件,分别用于响应按钮的单击操作、列表项的单击操作,以及媒体播放后的响应操作。

MainActivity类中也定义了一些必要的对象和变量,并重写了Activity的onCreate()和onDestroy()方法,分别用于对象的初始化和释放。

在onCreate()方法中,除了一些基本的初始化操作,还调用了一些自定义方法,如:

  • setButtonColor()方法,设置按钮的颜色,包括两种状态。第一个参数指定按钮对象,第二个参数设置为true时,设置为当前操作按钮颜色,设置为false时设置为非当前操作按钮操作。
  • loadPlayList()方法,用于从指定的位置读取并载入MP3播放列表。

首先来看setButtonColor()方法的实现,如下面的代码。

Java
// 设置按钮状态
private void setButtonColor(Button btn,boolean isCur){
    if(isCur){
        btn.setBackgroundColor(getColor(R.color.purple_500));
        btn.setTextColor(getColor(R.color.white));
    }else{
        btn.setBackgroundColor(getColor(R.color.white));
        btn.setTextColor(getColor(R.color.black));
    }
}

接下来是loadPlayList()方法的实现,如下面的代码。

Java
// 载入列表
private void loadPlayList(){
    File dir = new File(getExternalFilesDir(null),"");
    //
    File[] files = dir.listFiles();
    if(files==null){
        Toast.makeText(MainActivity.this,
          "请在Android\\data\\com.caohuayu.audioplayer\\files文件夹准备.mp3文件",
             Toast.LENGTH_LONG).show();
       return;
    }
    ArrayList<File>fileList = new ArrayList<File>();
    for(File f:files){
        if(f.getName().toLowerCase().endsWith(".mp3"))
            fileList.add(f);
    }
    //生成播放列表和显示列表
    int size = fileList.size();
    if(size>0){
        filenames=new String[size];
        shownames=new String[size];
        for(int i=0;i<size;i++){
            filenames[i] = fileList.get(i).getPath();
            String s = fileList.get(i).getName();
            shownames[i] = s.substring(0,s.length()-4);
        }
        // 绑定列表
        ArrayAdapter<String> ada=new ArrayAdapter<String>(
                MainActivity.this,android.R.layout.simple_list_item_1,shownames);
        lst1.setAdapter(ada);
        // 准备播放器
        try {
            currentIndex = 0;
            //
            player.setDataSource(filenames[currentIndex]);
            player.prepare();
            Toast.makeText(MainActivity.this,
"音乐已准备",Toast.LENGTH_LONG).show();
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }else{
        shownames=null;
        player.release();;
        Toast.makeText(MainActivity.this,
          "请在Android\\data\\com.caohuayu.audioplayer\\files文件夹准备.mp3文件",
            Toast.LENGTH_LONG).show();
    }
}

loadPlayList()方法中,我们约定MP3文件保存在安卓设备的Android/data/com.caohuayu.audioplayer/files文件夹中,测试时,需要大家手工将自己喜爱的MP3复制到此文件夹。请注意,在Android/data文件夹中应找到应用的包名,这里是com.caohuayu.audioplayer,如果在创建应用时使用了不同的包名称,应在这里找到相应的文件夹。

在读取文件夹中的MP3文件后,会将完整的文件路径保存到filenames数组中,而基础的文件名(不包括扩展名)会保存在shownames数组中,并显示在播放列表中。

MainActivity类中的另外一个辅助方法是play(),用于开始播放列表中指定索引位置的文件,方法的实现如下。

Java
// 播放指定索引
private void play(int index) {
    try{
        currentIndex = index;
        player.reset();
        player.setDataSource(filenames[currentIndex]);
        player.prepare();
        player.start();
        // 选中播放项
        btnPlay.setText(R.string.stop);
        txtMsg.setText("Playing : "+shownames[currentIndex]);
    }catch(Exception ex){
        ex.printStackTrace();
    }
}

当调用loadPlayList()方法载入播放列表成功后,就可以点击播放按钮开始播放,四个按钮的响应代码使用onClick()方法实现,如下面的代码。

Java
// 按钮操作响应
@Override
public void onClick(View v) {
    int vid = v.getId();
    if(vid==R.id.btnPlay){
        if(filenames==null || filenames.length==0){
            loadPlayList();
        }else{
            if(player.isPlaying()){
                player.pause();
                btnPlay.setText(R.string.play);
                txtMsg.setText(R.string.stop);
            }else{
                player.start();
                btnPlay.setText(R.string.stop);
                txtMsg.setText("Playing : "+shownames[currentIndex]);
            }
        }
    }else if(vid==R.id.btnSingle){
        repeatState = 1;
        setButtonColor(btnSingle,true);
        setButtonColor(btnRandom,false);
        setButtonColor(btnRepeat,false);
    }else if(vid==R.id.btnRandom){
        repeatState = 2;
        setButtonColor(btnSingle,false);
        setButtonColor(btnRandom,true);
        setButtonColor(btnRepeat,false);
     }else if(vid==R.id.btnRepeat){
        repeatState = 3;
        setButtonColor(btnSingle,false);
        setButtonColor(btnRandom,false);
        setButtonColor(btnRepeat,true);
     }
}

当一首曲目播放完成后,会调用onCompletion()方法,如下面的代码。

Java
// 播放器状态
@Override
public void onCompletion(MediaPlayer mp){
    if(repeatState==1){
        // 单曲
        play(currentIndex);
    }else if(repeatState==2){
        // 随机
        play(new Random().nextInt(shownames.length));
    }else{
        // 循环
        int index = currentIndex+1;
        if(index>=shownames.length) index=0;
        play(index);
    }
}

此时,会根据repeatState变量判断循环模式,并开始下一首MP3的播放。

最后是点击列表项响应操作,如下面的代码。

Java
// 列表项点击响应
@Override
public void onItemClick(AdapterView<?> parent,
                        View view, int position, long id) {
    play((int)id);
}

相关资源

应用中使用的颜色资源定义在/res/values/colors.xml文件,如下面的代码。

XML
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
</resources>

相关的文本定义在/res/values/strings.xml,如下面的代码。

XML
<resources>
    <string name="app_name">AudioPlayer</string>
    <string name="single">Single</string>
    <string name="random">Random</string>
    <string name="repeat">Repeat</string>
    <string name="play">Play</string>
    <string name="stop">Stop</string>
</resources>

相应的中文内容定义在/res/values-zh/strings.xml文件,可以在Projects查看模式下创建,文件内容定义如下。

XML
<resources>
    <string name="app_name">AudioPlayer</string>
    <string name="single">单曲</string>
    <string name="random">随机</string>
    <string name="repeat">循环</string>
    <string name="play">播放</string>
    <string name="stop">停止</string>
</resources>

其它

附件中包含了可安装的.apk文件,请注意安装后将mp3文件复制到设备的Android/data/com.caohuayu.audioplayer/files目录中。

扩展资源中包含完整的源代码!

附件