Android-简单音乐播放器设计

音乐播放器

项目地址:https://gitee.com/wang-junrong/my-music

一、效果展示

◼ 两种状态:歌单、歌曲

在这里插入图片描述


二、布局设计

1.主页设计

◼ 主要分为三部分:切换界面的按钮部分、切换页面部分、播放器部分

activity_main.xml

image-20230403204051654

2.实现页面切换的 Fragment

◼ 新建两个 Fragment:分别为 fgm_listfgm_song ,会自动生成两个类,以及配套的xml布局文件

fragment_fgm_list :在里面放 ListView 组件,显示歌单

在这里插入图片描述

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".fgm_list">

    <ListView
        android:id="@+id/fl_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

fragment_fgm_song:在里面放一个 ImageView 组件和 TextView 组件,显示歌曲专辑

image-20230403205950480

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fgm_song">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="397dp"
        android:layout_height="369dp"
        android:layout_gravity="center|top"
        android:scaleX="0.7"
        android:scaleY="0.7" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="355dp"
        android:layout_height="91dp"
        android:layout_gravity="bottom|center"
        android:text="暂无歌曲"
        android:textColor="@color/black"
        android:textSize="23dp" />

</FrameLayout>

三、后端实现

1.MainActivity

◼ 主要实现:页面的切换、播放器及相关按钮(播放/暂停/停止/上一首/下一首/播放模式)、实时显示进度条、播放时间

(1)页面切换

在这里插入图片描述

◼ Fragment编程
// FragmentManager类用于管理 Fragment。
FragmentManager fm = getSupportFragmentManager(); 
// FragmentTransaction类用于开始对 Fragment 进行操作,比如添加、替换、移除等。
FragmentTransaction ft = fm.beginTransaction(); 
// 创建自定义的Fragment子类对象
fgm_list fl = new fgm_list();
fgm_song fs = new fgm_song();
//切换页面
ft.replace(R.id.main_container, fl);   //先替换
ft.commit();  //再提交
a.给顶部两个TextView添加事件change

在这里插入图片描述

b.处理点击事件——实现页面切换
private FragmentManager fm;
private FragmentTransaction ft;
private fgm_list fl; //歌单
private fgm_song fs; //歌曲专辑
private int page;  //当前页面
//与onCreate同级
//切换页面
public void change(View v) {
    
    
    fm = getSupportFragmentManager();
    ft = fm.beginTransaction();
    fl = new fgm_list();
    fs = new fgm_song();
    Bundle bundle = new Bundle(); //Bundle类用于activity向fragment传输数据
    int position = -1; //存放当前歌曲在列表的位置,初始值为-1
    
    if(player.isPlaying()){
    
     //判断是否在播放,更新当前播放歌曲位置
        position=player.getCurrentMediaItemIndex();
    }
    bundle.putInt("position", position);
    fs.setArguments(bundle); //传给fgm_song的对象fs

    switch (v.getId()) {
    
    
        case R.id.tv1: //歌单TextView的id
            if(page ! = 1){
    
    
                page=1;
                ft.replace(R.id.main_container, fl);  //更换成歌单页面
                tv_list.setAlpha(1.0f);  //将歌单TextView的字体的透明度设为1.0f
                tv_song.setAlpha(0.4f);  //将歌曲TextView的字体的透明度设为0.4f
            }
            break;
        case R.id.tv2: //歌曲TextView的id
            if (page ! = 2) {
    
    
                page=2;
                ft.replace(R.id.main_container, fs); //切换成歌曲页面
                tv_list.setAlpha(0.4f);
                tv_song.setAlpha(1.0f);
            }
            break;

    }
    ft.commit();
}

image-20230404101919952

(2)播放器及相关按钮

在这里插入图片描述

◼ ExoPlayer编程
// 创建播放器
ExoPlayer player = new ExoPlayer.Builder(MainActivity.this).build();
// 添加媒体
player.addMediaItem(mediaItem);
// 设置播放模式
player.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL); //列表循环
// 就绪并开始播放
player.prepare();
player.play();
// 从播放器列表指定媒体播放
player.seekTo(position, 0); //position:指定媒体位置,0:指定媒体资源从0ms开始播放
a.初始化播放器
ExoPlayer player; //播放器
List<String> music_list = new ArrayList<>(); //歌曲名ArrayList集合
private boolean prepared; //播放器是否准备好
private int countMusic; //歌曲总数
private int mode; //播放模式,0为循环,1为单曲循环
//初始化播放器
public void initExoPlayer() {
    
    
    mode = 0;
    prepared = false;
    player = new ExoPlayer.Builder(MainActivity.this).build();
    player.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL); //列表循环

    //将全部歌曲加入到player
    for (String song : music_list) {
    
    
        Uri uri = Uri.parse("asset:///music/" + song); //获取歌曲的uri
        MediaItem mediaItem = MediaItem.fromUri(uri); //通过uri获取媒体资源
        player.addMediaItem(mediaItem); //添加到播放器中
    }
}
b.设置播放歌曲
TextView current_music; //显示当前播放的音乐名
private Timer timer; //定时器
//设置播放歌曲,接收一个参数(歌曲在列表中的位置)
public void setExoPlayer(int position) {
    
    
    //检查一下是否有歌曲正在播放,如果有则先停止
    if (player.isPlaying()) {
    
    
        player.stop();
    }
    
    //设置实现当前播放歌曲,通过split去掉.mp3后缀
    current_music.setText("正在播放 — " + music_list.get(position).split("\\.")[0]);

    player.prepare(); //会触发下面的监听器
    player.play();
    player.seekTo(position, 0);
    timer = new Timer();
    timer.schedule(new ProgressUpdate(), 0, 1000); //启动定时器去更新界面
    btn_play.setImageResource(R.drawable.pause); //将play按钮变为pause
}
c.播放器的监听器
SeekBar seekBar; //进度条
TextView tv_total; //歌曲总时长
//播放器的监听器
Player.Listener listener2 = new Player.Listener() {
    
    
    @Override
    //播放器状态监听器
    public void onPlaybackStateChanged(int playbackState) {
    
    
        if (playbackState = = ExoPlayer.STATE_READY) {
    
     //播放器准备好了
            prepared = true;
            long realDurationMillis = player.getDuration(); //获取媒体文件的时长(毫秒)
            seekBar.setMax((int) realDurationMillis); // 设置SeekBar最大值
            tv_total.setText(format(realDurationMillis)); //设置音乐总时长
        }
    }
};
// 在初始化init函数中添加
player.addListener(listener2); //添加监听器
d.相关按钮的监听器
//监听器
View.OnClickListener listener4 = new View.OnClickListener() {
    
    
    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()) {
    
    
            case R.id.ib_play: //播放按钮
                clickPlay();
                break;
            case R.id.ib_pre: //上一首
                clickPre(); 
                break;
            case R.id.ib_next: //下一首
                clickNext();
                break;
            case R.id.ib_repeat: //循环模式
                clickMode();
                break;
            case R.id.ib_stop: //停止按钮
                clickStop();
        }
    }
};

//播放按钮
public void clickPlay() {
    
    
    if (!prepared) //播放器没有准备好
        return;
    if (player.isPlaying()) {
    
     //处于播放状态
        player.pause();
        btn_play.setImageResource(R.drawable.play); //暂停后,修改成播放的图标
        timer.cancel(); //停止定时器
        timer = new Timer(); //新建定时器
    } else {
    
       //处于暂停状态
        player.play();
        btn_play.setImageResource(R.drawable.pause); //播放后,修改成暂停的图标
        timer = new Timer();
        timer.schedule(new ProgressUpdate(), 0, 1000); //启动定时器
    }
}

//上一首
public void clickPre() {
    
    
    String song;
    int index = player.getCurrentMediaItemIndex();
    if (index = = 0) {
    
     //已经是第一首
        index = countMusic - 1;
    } else {
    
    
        index--;
    }
    tv_list.setAlpha(0.4f);
    tv_song.setAlpha(1.0f);
    setExoPlayer(index); //播放位置为index的歌曲
}

//下一首
public void clickNext() {
    
    
    int index = player.getCurrentMediaItemIndex();
    if (index = = countMusic - 1) {
    
     //已经是最后一首
        index = 0;
    } else {
    
    
        index++;
    }
    tv_list.setAlpha(0.4f);
    tv_song.setAlpha(1.0f);
    setExoPlayer(index);
}

//循环模式
public void clickMode() {
    
    
    if (mode = = 0) {
    
     //模式为列表循环
        mode = 1;
        btn_repeat.setImageResource(R.drawable.repeat_once); //修改图标
        player.setRepeatMode(ExoPlayer.REPEAT_MODE_ONE); //修改成单曲循环
    } else {
    
      //模式为单曲循环
        mode = 0;
        btn_repeat.setImageResource(R.drawable.repeat);
        player.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL); //修改成列表循环
    }
}

//停止按钮
public void clickStop() {
    
    
    prepared = false;
    player.stop();
    
    //将播放器的相关组件都设置成停止状态
    player.seekTo(0);
    seekBar.setProgress(0);
    tv_total.setText(format(0));
    tv_progress.setText(format(0));
    current_music.setText("欢迎使用 听我想听");
    btn_play.setImageResource(R.drawable.play);

    //先将歌曲位置信息-1传给歌曲页面,再切换到列表页面
    fm = getSupportFragmentManager();
    ft = fm.beginTransaction();
    fs = new fgm_song();
    fl =new fgm_list();
    Bundle bundle = new Bundle();
    bundle.putInt("position", -1);
    fs.setArguments(bundle);
    page=1;
    tv_list.setAlpha(1.0f);
    tv_song.setAlpha(0.4f);
    ft.replace(R.id.main_container, fs); //将-1传给fs
    ft.replace(R.id.main_container, fl); //再切换到列表页面
    ft.commit();
}

在这里插入图片描述

(3)实时显示进度条

在这里插入图片描述

a.更新界面的定时器
SeekBar seekBar; //进度条
TextView tv_total; //歌曲总时长
TextView tv_progress; //歌曲时长提示
TextView current_music; //当前播放的音乐
private int oldPosition = -1;
// 自定义定时器类
private class ProgressUpdate extends TimerTask {
    
    
    @Override
    public void run() {
    
    
        runOnUiThread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //根据播放器播放位置去更新进度掉
                long position = player.getContentPosition();
                long total = player.getDuration();
                seekBar.setProgress((int) position);
                tv_progress.setText(format(position));
                
                if (prepared) {
    
    
                    //更新当前播放歌曲总时间和名字
                    tv_total.setText(format(player.getDuration()));
                    current_music.setText("正在播放 — " 
                                          + music_list.get(player.getCurrentMediaItemIndex()).split("\\.")[0]);
                    //判断当前歌曲是否播放完,切换到下一首的歌曲页面
                    if (player.getCurrentMediaItemIndex() ! = oldPosition) {
    
    
                        oldPosition = player.getCurrentMediaItemIndex();
                        fm = getSupportFragmentManager();
                        ft = fm.beginTransaction();
                        fs = new fgm_song();
                        Bundle bundle = new Bundle();
                        bundle.putInt("position",player.getCurrentMediaItemIndex());
                        fs.setArguments(bundle);
                        page=2;
                        ft.replace(R.id.main_container, fs);
                        ft.commit();
                    }

                }
            }
        });
    }
}

◼ 启动定时器

timer = new Timer();
timer.schedule(new ProgressUpdate(), 0, 1000);
b.SeekBar的监听器
SeekBar.OnSeekBarChangeListener listener3 = new SeekBar.OnSeekBarChangeListener() {
    
    
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    
    
        if (prepared && fromUser) {
    
    
            player.seekTo(progress);
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    
    
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
    
    
        player.play();
        if(player.isPlaying()){
    
    
            btn_play.setImageResource(R.drawable.pause);
        }
        tv_progress.setText(format(seekBar.getProgress()));
    }
};
//SeekBar添加监听器
seekBar.setOnSeekBarChangeListener(listener3);

2.Fragment

(1)fgm_list

◼ 主要实现:歌单列表填充、监听选择的歌曲、与MainActivity通信、返回选择歌曲的位置

在这里插入图片描述

a.歌单列表填充
ListView listView;
Adapter adapter;
List<String> music_list = new ArrayList<>();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    
    
    // Inflate the layout for this fragment
    View view=inflater.inflate(R.layout.fragment_fgm_list, container, false);

    listView = view.findViewById(R.id.fl_list);
    music_list = getMusic();
    adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, music_list);
    listView.setAdapter((ListAdapter) adapter); //需强转成ListAdapter
    listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); //选择模式
    listView.setOnItemClickListener(listener1); //添加监听器

    return view;
}

public List<String> getMusic() {
    
    
    List<String> music_List = new ArrayList<>();
    try {
    
    
        String[] fNames = getContext().getAssets().list("music"); //获取assets/pic目录下所有文件名
        for (String fn : fNames) {
    
    
            music_List.add(fn.split("\\.")[0]); //去掉.mp3
        }
    } catch (IOException e) {
    
    
        throw new RuntimeException(e);
    }
    return music_List;
}
b.ListView监听器
AdapterView.OnItemClickListener listener1 = new AdapterView.OnItemClickListener() {
    
    
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
    
        passData(String.valueOf(position)); //将数据传给Activity
    }
};
c.与MainActivity通信

◼ 首先在 fgm_list 定义一个接口 OnDataPass

//创建接口
interface OnDataPass {
    
    
    void onDataPass(int data);
}

◼ 接着创建一个 OnDataPass 类型的成员变量,并在需要传递数据的地方调用 onDataPass 方法

private OnDataPass dataPasser;

@Override
public void onAttach(Context context) {
    
    
    super.onAttach(context);
    dataPasser = (OnDataPass) context; //将上下文强转成OnDataPass对象
}

//封装一个传输数据的函数,在ListView监听器中调用
private void passData(int data) {
    
    
    dataPasser.onDataPass(data);
}

◼ 最后在 MainActivity 实现接口 OnDataPass

//实现接口
public class MainActivity extends AppCompatActivity implements OnDataPass {
    
    
    @Override
    public void onDataPass(int data) {
    
    
        // 处理传递过来的数据
        setExoPlayer(data);
    }
}
(2)fgm_song

◼ 主要实现:专辑封面的效果、与MainActivity通信

在这里插入图片描述

a.将专辑封面变成圆形
private ImageView imageView;
private int position;
List<String> pic_list = new ArrayList<>(); //存放专辑封面文件名的集合
public void circleImage(int position) throws IOException {
    
    
    // 根据位置信息加载图片并将其设置为ImageView的背景
    InputStream is = getContext().getAssets().open("pic/" + pic_list.get(position));
    
    // 创建BitmapShader对象并创建一个圆形位图
    Bitmap bitmap = BitmapFactory.decodeStream(is);
    BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    Bitmap circleBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(circleBitmap);
    Paint paint = new Paint();
    paint.setShader(shader);
    paint.setAntiAlias(true);
    float radius = Math.min(bitmap.getWidth(), bitmap.getHeight()) / 2f;
    canvas.drawCircle(bitmap.getWidth() / 2f, bitmap.getHeight() / 2f, radius, paint);

    // 将圆形的中心点设置为ImageView的中心
    imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);

    // 将Paint对象设置为不透明并在ImageView上绘制圆形
    paint.setAlpha(255);
    imageView.setImageBitmap(circleBitmap);
}
b.旋转专辑封面
public void spinImage(){
    
    
    // 创建一个旋转动画并将其应用于ImageView
    ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "rotation", 0, 360); //rotation:旋转角度
    animator.setDuration(8000); //动画的时长为 8000 毫秒
    animator.setRepeatCount(ObjectAnimator.INFINITE); //动画的重复次数设置为无限次
    animator.setInterpolator(new LinearInterpolator()); //设置动画的插值器为线性插值器,即匀速
    animator.start(); //启动动画效果
}
c.与Activity通信

◼ MainActivity 发送数据

Bundle bundle = new Bundle();
int position = -1;
if(player.isPlaying()){
    
    
    position=player.getCurrentMediaItemIndex();
}
bundle.putInt("position", position);
fs.setArguments(bundle);
ft.replace(R.id.main_container, fs);
ft.commit();

◼ fg_song 接收数据

Bundle bundle = getArguments();
if (bundle ! = null) {
    
    
    position = bundle.getInt("position");
}

四、优化样式

1.修改ActionBar

image-20230405152742357

ActionBar actionBar = getSupportActionBar();
actionBar.setTitle(" MUSIC");
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setLogo(R.drawable.song);
actionBar.setDisplayUseLogoEnabled(true);

2.修改主题颜色

image-20230405152311809

<item name="colorPrimary">#5E2196F3</item>
<item name="colorPrimaryVariant">#252196F3</item>
<item name="colorOnPrimary">@color/black</item>

3.修改应用配置

在这里插入图片描述

android:icon="@drawable/listen"
android:label="Player"

五、总结

1.通过Fragment实现页面切换

◼ 在本次播放器的设计过程中,我学到一些关于 Fragment 的简单编程,通过 Fragment 编程实现页面的切换。

主要思路:

(1)在主布局中嵌套一个空白的布局并给定id值

(2)创建多个 Fragment 页面,并给每个页面添加所需的组件

(3)通过 Fragment 编程实现切换

FragmentManager fm = getSupportFragmentManager(); 
FragmentTransaction ft = fm.beginTransaction(); 
fgm_list fl = new fgm_list();
ft.replace(R.id.main_container, fl);
ft.commit();

2.通过自定义接口实现数据传递

◼ 将 fragment 的数据传给 Activty 是这次大作业中遇到的比较大的难题,通过解决该问题,我学到了通过接口来实现数据的传递。

主要思路:

(1)在需要传数据的 Fragment 中声明一个接口

interface OnDataPass {
     
     
    void onDataPass(int data);
}

(2)在 Fragment 类中创建一个接口类型的成员变量,并在需要传递数据的地方调用接口的方法。

private OnDataPass dataPasser;

@Override
public void onAttach(Context context) {
     
     
    super.onAttach(context);
    dataPasser = (OnDataPass) context;
}

private void passData(int data) {
     
     
    dataPasser.onDataPass(data);
}

(3)通过 Activity 实现 Fragment 声明的接口来接收数据

public class MainActivity extends AppCompatActivity implements OnDataPass {
     
     
    @Override
    public void onDataPass(int data) {
     
     
        // 处理传递过来的数据
    }
}

3.通过Bundle实现数据传递

◼ 通过本次作业,我学会了如何使用 Bundle 来实现 Activity 向 Fragment 传递数据

主要思路:

(1)在 Activity 中将数据以键值对的方式放入 Bundle 对象中

Bundle bundle = new Bundle();
bundle.putInt("position", position);

(2)给声明的 Fragment 子类对象设置参数后再进行页面切换

fs.setArguments(bundle);
ft.replace(R.id.main_container, fs);
ft.commit();

(3)在 Fragment 子类中接收数据

Bundle bundle = getArguments();
if (bundle ! = null) {
     
     
    //处理接收的数据
}

4.ExoPlayer播放器的相关方法

◼ 在本次设计过程中,我接触到了一些课上没有介绍的 ExoPlayer 的方法

(1)addMediaItem():往播放器中添加媒体

addMediaItem()和setMediaItem()的区别:

  • addMediaItem()方法可以添加多个媒体项,而setMediaItem()方法只能设置一个媒体项

  • addMediaItem()方法返回一个MediaMetadata.Builder对象,可以通过该对象继续添加其他的媒体项,而setMediaItem()方法返回的是MediaMetadata对象本身。

  • 如果使用setMediaItem()方法设置媒体项,那么它将覆盖之前设置的所有媒体项,而addMediaItem()方法则不会覆盖已有的媒体项,而是将新的媒体项添加到列表的末尾

(2)getCurrentMediaItemIndex():返回当前媒体项目索引

(3)seekTo():将媒体播放位置调整到指定时间

seekTo()是一个重载函数:

void seekTo(long positionMs); //只可以指定播放位置
void seekTo(int mediaItemIndex, long positionMs); //可以指定媒体索引和播放位置

5.读取assets目录下文件

◼ 在本次设计过程中,我学会了读取 assets 目录下文件的两种方法

方法一:

InputStream is = getAssets().open("filename"); //打开指定的文件(输入流方式)
Bitmap bitmap = BitmapFactory.decodeStream(is); // 将输入流转为Bitmap类型

方法二:

Uri uri = Uri.parse("asset:///music/" + "song");
MediaItem mediaItem = MediaItem.fromUri(uri);

猜你喜欢

转载自blog.csdn.net/wjr1229/article/details/131663116