Android简易音乐重构MVVM Java版-新增歌曲播放界面+状态栏黑科技(十七)

关于

本篇主要实现播放界面包括,歌曲暂停、播放下一首、上一首、播放模式、歌词展示,实现根据歌曲背景深浅实现状态栏文字、标题文字黑白切换适配。
音乐播放封装使用《开源库MusicPlayManager - 封装StarrySky音乐库》
简易音乐app仅作为学习用,禁止用于商业及非法用途,如产生法律纠纷与本人无关

效果

在这里插入图片描述

新增歌曲播放界面

  新增activity_current_song_play.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">
    <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:

<?xml version="1.0" encoding="utf-8"?>
<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>

增加歌词view 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

<?xml version="1.0" encoding="utf-8"?>
<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());
    }
}

修改ApiService

@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'

添加SongPlayViewModel

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();
    }

}

修改CurrentSongPlayActivity.java

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;
    }
}

修改Constant.java

public static final String MUSIC_INFO = "MUSIC_INFO";

修改日推界面 DailySongsActivity

  实现日推界面播放歌曲,传歌曲信息给播放界面:

 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);
    }
}

修改日推适配器DailySongsAdapter

  新增单个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));
            }
        });
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪の星空朝酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值