作为音乐APP的主要功能,我放到了第三篇文章,因为播放音乐的功能并没有看上去那么简单,里面有很多细节是在写代码的时候就要考虑,并且加入到逻辑里面的,这可不是危言耸听,下面来看是怎样一个不简单吧。
既然要做播放音乐的功能自然要好好的设计一下UI了,不然太难看我可拿不出手,于是我参考了QQ和网易的列表播放页面,合二为一就产生了下面这个页面
从这张图可以看出什么呢?首先播放布局不随页面滚动,一直固定在屏幕的底部,其次是播放的进度是左边的这个logo中,而这个logo在播放的时候自动旋转,logo右边的是歌曲信息,当内容超过控件时会有跑马灯效果,最右边自然就是控制歌曲的播放和暂停了。你可能会问上一曲、下一曲呢?这个嘛,我打算放在下一篇文章再来说明,步步为营,循序渐进。
首先要修改这个布局先达到图中的效果。
下面我附上现在的布局代码。如果你之前是跟着来写的话,那么这里你可以对照着布局,再增加上去,不过你可能会漏掉一些细节,所以建议你先看懂布局,然后复制粘贴,万无一失。activity_local_music.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/app_bg"
android:orientation="vertical"
tools:context=".ui.MainActivity">
<!--Toolbar-->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/app_color"
app:navigationIcon="@mipmap/icon_return_white">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="本地音乐"
android:textColor="@color/white"
android:textSize="@dimen/sp_18" />
</androidx.appcompat.widget.Toolbar>
<!--主体页面-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--扫描布局和歌曲列表-->
<RelativeLayout
android:id="@+id/lay_body"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/lay_bottom">
<!--扫描音乐布局-->
<LinearLayout
android:id="@+id/lay_scan_music"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dp_140"
android:layout_height="@dimen/dp_140"
android:src="@mipmap/icon_empty" />
<!--扫描本地音乐-->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_scan_local_music"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="@dimen/dp_140"
android:layout_height="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_16"
android:insetTop="@dimen/dp_0"
android:insetBottom="@dimen/dp_0"
android:onClick="onClick"
android:text="扫描本地音乐"
android:textSize="@dimen/sp_14"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
app:backgroundTint="@color/transparent"
app:cornerRadius="@dimen/dp_20"
app:strokeColor="@color/white"
app:strokeWidth="@dimen/dp_1" />
</LinearLayout>
<!--列表-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_music"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<!--定位当前播放音乐按钮-->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_location_play_music"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="@dimen/dp_28"
android:layout_height="@dimen/dp_28"
android:layout_above="@+id/lay_bottom"
android:layout_alignParentRight="true"
android:layout_margin="@dimen/dp_36"
android:insetLeft="@dimen/dp_0"
android:insetTop="@dimen/dp_0"
android:insetRight="@dimen/dp_0"
android:insetBottom="@dimen/dp_0"
android:onClick="onClick"
android:textSize="@dimen/sp_14"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
android:visibility="gone"
app:backgroundTint="@color/white"
app:cornerRadius="@dimen/dp_20"
app:icon="@drawable/music_location"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconTint="@color/black" />
<!--底部播放控制布局-->
<LinearLayout
android:id="@+id/lay_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@color/bottom_bg_color"
android:gravity="center_vertical"
android:paddingLeft="@dimen/dp_8"
android:paddingTop="@dimen/dp_8"
android:paddingRight="@dimen/dp_16"
android:paddingBottom="@dimen/dp_8">
<!-- logo和播放进度 使用相对布局达成覆盖的效果-->
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--logo-->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_logo"
android:layout_width="@dimen/dp_48"
android:layout_height="@dimen/dp_48"
android:padding="1dp"
android:src="@mipmap/icon_music"
app:shapeAppearanceOverlay="@style/circleImageStyle"
app:strokeColor="@color/white"
app:strokeWidth="@dimen/dp_2" />
<!--播放进度 自定义View-->
<com.llw.goodmusic.view.MusicRoundProgressView
android:id="@+id/music_progress"
android:layout_width="@dimen/dp_48"
android:layout_height="@dimen/dp_48"
app:radius="20dp"
app:strokeColor="@color/black"
app:strokeWidth="2dp" />
</RelativeLayout>
<!--歌曲信息 歌名 - 歌手 -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv_song_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:paddingLeft="@dimen/dp_12"
android:paddingRight="@dimen/dp_12"
android:singleLine="true"
android:text="Good Music"
android:textColor="@color/white"
android:textSize="@dimen/sp_16" />
<!--歌曲控制按钮-->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_play"
android:layout_width="@dimen/dp_36"
android:layout_height="@dimen/dp_36"
android:insetLeft="@dimen/dp_0"
android:insetTop="@dimen/dp_0"
android:insetRight="@dimen/dp_0"
android:insetBottom="@dimen/dp_0"
android:onClick="onClick"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
app:backgroundTint="@color/transparent"
app:cornerRadius="@dimen/dp_18"
app:icon="@mipmap/icon_play"
app:iconGravity="textStart"
app:iconPadding="@dimen/dp_0"
app:iconSize="@dimen/dp_36" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</layout>
里面用到三个图标一个自定义View。图标你可以去我的源码里面拿,源码图标,自定义View我会写出来。在com.llw.goodmusic下新建一个view包,然后新建一个MusicRoundProgressView。
代码如下:
package com.llw.goodmusic.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.llw.goodmusic.R;
/**
* 圆形进度条
*
* @author llw
*/
public class MusicRoundProgressView extends View {
/**
* 画笔
*/
private Paint mPaint;
/**
* 画笔颜色
*/
private int mPaintColor;
/**
* 半径
*/
private float mRadius;
/**
* 圆环半径
*/
private float mRingRadius;
/**
* 圆环宽度
*/
private float mStrokeWidth;
/**
* 圆心 X 轴坐标
*/
private int mCenterX;
/**
* 圆心 Y 轴坐标
*/
private int mCenterY;
/**
* 总进度
*/
private int mTotalProgress;
/**
* 当前进度
*/
private int mProgress;
public MusicRoundProgressView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressView);
//半径
mRadius = typedArray.getDimension(R.styleable.RoundProgressView_radius, 40);
//宽度
mStrokeWidth = typedArray.getDimension(R.styleable.RoundProgressView_strokeWidth, 5);
//颜色
mPaintColor = typedArray.getColor(R.styleable.RoundProgressView_strokeColor, 0xFFFFFFFF);
//圆环半径 = 半径 + 圆环宽度的1/2
mRingRadius = mRadius + mStrokeWidth / 2;
//画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mPaintColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
}
@Override
protected void onDraw(Canvas canvas) {
mCenterX = getWidth() / 2;
mCenterY = getHeight() / 2;
if (mProgress > 0) {
RectF rectF = new RectF();
rectF.left = (mCenterX - mRingRadius);
rectF.top = (mCenterY - mRingRadius);
rectF.right = mRingRadius * 2 + (mCenterX - mRingRadius);
rectF.bottom = mRingRadius * 2 + (mCenterY - mRingRadius);
canvas.drawArc(rectF, -90, ((float) mProgress / mTotalProgress) * 360, false, mPaint);
}
}
/**
* 设置进度
*/
public void setProgress(int progress, int totalProgress) {
mProgress = progress;
mTotalProgress = totalProgress;
//重绘
postInvalidate();
}
}
里面涉及到的样式如下:
在styles.xml中增加如下代码:
<!--圆形进度条-->
<declare-styleable name="RoundProgressView">
<attr name="radius" format="dimension"/>
<attr name="strokeWidth" format="dimension"/>
<attr name="strokeColor" format="color"/>
</declare-styleable>
现在你的自定义从理论上来说就不会报错了。当然可能需要改一下包名之类,下面就是回到LocalMusicActivty。
首先在当前定位按钮后面加上这些变量
/**
* 底部logo图标,点击之后弹出当前播放歌曲详情页
*/
private ShapeableImageView ivLogo;
/**
* 底部当前播放歌名
*/
private MaterialTextView tvSongName;
/**
* 底部当前歌曲控制按钮, 播放和暂停
*/
private MaterialButton btnPlay;
/**
* 音频播放器
*/
private MediaPlayer mediaPlayer;
/**
* 记录当前播放歌曲的位置
*/
public int mCurrentPosition = -1;
/**
* 自定义进度条
*/
private MusicRoundProgressView musicProgress;
/**
* 音乐进度间隔时间
*/
private static final int INTERNAL_TIME = 1000;
/**
* 图片动画
*/
private ObjectAnimator logoAnimation;
每一个都有注释,然后绑定相关的控件
同样点击事件必不可少
常规的操作是通过点击音乐列表中的某一首歌之后播放歌曲。还记得列表的点击事件在哪里吗?当然是在**showLocalMusicData()**方法里面,之前在这个方法中设置适配器和列表的一些相关属性和数据,当然还有点击事件。 回顾一下这个代码:
//item的点击事件
mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
if (view.getId() == R.id.item_music) {
if (oldPosition == -1) {
//未点击过 第一次点击
oldPosition = position;
mList.get(position).setCheck(true);
} else {
//大于 1次
if (oldPosition != position) {
mList.get(oldPosition).setCheck(false);
mList.get(position).setCheck(true);
//重新设置位置,当下一次点击时position又会和oldPosition不一样
oldPosition = position;
}
}
mAdapter.changeState();
}
}
});
我不想这里面的代码太多,所以我新写了一个方法。
/**
* 控制播放位置
*
* @param position
*/
private void playPositionControl(int position) {
if (oldPosition == -1) {
//未点击过 第一次点击
oldPosition = position;
mList.get(position).setCheck(true);
} else {
//大于 1次
if (oldPosition != position) {
mList.get(oldPosition).setCheck(false);
mList.get(position).setCheck(true);
//重新设置位置,当下一次点击时position又会和oldPosition不一样
oldPosition = position;
}
}
mAdapter.changeState();
}
当点击这个item时将position传递给全局变量mCurrentPosition。
然后通过changeSong(mCurrentPosition);方法来播放歌曲
/**
* 切换歌曲
*/
private void changeSong(int position) {
if (mediaPlayer == null) {
mediaPlayer = new MediaPlayer();
}
try {
//切歌前先重置,释放掉之前的资源
mediaPlayer.reset();
BLog.i(TAG, mList.get(position).path);
//设置播放音频的资源路径
mediaPlayer.setDataSource(mList.get(position).path);
//设置播放的歌名和歌手
tvSongName.setText(mList.get(position).song + " - " + mList.get(position).singer);
//如果内容超过控件,则启用跑马灯效果
tvSongName.setSelected(true);
//开始播放前的准备工作,加载多媒体资源,获取相关信息
mediaPlayer.prepare();
//开始播放音频
mediaPlayer.start();
//播放按钮控制
if (mediaPlayer.isPlaying()) {
btnPlay.setIcon(getDrawable(R.mipmap.icon_pause));
btnPlay.setIconTint(getColorStateList(R.color.gold_color));
} else {
btnPlay.setIcon(getDrawable(R.mipmap.icon_play));
btnPlay.setIconTint(getColorStateList(R.color.white));
}
} catch (IOException e) {
e.printStackTrace();
}
}
相信代码都能够看懂,播放歌曲之前先实例化,然后重置mediaPlayer,设置相关的信息之后就开始播放,这个时候也要处理一下按钮的状态。那么现在你再列表中就可以随意点击了,点击那一首就播放哪一首。现在的确是有播放音乐了,但是我也需要暂停啊。
在底部播放按钮btn_play的点击事件中进行处理。
case R.id.btn_play:
//控制音乐 播放和暂停
if (mediaPlayer == null) {
//没有播放过音乐 ,点击之后播放第一首
oldPosition = 0;
mCurrentPosition = 0;
mList.get(mCurrentPosition).setCheck(true);
mAdapter.changeState();
changeSong(mCurrentPosition);
} else {
//播放过音乐 暂停或者播放
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
btnPlay.setIcon(getDrawable(R.mipmap.icon_play));
btnPlay.setIconTint(getColorStateList(R.color.white));
} else {
mediaPlayer.start();
btnPlay.setIcon(getDrawable(R.mipmap.icon_pause));
btnPlay.setIconTint(getColorStateList(R.color.gold_color));
}
}
break;
也要考虑到用户一进入这个页面直接点击底部播放按钮的因素,这样就直接播放列表中的第一首,至于记录当前歌曲的位置和播放进度,下一次进入时继续这个进度,这个功能放到后面来实现,先考虑这个页面的。
说道自动下一曲,就是没有人为干涉的情况下,当前歌曲播放完毕之后自行播放下一首。这里需要实现MediaPlayer的OnCompletionListener,r方法如下:
/**
* 播放完成之后自动下一曲
*
* @param mp
*/
@Override
public void onCompletion(MediaPlayer mp) {
int position = -1;
if (mList != null) {
if (mCurrentPosition == mList.size() - 1) {
//当前为最后一首歌时,则切换到列表的第一首歌
position = mCurrentPosition = 0;
} else {
position = ++mCurrentPosition;
}
}
//移动播放位置
playPositionControl(position);
//切歌
changeSong(position);
}
在这个方法里面通过监听到歌曲完毕之后,先判断是否在最后一首歌,是则从第一首歌开始,不是则位置+1,然后移动播放的位置,更新列表数据,之后就通过刚才得到的位置进行切歌。当然这个功能要完成还需要最后一个不走。记得加一个监听才行,如下所示,可以在对MediaPlayer进行实例化的时候设置完成播放时的监听。不加,则你的音乐播放完了就一直在哪里不动。
播放进度对于用户来说是比较重要的,这里我没有用Seekbar,来让用户看到播放进度并且可以手动拖动,而是用了一个自定义View,只用来显示歌曲当前的播放进度,没有具体的播放时间和操作控件,这样做会让你的页面显得很简洁,同时静中有动,说了这么多,不如写代码来的实际。
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
// 展示给进度条和当前时间
int progress = mediaPlayer.getCurrentPosition();
musicProgress.setProgress(progress, mediaPlayer.getDuration());
//更新进度
updateProgress();
return true;
}
});
首先通过Handle来发送消息,定时更新进度。
/**
* 更新进度
*/
private void updateProgress() {
// 使用Handler每间隔1s发送一次空消息,通知进度条更新
// 获取一个现成的消息
Message msg = Message.obtain();
// 使用MediaPlayer获取当前播放时间除以总时间的进度
int progress = mediaPlayer.getCurrentPosition();
msg.arg1 = progress;
mHandler.sendMessageDelayed(msg, INTERNAL_TIME);
}
发送消息
在changeSong方法中,当开始播放时,设置当前的进度和音乐的总进度,然后通过**updateProgress()**方法来发送消息。在handler中更新进行自定义View的重新位置,这样就可以看到进度增长了。因为不管你是点击列表得item还是点击底部的播放按钮,都会进入changeSong方法中,所以我放在这个里面。
在静中增加动,可以给用户更好的体验,所以我想到了图片的自转。通过属性动画来实现。
/**
* 初始化动画
*/
private void initAnimation() {
logoAnimation = ObjectAnimator.ofFloat(ivLogo, "rotation", 0.0f, 360.0f);
logoAnimation.setDuration(3000);
logoAnimation.setInterpolator(new LinearInterpolator());
logoAnimation.setRepeatCount(-1);
logoAnimation.setRepeatMode(ObjectAnimator.RESTART);
}
在这个方法里面进行了动画的参数配置,我来解释一下上面的配置做了什么事情,首先是顺时针方向旋转360°。然后旋转一圈耗时3s,使用线性插值器,重复旋转。下面就是用的地方了。
在歌曲播放的时候,开始旋转,可以暂停和继续。同时在底部的播放按钮里面也需要做相应的动画控制。
最后在播放完成监听方法里面重置这个动画
好了,功能就写完了,下面直接运行吧。
看这个图片是不是有那么点意思了呢?
写代码的工程中逻辑很重要,最好是一气呵成,当你的思路被打断,无法集中注意力时,是写不好代码的,而文章则是在代码写好之后再写的,如果有什么问题及时提出来,我会尽快解决。 源码地址:Good Music