音乐播放器
一、效果展示
◼ 两种状态:歌单、歌曲
二、布局设计
1.主页设计
◼ 主要分为三部分:切换界面的按钮部分、切换页面部分、播放器部分
◼activity_main.xml
2.实现页面切换的 Fragment
◼ 新建两个 Fragment:分别为 fgm_list
和 fgm_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
组件,显示歌曲专辑
<?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();
}
(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
ActionBar actionBar = getSupportActionBar();
actionBar.setTitle(" MUSIC");
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setLogo(R.drawable.song);
actionBar.setDisplayUseLogoEnabled(true);
2.修改主题颜色
<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);