本篇主要实现播放界面包括,歌曲暂停、播放下一首、上一首、播放模式、歌词展示,实现根据歌曲背景深浅实现状态栏文字、标题文字黑白切换适配。
音乐播放封装使用《开源库MusicPlayManager - 封装StarrySky音乐库》
简易音乐app仅作为学习用,禁止用于商业及非法用途,如产生法律纠纷与本人无关
新增activity_current_song_play.xml
<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">
<data>
<variable
name="vm"
type="com.tobery.personalmusic.ui.song.SongPlayViewModel" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.song.CurrentSongPlayActivity">
<ImageView
android:id="@+id/img_bc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/bg_default_song"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="52dp" />
<View
android:id="@+id/view_title_bg"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_45"
app:layout_constraintTop_toTopOf="parent"
/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/view_title_bg"
app:layout_constraintBottom_toBottomOf="@id/view_title_bg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textColor="@color/white"
/>
<View
android:id="@+id/view_body"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_0"
app:layout_constraintTop_toBottomOf="@id/view_title_bg"
app:layout_constraintBottom_toTopOf="@id/view_bottom"
/>
<ImageView
android:id="@+id/iv_music_cover"
android:layout_width="@dimen/dp_200"
android:layout_height="@dimen/dp_200"
imSrc="@{vm.currentSongUrl}"
error="@{@drawable/shape_music_record}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/view_title_bg"
app:layout_constraintBottom_toBottomOf="@id/view_bottom"
/>
<com.tobery.personalmusic.widget.LyricView
android:id="@+id/lrc"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_0"
app:current_color="#ffffff"
app:default_label="暂无歌词,我也不知道怎么上传OvO"
app:lrc_padding="@dimen/dp_10"
app:normal_color="#808080"
app:text_divider="@dimen/dp_15"
app:text_gravity="center"
app:text_size="@dimen/sp_18"
app:time_color="#c5c3c2"
app:time_text_size="@dimen/sp_13"
app:timeline_color="#4d4948"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/view_title_bg"
app:layout_constraintBottom_toTopOf="@id/view_bottom"
/>
<View
android:id="@+id/view_bottom"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_90"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/tv_past_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/view_bottom"
android:layout_marginStart="@dimen/dp_8"
android:textSize="@dimen/sp_12" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="@dimen/dp_0"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:max="100"
android:maxHeight="@dimen/dp_3"
android:minHeight="@dimen/dp_3"
android:progressDrawable="@drawable/seekbar_bg"
android:thumb="@drawable/seekbar_thumb"
app:layout_constraintTop_toTopOf="@id/view_bottom"
app:layout_constraintStart_toEndOf="@id/tv_past_time"
app:layout_constraintEnd_toStartOf="@id/tv_total_time"
android:layout_marginStart="@dimen/dp_5"
/>
<TextView
android:id="@+id/tv_total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="@id/view_bottom"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="@dimen/dp_8"
android:textSize="@dimen/sp_12" />
<ImageView
android:id="@+id/iv_play_mode"
android:layout_width="@dimen/dp_45"
android:layout_height="@dimen/dp_45"
android:padding="@dimen/dp_15"
android:layout_marginEnd="@dimen/dp_10"
android:scaleType="centerCrop"
android:src="@drawable/ic_play_list_loop_white"
app:layout_constraintTop_toTopOf="@id/seek_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_pre"
/>
<ImageView
android:id="@+id/iv_pre"
android:layout_width="@dimen/dp_45"
android:layout_height="@dimen/dp_45"
android:padding="@dimen/dp_15"
android:scaleType="centerCrop"
android:src="@drawable/shape_pre"
android:onClick="@{() -> vm.previous()}"
app:layout_constraintTop_toTopOf="@id/seek_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_play"
/>
<ImageView
android:id="@+id/iv_play"
android:layout_width="@dimen/dp_50"
android:layout_height="@dimen/dp_50"
android:padding="@dimen/dp_5"
android:src="@drawable/shape_play_white"
app:layout_constraintTop_toTopOf="@id/seek_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:onClick="@{() -> vm.musicPlayOrPause()}"
/>
<ImageView
android:id="@+id/iv_next"
android:layout_width="@dimen/dp_45"
android:layout_height="@dimen/dp_45"
android:padding="@dimen/dp_15"
android:scaleType="centerCrop"
android:src="@drawable/shape_next"
android:onClick="@{() -> vm.next()}"
app:layout_constraintStart_toEndOf="@id/iv_play"
app:layout_constraintTop_toTopOf="@id/seek_bar"
app:layout_constraintBottom_toBottomOf="parent"
/>
<ImageView
android:id="@+id/iv_list"
android:layout_width="@dimen/dp_45"
android:layout_height="@dimen/dp_45"
android:layout_marginStart="@dimen/dp_10"
android:padding="@dimen/dp_10"
android:scaleType="centerCrop"
android:src="@drawable/ic_song_play_list"
app:layout_constraintStart_toEndOf="@id/iv_next"
app:layout_constraintTop_toTopOf="@id/seek_bar"
app:layout_constraintBottom_toBottomOf="parent"
/>
androidx.constraintlayout.widget.ConstraintLayout>
layout>
新增bg_default_song.xml
:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<gradient
android:endColor="#221B1A"
android:gradientRadius="120%p"
android:startColor="#4b3832"
android:type="radial" />
shape>
LyricView
public class LyricView extends View {
private static final String TAG = "LyricView";
//播放图片的宽高
private static final float PLAY_DRAWABLE_WIDTH = ContextProvider.get().getContext().getResources().getDimension(R.dimen.dp_30);
//时间 也就是 [XX:XX] 的宽度
private static final float TIME_TEXT_WIDTH = ContextProvider.get().getContext().getResources().getDimension(R.dimen.dp_40);
//时间线从一行 滑到另一行的时间
private static final int ANIMATION_DURATION = 1000;
//时间线从当前行 的某处 移到中间的时间
private static final int ADJUST_DURATION = 100;
private List<LrcEntry> mLrcEntryList = new ArrayList<>();
//歌词画笔
private TextPaint mLrcPaint = new TextPaint();
//时间线 画笔
private TextPaint mTimePaint = new TextPaint();
//时间线 画笔属性
private Paint.FontMetrics mTimeFontMetrics;
//播放按钮
private Drawable mPlayDrawable;
//普通字体的颜色
private int mNormalTextColor;
//字体的大小(普通和选中的都一样大
private float mTextSize;
//选中行 字体颜色
private int mCurrentTextColor;
//时间线 所在的一行歌词 已经时间的颜色
private int mTimelineTextColor;
//时间线的颜色
private int mTimelineColor;
//padding值
private float mPadding;
//没有加载完歌词的 默认显示text
private String mDefaultLabel;
//滑动的动画
private ValueAnimator mAnimator;
//手势监听器
private GestureDetector mGestureDetector;
//播放监听器
private OnPlayClickListener mListener;
//滑动类
private Scroller mScroller;
//是否正在滑动/正在点击/显示时间线
private boolean isFling, isTouching, isShowTimeline;
//歌词位置
private int mTextGravity;
//手指在 屏幕上滑动的 偏移量
private float mOffset;
//每句歌词之间的间隔
private float mDividerHeight = 0;
//选中的歌词,即时间线所在的歌词
private int mCurrentLine;
//单击该view,如果不是点击播放按钮,则歌词界面消失
private OnCoverChangeListener coverChangeListener;
/**
* 播放按钮的监听器,如果成功消费该事件,则更新Ui
*/
public interface OnPlayClickListener {
boolean onPlayClick(long time);
}
public LyricView(Context context) {
this(context, null);
}
public LyricView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LyricView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
mGestureDetector.setIsLongpressEnabled(false);
mScroller = new Scroller(context);
}
private void initView(Context context, AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LyricView);
mTextSize = ta.getDimension(R.styleable.LyricView_text_size, getResources().getDimension(R.dimen.sp_16));
mDividerHeight = ta.getDimension(R.styleable.LyricView_text_divider, getResources().getDimension(R.dimen.dp_10));
mNormalTextColor = ta.getColor(R.styleable.LyricView_normal_color, Color.parseColor("#ccffffff"));
mCurrentTextColor = ta.getColor(R.styleable.LyricView_current_color, Color.parseColor("#ffffff"));
mTimelineTextColor = ta.getColor(R.styleable.LyricView_time_color, Color.parseColor("#ccffffff"));
mDefaultLabel = ta.getString(R.styleable.LyricView_default_label);
mPadding = ta.getDimension(R.styleable.LyricView_lrc_padding, 0);
mTimelineColor = ta.getColor(R.styleable.LyricView_timeline_color, Color.parseColor("#f0f0f0"));
mPlayDrawable = ResourcesCompat.getDrawable(getResources(),R.drawable.ic_lrc_play,null);
float timeTextSize = ta.getDimension(R.styleable.LyricView_time_text_size, getResources().getDimension(R.dimen.sp_10));
mTextGravity = ta.getInteger(R.styleable.LyricView_text_gravity, LrcEntry.GRAVITY_CENTER);
ta.recycle();
mLrcPaint.setAntiAlias(true);
mLrcPaint.setTextSize(mTextSize);
mLrcPaint.setTextAlign(Paint.Align.LEFT);
mTimePaint.setAntiAlias(true);
mTimePaint.setTextSize(timeTextSize);
mTimePaint.setTextAlign(Paint.Align.CENTER);
mTimePaint.setStrokeWidth(getResources().getDimension(R.dimen.dp_2));
mTimePaint.setStrokeCap(Paint.Cap.ROUND);
mTimeFontMetrics = mTimePaint.getFontMetrics();
}
/**
* 加载双语歌词
*/
public void loadLrc(String mainLrcText, String secondLrcText) {
Log.d(TAG,"mainLrcText : "+mainLrcText+" ");
reset();
String[] lrc = new String[2];
lrc[0] = mainLrcText;
lrc[1] = secondLrcText;
List<LrcEntry> parseList = LrcUtils.parseLrc(lrc);
if (parseList != null && !parseList.isEmpty()) {
mLrcEntryList.addAll(parseList);
}
Collections.sort(mLrcEntryList);
initEntryList();
invalidate();
}
private void reset() {
endAnimation();
mScroller.forceFinished(true);
isShowTimeline = false;
isTouching = false;
isFling = false;
removeCallbacks(hideTimelineRunnable);
mLrcEntryList.clear();
mOffset = 0;
mCurrentLine = 0;
invalidate();
}
private Runnable hideTimelineRunnable = () -> {
if (lrcNotEmpty() && isShowTimeline) {
isShowTimeline = false;
smoothScrollTo(mCurrentLine, ANIMATION_DURATION);
}
};
private void endAnimation() {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.end();
}
}
/**
* 传入时间刷新歌词
*/
public void updateTime(long time) {
if (!lrcNotEmpty()) {
return;
}
runOnUi(() -> {
int line = findShowLine(time);
if (line != mCurrentLine) {
mCurrentLine = line;
if (!isShowTimeline) {
smoothScrollTo(line, ANIMATION_DURATION);
} else {
invalidate();
}
}
});
}
/**
* 用二分查找 对应的时间应该显示第几行歌词
*/
private int findShowLine(long time) {
int left = 0;
int right = mLrcEntryList.size();
while (left <= right) {
int middle = (left + right) / 2;
long middleTime = mLrcEntryList.get(middle).getTime();
if (time < middleTime) {
right = middle - 1;
} else {
if (middle + 1 >= mLrcEntryList.size() || time < mLrcEntryList.get(middle + 1).getTime()) {
return middle;
}
left = middle + 1;
}
}
return 0;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//一有变化就重新布局子View
if (changed) {
initPlayDrawable();
initEntryList();
if (lrcNotEmpty()) {
smoothScrollTo(mCurrentLine, 0);
}
}
}
/**
* 滑动到指定的行
*/
private void smoothScrollTo(int line, int duration) {
// if (!isShowTimeline) {
// mCurrentLine = line;
// }
float offset = getOffset(line);
endAnimation();
mAnimator = ValueAnimator.ofFloat(mOffset, offset);
mAnimator.setDuration(duration);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(animation -> {
mOffset = (float) animation.getAnimatedValue();
invalidate();
});
LrcUtils.resetDurationScale();
mAnimator.start();
}
/**
* 获取 改行歌词到顶部的 offset
*/
private float getOffset(int line) {
if (mLrcEntryList.get(line).getOffset() == Float.MIN_VALUE) {
float offset = getHeight() / 2;
for (int i = 1; i <= line; i++) {
offset -= (mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) / 2 + mDividerHeight;
}
mLrcEntryList.get(line).setOffset(offset);
}
return mLrcEntryList.get(line).getOffset();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerY = getHeight() / 2;
//没有歌词 则显示默认
if (!lrcNotEmpty()) {
mLrcPaint.setColor(mCurrentTextColor);
@SuppressLint("DrawAllocation")
StaticLayout staticLayout = new StaticLayout(mDefaultLabel, mLrcPaint, (int) getLrcWidth(), Layout.Alignment.ALIGN_CENTER, 1f, 0f, false);
drawText(canvas, staticLayout, centerY);
}
int centerLine = getCenterLine();
//画时间线、时间和播放图标
if (isShowTimeline) {
mPlayDrawable.draw(canvas);
mTimePaint.setColor(mTimelineColor);
canvas.drawLine(TIME_TEXT_WIDTH, centerY, getWidth() - TIME_TEXT_WIDTH, centerY, mTimePaint);
mTimePaint.setColor(mTimelineTextColor);
String timeText = LrcUtils.formatTime(mLrcEntryList.get(centerLine).getTime());
//高度居中显示
float timeX = getWidth() - TIME_TEXT_WIDTH / 2;
float timeY = centerY - (mTimeFontMetrics.descent + mTimeFontMetrics.ascent) / 2;
canvas.drawText(timeText, timeX, timeY, mTimePaint);
}
//画布偏移offset 位置
canvas.translate(0, mOffset);
float y = 0;
mLrcPaint.setTextSize(mTextSize);
for (int i = 0; i < mLrcEntryList.size(); i++) {
if (i > 0) {
y += (mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) / 2 + mDividerHeight;
}
if (i == mCurrentLine) {
mLrcPaint.setColor(mCurrentTextColor);
} else if (isShowTimeline && i == centerLine) {
mLrcPaint.setColor(mTimelineTextColor);
} else {
mLrcPaint.setColor(mNormalTextColor);
}
drawText(canvas, mLrcEntryList.get(i).getStaticLayout(), y);
}
}
/**
* 获取当前在视图中央的行数
*/
private int getCenterLine() {
int centerLine = 0;
float minDistance = Float.MAX_VALUE;
for (int i = 0; i < mLrcEntryList.size(); i++) {
if (Math.abs(mOffset - getOffset(i)) <= minDistance) {
minDistance = Math.abs(mOffset - getOffset(i));
centerLine = i;
}
}
return centerLine;
}
private void drawText(Canvas canvas, StaticLayout staticLayout, float y) {
canvas.save();
canvas.translate(mPadding, y - staticLayout.getHeight() / 2);
staticLayout.draw(canvas);
canvas.restore();
}
private void initPlayDrawable() {
int l = (int) ((TIME_TEXT_WIDTH - PLAY_DRAWABLE_WIDTH) / 2);
int t = (int) (getHeight() / 2 - PLAY_DRAWABLE_WIDTH / 2);
int r = (int) (l + PLAY_DRAWABLE_WIDTH);
int b = (int) (t + PLAY_DRAWABLE_WIDTH);
mPlayDrawable.setBounds(l, t, r, b);
}
/**
* 在布局的时候或者 解析完歌词的时候需要初始化list里面每一句歌词
*/
private void initEntryList() {
if (getWidth() == 0 || !lrcNotEmpty()) {
return;
}
for (LrcEntry lrcEntry : mLrcEntryList) {
lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity);
}
//以view高度 1/2 为中轴线
mOffset = getHeight() / 2f;
}
/**
* 获取歌词宽度
*/
private float getLrcWidth() {
return getWidth() - 2 * mPadding;
}
/**
* 歌词 是否 不为空
*/
private boolean lrcNotEmpty() {
return !mLrcEntryList.isEmpty();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
isTouching = false;
if (lrcNotEmpty() && !isFling) {
smoothScrollTo(getCenterLine(), ADJUST_DURATION);
}
}
return mGestureDetector.onTouchEvent(event);
}
/**
* 手势监听器
*/
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (lrcNotEmpty() && mListener != null) {
mScroller.forceFinished(true);
removeCallbacks(hideTimelineRunnable);
isTouching = true;
invalidate();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (lrcNotEmpty()) {
isShowTimeline = true;
mOffset += -distanceY;
//不能超出边界
mOffset = Math.min(mOffset, getOffset(0));
mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1));
invalidate();
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (lrcNotEmpty()) {
//在 fling开始滑动
mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0,
(int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0));
isFling = true;
}
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.d(TAG, "onSingleTapConfirmed");
if (lrcNotEmpty() && isShowTimeline && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) {
int centerLine = getCenterLine();
long centerLineTime = mLrcEntryList.get(centerLine).getTime();
//按了播放才回去更新UI
if (mListener != null && mListener.onPlayClick(centerLineTime)) {
postDelayed(hideTimelineRunnable, 400);
mCurrentLine = centerLine;
invalidate();
}
} else {
if (coverChangeListener != null) {
coverChangeListener.onCoverChange();
postDelayed(hideTimelineRunnable, 400);
}
}
return super.onSingleTapConfirmed(e);
}
};
public interface OnCoverChangeListener {
void onCoverChange();
}
public void setCoverChangeListener(OnCoverChangeListener coverChangeListener) {
this.coverChangeListener = coverChangeListener;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mOffset = mScroller.getCurrY();
invalidate();
}
if (isFling && mScroller.isFinished()) {
isFling = false;
if (lrcNotEmpty() && !isTouching) {
smoothScrollTo(getCenterLine(), ADJUST_DURATION);
}
}
}
@Override
protected void onDetachedFromWindow() {
removeCallbacks(hideTimelineRunnable);
super.onDetachedFromWindow();
}
/**
* 主线程中运行
*/
private void runOnUi(Runnable r) {
if (Looper.getMainLooper() == Looper.myLooper()) {
r.run();
} else {
post(r);
}
}
public void setListener(OnPlayClickListener mListener) {
this.mListener = mListener;
}
}
修改attrs.xml
:
<resources>
<declare-styleable name="LyricView">
<attr name="text_size" format="dimension"/>
<attr name="text_divider" format="dimension"/>
<attr name="normal_color" format="reference|color"/>
<attr name="current_color" format="reference|color"/>
<attr name="default_label" format="string"/>
<attr name="lrc_padding" format="dimension"/>
<attr name="timeline_color" format="reference|color"/>
<attr name="time_color" format="reference|color"/>
<attr name="time_text_size" format="dimension"/>
<attr name="text_gravity">
<enum name="center" value="0" />
<enum name="left" value="1" />
<enum name="right" value="2" />
attr>
declare-styleable>
resources>
添加行歌词实体类LrcEntry
:
/**
* 一行歌词的实体类,因为要按照时间排序,所以需要实现Comparable
* 用它来画到View上
*/
public class LrcEntry implements Comparable<LrcEntry> {
//歌词所对应的时间
private long time;
//第一种语言的歌词的内容
private String text;
//第二种语言的歌词内容,一开始是空的,需要设置
private String secondText;
//StaticLayout,可以自动换行
private StaticLayout staticLayout;
//这一行歌词距离视图顶部的距离
private float offset = Float.MIN_VALUE;
//歌词的居中/左/右显示
public static final int GRAVITY_CENTER = 0;
public static final int GRAVITY_LEFT = 1;
public static final int GRAVITY_RIGHT = 2;
public LrcEntry(long time, String text) {
this.time = time;
this.text = text;
}
/**
* 绘制时每一行都要初始化其内容、宽度、绘制格式
*/
public void init(TextPaint paint, int width, int gravity) {
Layout.Alignment align;
switch (gravity) {
case GRAVITY_LEFT:
align = Layout.Alignment.ALIGN_NORMAL;
break;
default:
case GRAVITY_CENTER:
align = Layout.Alignment.ALIGN_CENTER;
break;
case GRAVITY_RIGHT:
align = Layout.Alignment.ALIGN_OPPOSITE;
break;
}
staticLayout = new StaticLayout(getShowText(), paint, width, align, 1f, 0f, false);
offset = Float.MIN_VALUE;
}
public StaticLayout getStaticLayout() {
return staticLayout;
}
public void setOffset(float offset) {
this.offset = offset;
}
/**
* 获取歌词高度
*/
public int getHeight() {
if (staticLayout == null) {
return 0;
}
return staticLayout.getHeight();
}
private String getShowText() {
if (!TextUtils.isEmpty(secondText)) {
return text + "\n" + secondText;
}
return text;
}
public void setSecondText(String secondText) {
this.secondText = secondText;
}
public String getText() {
return text;
}
public String getSecondText() {
return secondText;
}
public float getOffset() {
return offset;
}
public long getTime() {
return time;
}
/**
* 根据歌词时间来比较大小
*/
@Override
public int compareTo(LrcEntry entry) {
if (entry == null) {
return -1;
}
return (int) (time - entry.getTime());
}
}
@GET("lyric") //获取歌词
LiveData<ApiResponse<LyricEntity>> getLyric(@Query("id") long songId);
新增歌词实体类LyricEntity
@NoArgsConstructor
@Data
public class LyricEntity {
private boolean sgc;
private boolean sfy;
private boolean qfy;
private LrcEntity lrc;
private KlyricEntity klyric;
private TlyricEntity tlyric;
private RomalrcEntity romalrc;
private int code;
@NoArgsConstructor
@Data
public static class LrcEntity {
private int version;
private String lyric;
}
@NoArgsConstructor
@Data
public static class KlyricEntity {
private int version;
private String lyric;
}
@NoArgsConstructor
@Data
public static class TlyricEntity {
private int version;
private String lyric;
}
@NoArgsConstructor
@Data
public static class RomalrcEntity {
private int version;
private String lyric;
}
}
//提取颜色
implementation 'androidx.palette:palette:1.0.0'
public class SongPlayViewModel extends ViewModel {
private SavedStateHandle state;
public UserInfoUi ui;
private String userInfo;
public Boolean isShowLrc = false;
public ObservableField<String> currentSongUrl = new ObservableField<>("");
public ObservableLong currentSongId = new ObservableLong();
public SongPlayViewModel(SavedStateHandle savedStateHandle) {
this.state = savedStateHandle;
}
public LiveData<ApiResponse<LyricEntity>> getLyric() {
return RetrofitUtils.getmApiUrl().getLyric(currentSongId.get());
}
public void musicPlayOrPause(){
if (MusicPlay.isPlaying()){
MusicPlay.pauseMusic();
}else {
MusicPlay.restoreMusic();
}
}
public void next(){
MusicPlay.skipToNext();
}
public void previous(){
MusicPlay.skipToPrevious();
}
}
public class CurrentSongPlayActivity extends BaseActivity {
private ActivityCurrentSongPlayBinding binding;
private ObjectAnimator rotationAnim;
private MusicInfo musicInfo;
private SongPlayViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityCurrentSongPlayBinding.inflate(getLayoutInflater());
viewModel = new ViewModelProvider(this).get(SongPlayViewModel.class);
binding.setVm(viewModel);
binding.setLifecycleOwner(this);
setContentView(binding.getRoot());
initView();
initAnim();
initObserver();
}
private void initObserver() {
//加载歌词
viewModel.getLyric()
.observe(this, lyricEntityApiResponse -> {
ViewExtensionKt.printLog("当前"+lyricEntityApiResponse.getMessage());
if (lyricEntityApiResponse.getStatus() == Status.SUCCESS){
if (lyricEntityApiResponse.getData().getLrc() != null){
if (lyricEntityApiResponse.getData().getTlyric() != null){
binding.lrc.loadLrc(lyricEntityApiResponse.getData().getLrc().getLyric(),lyricEntityApiResponse.getData().getTlyric().getLyric());
}else {
binding.lrc.loadLrc(lyricEntityApiResponse.getData().getLrc().getLyric(),"");
}
}else {
binding.lrc.loadLrc("","");
}
}
});
}
private void initView() {
musicInfo = getIntent().getParcelableExtra(MUSIC_INFO);
initImageBg(musicInfo);
initListener();
binding.viewBody.setOnClickListener(view -> {
if (ClickUtil.enableClick()){
viewModel.isShowLrc = !viewModel.isShowLrc;
showLyrics(viewModel.isShowLrc);
}
});
//移动歌曲进度
binding.seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
MusicPlay.seekTo(seekBar.getProgress(),true);
binding.lrc.updateTime(seekBar.getProgress());
}
});
//修改模式
binding.ivPlayMode.setOnClickListener(v -> {
if (ClickUtil.enableClick()){
changeRepeatMode();
}
});
binding.lrc.setListener(time -> {
MusicPlay.seekTo(time,true);
return true;
});
binding.lrc.setCoverChangeListener(()->{
viewModel.isShowLrc = false;
showLyrics(false);
});
}
private void initImageBg(MusicInfo musicInfo) {
viewModel.currentSongUrl.set(musicInfo.getSongCover());
RequestOptions options = new RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.bitmapTransform(new BlurTransformation(25, 30));
viewModel.currentSongId.set(Long.parseLong(musicInfo.getSongId()));
initObserver();
Glide.with(this)
.asBitmap()
.load(musicInfo.getSongCover())
.transition(BitmapTransitionOptions.withCrossFade(1500))
.apply(options)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
binding.imgBc.setImageBitmap(resource);
StatusBarUtil.setTranslucentForImageView(CurrentSongPlayActivity.this,0,binding.viewTitleBg);
Palette.from(resource)
.setRegion(0,0,getScreenWidth(),getStatusBarHeight())
.maximumColorCount(6)
.generate(palette -> {
Palette.Swatch mostPopularSwatch = null;
for (Palette.Swatch swatch: palette.getSwatches()){
if (mostPopularSwatch == null
|| swatch.getPopulation() > mostPopularSwatch.getPopulation()){
mostPopularSwatch = swatch;
}
}
if (mostPopularSwatch!= null){
double luminance =ColorUtils.calculateLuminance(mostPopularSwatch.getRgb());
// 当luminance小于0.5时,我们认为这是一个深色值.
if (luminance < 0.5){
setDarkStatusBar();
}else {
setLightStatusBar();
}
}
});
}
});
binding.tvTitle.setText(musicInfo.getSongName());
binding.tvSinger.setText(musicInfo.getArtist());
}
private int getScreenWidth(){
DisplayMetrics displayMetrics =new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
private int getStatusBarHeight(){
int result = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0){
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}
private void initListener() {
MusicPlay.onPlayStateListener(this, new OnMusicPlayStateListener() {
@Override
public void onPlayState(@NonNull PlayManger playManger) {
switch (playManger.getStage()){
case PlayManger.PAUSE:
case PlayManger.IDLE:
rotationAnim.pause();
binding.ivPlay.setImageResource(R.drawable.shape_play_white);
break;
case PlayManger.PLAYING:
rotationAnim.start();
binding.ivPlay.setImageResource(R.drawable.shape_pause_white);
break;
case PlayManger.BUFFERING:
ViewExtensionKt.printLog("缓冲");
break;
case PlayManger.SWITCH:
//切歌
if (playManger.getSongInfo() != null){
initImageBg(playManger.getSongInfo());
}
break;
}
}
});
MusicPlay.onPlayProgressListener(new OnMusicPlayProgressListener() {
@Override
public void onPlayProgress(long curP, long duration) {
if (binding.seekBar.getMax() != duration){
binding.seekBar.setMax((int) duration);
binding.tvTotalTime.setText(TimeUtil.getTimeNoYMDH(duration));
}
binding.tvPastTime.setText(TimeUtil.getTimeNoYMDH(curP));
binding.lrc.updateTime(curP);
binding.seekBar.setProgress((int) curP);
}
});
}
//根据isShowLyrics来判断是否展示歌词
private void showLyrics(boolean isShowLyrics) {
binding.ivMusicCover.setVisibility(isShowLyrics ? View.GONE : View.VISIBLE);
binding.lrc.setVisibility(isShowLyrics ? View.VISIBLE : View.GONE);
}
//使状态栏图标黑
private void setLightStatusBar(){
binding.tvTitle.setTextColor(getColor(R.color.black80));
binding.tvSinger.setTextColor(getColor(R.color.black70));
int flags = getWindow().getDecorView().getSystemUiVisibility();
getWindow().getDecorView().setSystemUiVisibility(flags | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
//使状态栏图标白
private void setDarkStatusBar(){
binding.tvTitle.setTextColor(getColor(R.color.white));
binding.tvSinger.setTextColor(getColor(R.color.white80));
int flags = getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
getWindow().getDecorView().setSystemUiVisibility(flags^View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
private void changeRepeatMode(){
int currentModel = MusicPlay.getRepeatMode();
switch (currentModel){
case SpConstant.REPEAT_MODE_NONE:
MusicPlay.setRepeatMode(SpConstant.REPEAT_MODE_ONE,true);
ToastUtils.show(getString(R.string.repeat_one));
break;
case SpConstant.REPEAT_MODE_ONE:
MusicPlay.setRepeatMode(SpConstant.REPEAT_MODE_SHUFFLE,false);
ToastUtils.show(getString(R.string.repeat_random));
break;
case SpConstant.REPEAT_MODE_SHUFFLE:
MusicPlay.setRepeatMode(SpConstant.REPEAT_MODE_NONE,true);
ToastUtils.show(getString(R.string.repeat_none));
break;
}
}
private void initAnim() {
rotationAnim = ObjectAnimator.ofFloat(binding.ivMusicCover, "rotation", 360f);
rotationAnim.setDuration(25 * 1000);
rotationAnim.setInterpolator(new LinearInterpolator());
rotationAnim.setRepeatCount(100000);
rotationAnim.setRepeatMode(ValueAnimator.RESTART);
rotationAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
super.onAnimationEnd(animation, isReverse);
rotationAnim.start();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
rotationAnim.cancel();
rotationAnim.removeAllListeners();
rotationAnim = null;
}
}
public static final String MUSIC_INFO = "MUSIC_INFO";
实现日推界面播放歌曲,传歌曲信息给播放界面:
public class DailySongsActivity extends BaseActivity {
private ActivityDailySongsBinding binding;
private DailySongsAdapter adapter;
private DailySongsViewModel viewModel;
private ArrayList<MusicInfo> songList = new ArrayList<>(); //增加歌曲信息列表
public String date = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDailySongsBinding.inflate(getLayoutInflater());
viewModel = new ViewModelProvider(this).get(DailySongsViewModel.class);
setContentView(binding.getRoot());
initRecycle();
initView();
initObserver();
}
private void initView() {
StatusBarUtil.setColor(this,getResources().getColor(R.color.colorPrimary,null),0);
binding.tvDay.setText(Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "");
binding.tvMonth.setText("/"+(Calendar.getInstance().get(Calendar.MONTH)+1)+"");
binding.title.ivBack.setOnClickListener(view -> {
if (ClickUtil.enableClick()){
finish();
}
});
binding.rvPlayTop.setOnClickListener(v -> {
if (ClickUtil.enableClick()){
//播放歌曲列表,从第一个开始
MusicPlay.playMusicByList(songList,0);
startActivity(new Intent(this, CurrentSongPlayActivity.class)
.putExtra(MUSIC_INFO,songList.get(0)));
}
});
}
private void initObserver() {
viewModel.getDailySongs().observe(this, dailySongsEntityApiResponse -> {
if (dailySongsEntityApiResponse.getStatus() == Status.SUCCESS){
adapter.setDataList(dailySongsEntityApiResponse.getData().getData().getDailySongs(),dailySongsEntityApiResponse.getData().getData().getRecommendReasons());
for (DailySongsEntity.DataEntity.SongsEntity data: dailySongsEntityApiResponse.getData().getData().getDailySongs()){
MusicInfo musicInfo = new MusicInfo();
musicInfo.setSongUrl(Constant.SONG_URL+data.getId());
musicInfo.setSongId(String.valueOf(data.getId()));
String songName = data.getName();
if (data.getTns() != null){ //外语翻译歌名
songName += "("+data.getTns().get(0)+")";
}
musicInfo.setSongName(songName);
musicInfo.setArtist(data.getAr().get(0).getName());
musicInfo.setSongCover(data.getAl().getPicUrl());
songList.add(musicInfo);
}
}
});
}
private void initRecycle() {
adapter = new DailySongsAdapter(this);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
binding.rvDaily.setLayoutManager(manager);
binding.rvDaily.setAdapter(adapter);
binding.rvDaily.setHasFixedSize(true);
}
}
新增单个item点击播放功能
@Override
public void onBindViewHolder(@NonNull DailySongsViewHolder holder, int position) {
DailySongsEntity.DataEntity.SongsEntity bean = dataList.get(position);
holder.tvSongName.setText(bean.getName());
holder.tvSinger.setText(bean.getAr().get(0).getName()+"-"+bean.getAl().getName());
BindingAdapter.loadRadiusImage(holder.imgSong,bean.getAl().getPicUrl());
MusicInfo musicInfo = new MusicInfo();
musicInfo.setSongUrl(Constant.SONG_URL+bean.getId());
musicInfo.setSongId(String.valueOf(bean.getId()));
String songName = bean.getName();
if (bean.getTns() != null){ //外语翻译歌名
songName += "("+bean.getTns().get(0)+")";
}
musicInfo.setSongName(songName);
musicInfo.setArtist(bean.getAr().get(0).getName());
musicInfo.setSongCover(bean.getAl().getPicUrl());
for (DailySongsEntity.DataEntity.RecommendReasonsEntity data: reasonList){
if (data.getSongId() == bean.getId()){
holder.tvRecommend.setText(data.getReason());
}
}
if (bean.getFee() == 1){//1表示vip歌曲
holder.tvVip.setVisibility(View.VISIBLE);
}
if (bean.getSq() != null){//表示该歌曲有SQ版本
holder.tvSq.setVisibility(View.VISIBLE);
}
if (bean.getHr() != null){//表示有hi版
holder.tvSq.setText(mContext.getString(R.string.music_type_hi));
holder.tvSq.setVisibility(View.VISIBLE);
}
holder.itemView.setOnClickListener(view -> {
if (ClickUtil.enableClick()){
MusicPlay.playMusicByInfo(musicInfo);
mContext.startActivity(new Intent(
mContext, CurrentSongPlayActivity.class
).putExtra(MUSIC_INFO,musicInfo));
}
});
}