音乐播放器
https://github.com/JakeWharton/butterknife
界面图镇贴:
功能分析
播放
- 点击歌曲播放(recyclerView的Item的点击事件)
- 点击按钮播放(按钮的点击事件)
暂停
上一首
下一首
布局
页面主布局
采用约束布局。
recyclerView放在上部分,下部分作为当前正在播放歌曲的信息展示和切换、播放、暂停歌曲的按钮。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:background="@mipmap/bg2">
<ImageView
android:id="@+id/iv_splitline"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/violet"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="75dp"
/>
<ImageView
android:id="@+id/iv_current_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerVertical="true"
android:background="@mipmap/a1"
android:src="@mipmap/icon_song"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_current_last"
app:layout_constraintHorizontal_bias="0.036"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_splitline"
app:layout_constraintVertical_bias="0.448" />
<TextView
android:id="@+id/tv_current_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text=""
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/iv_current_icon"
app:layout_constraintTop_toBottomOf="@+id/iv_splitline"
app:layout_constraintVertical_bias="0.2" />
<TextView
android:id="@+id/tv_current_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text=""
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/iv_current_icon"
app:layout_constraintTop_toBottomOf="@+id/tv_current_title"
app:layout_constraintVertical_bias="0.277" />
<ImageView
android:id="@+id/iv_current_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@mipmap/icon_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_splitline"
android:layout_marginEnd="20dp"/>
<ImageView
android:id="@+id/iv_current_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:layout_toStartOf="@id/iv_current_next"
android:src="@mipmap/icon_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_current_next"
app:layout_constraintTop_toBottomOf="@+id/iv_splitline" />
<ImageView
android:id="@+id/iv_current_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:layout_toStartOf="@id/iv_current_play"
android:src="@mipmap/icon_last"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_current_play"
app:layout_constraintTop_toBottomOf="@+id/iv_splitline" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintBottom_toTopOf="@+id/iv_splitline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
recyclerView中的ite布局
采用 卡片式布局,把每一个item作为一张卡片。
卡片中主要展示:歌曲排序的序列号,歌曲名,歌手名,专辑名,时长。
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
app:contentPadding="10dp"
app:cardCornerRadius="10dp"
app:cardElevation="1dp"
app:cardBackgroundColor="@color/pink">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginStart="10dp"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@id/tv_number"
android:singleLine="true"
android:text=""
android:textSize="18sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/tv_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_title"
android:layout_alignStart="@id/tv_title"
android:layout_marginTop="5dp"
android:text=""
/>
<TextView
android:id="@+id/tv_line"
android:layout_width="2dp"
android:layout_height="18dp"
android:background="@color/textcolor"
android:layout_toEndOf="@id/tv_singer"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_alignTop="@id/tv_singer"
/>
<TextView
android:id="@+id/tv_album"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/tv_singer"
android:layout_toEndOf="@id/tv_line"
android:ellipsize="end"
android:singleLine="true"
android:text=""
android:textColor="@color/textcolor"
android:textSize="14sp"
/>
<TextView
android:id="@+id/tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_singer"
android:layout_alignParentEnd="true"
android:text=""
android:textSize="14sp"
android:textColor="@color/textcolor"
android:layout_centerVertical="true"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
数据载入
初始化布局
private void initView() {
lastIv = findViewById(R.id.iv_current_last);
playIv = findViewById(R.id.iv_current_play);
nextIv = findViewById(R.id.iv_current_next);
songTv = findViewById(R.id.tv_current_title);
singerTv = findViewById(R.id.tv_current_singer);
musicRv = findViewById(R.id.rv);
lastIv.setOnClickListener(this);
playIv.setOnClickListener(this);
nextIv.setOnClickListener(this);
}
编写音乐实体类
目的:存放之后查询到的音乐的信息。
LocalMusicBean musicBean;
public class LocalMusicBean {
private String id;
private String song;
private String singer;
private String album;
private String duration;
private String path;
public LocalMusicBean() {
}
public LocalMusicBean(String id, String song, String singer, String album, String duration, String path) {
this.id = id;
this.song = song;
this.singer = singer;
this.album = album;
this.duration = duration;
this.path = path;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSong() {
return song;
}
public void setSong(String song) {
this.song = song;
}
public String getSinger() {
return singer;
}
public void setSinger(String singer) {
this.singer = singer;
}
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getDuration() {
return duration;
}
public void setDuration(String duration) {
this.duration = duration;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
设置数据源
查找出手机中的音乐文件,把所有音乐文件存入数据源当中。
List<LocalMusicBean> mData;
String[] selectionArgs = new String[]{getString(R.string.music)};
String selection = MediaStore.Audio.Media.DATA + getString(R.string.like);
@SuppressLint("Recycle") Cursor cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, MediaStore.Audio.AudioColumns.IS_MUSIC);
int id = 0;
while (cursor.moveToNext()) {
String song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
String singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
String album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat(getString(R.string.timeformat));
String time = dateFormat.format(new Date(duration));
if (!time.equals(getString(R.string.timeiszeero))) {
id++;
String sid = String.valueOf(id);
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
musicBean = new LocalMusicBean(sid, song, singer, album, time, path);
mData.add(musicBean);
}
}
musicAdapter.notifyDataSetChanged();
设置适配器
把数据源中的数据加载到布局中显示出来。
private LocalMusicAdapter musicAdapter;
public class LocalMusicAdapter extends RecyclerView.Adapter<LocalMusicAdapter.localMusicViewHolder> {
Context context;
List<LocalMusicBean> mDatas;
onItemClickListener onItemClickListener;
public void setOnItemClickListener(LocalMusicAdapter.onItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface onItemClickListener{
void OnItemClick(View view,int position);
}
public LocalMusicAdapter(Context context, List<LocalMusicBean> mDatas) {
this.context = context;
this.mDatas = mDatas;
}
@NonNull
@Override
public localMusicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_local_music, parent, false);
return new localMusicViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull localMusicViewHolder holder, int position) {
LocalMusicBean musicBean = mDatas.get(position);
holder.idTv.setText(musicBean.getId());
holder.songTv.setText(musicBean.getSong());
holder.singerTv.setText(musicBean.getSinger());
holder.albumTv.setText(musicBean.getAlbum());
holder.timeTv.setText(musicBean.getDuration());
holder.itemView.setOnClickListener(view -> {
onItemClickListener.OnItemClick(view,position);
});
}
@Override
public int getItemCount() {
return mDatas.size();
}
static class localMusicViewHolder extends RecyclerView.ViewHolder {
TextView idTv,songTv,singerTv,albumTv,timeTv;
public localMusicViewHolder(@NonNull View itemView) {
super(itemView);
idTv = itemView.findViewById(R.id.tv_number);
songTv = itemView.findViewById(R.id.tv_title);
singerTv = itemView.findViewById(R.id.tv_singer);
albumTv = itemView.findViewById(R.id.tv_album);
timeTv = itemView.findViewById(R.id.tv_duration);
}
}
}
点击事件
点击歌曲播放对应歌曲,点击上一首按钮,播放上一首歌曲;点击下一首歌曲,播放下一首歌曲。
private int currentPosition = -1;
private int currentPositionInMusic = 0;
MediaPlayer mediaPlayer;
private void setEventListener() {
musicAdapter.setOnItemClickListener((view, position) -> {
currentPosition = position;
musicBean = mData.get(position);
playMusicInMusicBean(musicBean);
});
}
public void playMusicInMusicBean(LocalMusicBean musicBean) {
songTv.setText(musicBean.getSong());
singerTv.setText(musicBean.getSinger());
stopMusic();
mediaPlayer.reset();
try {
currentPositionInMusic = 0;
mediaPlayer.setDataSource(musicBean.getPath());
playMusic();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
private void playMusic(){
if (currentPositionInMusic == 0){
if (mediaPlayer != null && !mediaPlayer.isPlaying()){
try {
mediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mediaPlayer.start();
}
}else{
mediaPlayer.seekTo(currentPositionInMusic);
mediaPlayer.start();
}
playIv.setImageResource(R.mipmap.icon_pause);
}
private void pauseMusic() {
if (mediaPlayer != null && mediaPlayer.isPlaying()){
currentPositionInMusic = mediaPlayer.getCurrentPosition();
mediaPlayer.pause();
playIv.setImageResource(R.mipmap.icon_play);
}
}
private void stopMusic() {
if (mediaPlayer != null){
mediaPlayer.pause();
mediaPlayer.seekTo(0);
mediaPlayer.stop();
playIv.setImageResource(R.mipmap.icon_play);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@SuppressLint("NonConstantResourceId")
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.iv_current_last:
if (currentPosition == -1){
Toast.makeText(this,"请选择要播放的音乐",Toast.LENGTH_LONG).show();
return;
}
if (currentPosition == 0){
Toast.makeText(this,"已经是第一首了 ",Toast.LENGTH_LONG).show();
}
else {
currentPosition--;
currentPositionInMusic = 0;
LocalMusicBean lastMusicBean = mData.get(currentPosition);
playMusicInMusicBean(lastMusicBean);
}
break;
case R.id.iv_current_play:
if (currentPosition == -1){
Toast.makeText(this,"请选择要播放的音乐",Toast.LENGTH_LONG).show();
return;
}
if (mediaPlayer.isPlaying()){
pauseMusic();
}else {
playMusic();
}
break;
case R.id.iv_current_next:
if (currentPosition == -1){
Toast.makeText(this,"请选择要播放的音乐 ",Toast.LENGTH_LONG).show();
return;
}else if (currentPosition == mData.size()-1){
Toast.makeText(this,"已经是最后一首了",Toast.LENGTH_LONG).show();
}else {
currentPosition++;
currentPositionInMusic = 0;
LocalMusicBean nextMusicBean = mData.get(currentPosition);
playMusicInMusicBean(nextMusicBean);
}
break;
default:
break;
}
}
代码逻辑:先通过recyclerView的点击事件拿到歌曲在列表中的位置和对应歌曲实体,然后再根据歌曲实体拿到歌曲信息加载到布局中。最后根据所拿信息调用MediaPlayer的方法进行播放。
注意:暂停功能中用到了播放位置,在第一次播放时currentPositionInMusic对播放无影响,但是如果按了暂停,currentPositionInMusic发生改变,如果这时候点击事件不是继续播放,且没有对currentPositionInMusic进行重置,那么会进入继续播放逻辑的代码中,但是继续播放逻辑的代码没有对歌曲实体进行加载和准备,所以会报错。因此,在非继续播放事件发生前需要对currentPositionInMusic进行重置。
整个项目已经在gitee上开源,如有需要请自取。gitee地址:https://gitee.com/luozhaosong/new-music-app
|