Android開源音樂播放器之自動滾動歌詞

chay發表於2019-01-11

系列文章

前言

上一節我們仿照雲音樂實現了黑膠唱片專輯封面,這節我們該實現歌詞顯示了。當然,歌詞不僅僅是顯示就完了,作為一個有素質的音樂播放器,我們當然還需要根據歌曲進度自動滾動歌詞,並且要支援上下拖動。

簡介

Android歌詞控制元件,支援上下拖動歌詞,歌詞自動換行,自定義屬性。

Android開源音樂播放器之自動滾動歌詞

更新說明

v 2.0

  • 新增上下拖動歌詞功能

v 1.4

  • 解析歌詞放在工作執行緒中
  • 優化多行歌詞時動畫不流暢

v 1.3

  • 支援多個時間標籤

v 1.2

  • 支援RTL(從右向左)語言

v 1.1

  • 新增歌詞自動換行
  • 新增自定義歌詞Padding
  • 優化歌詞解析

v 1.0

  • 支援自動滾動
  • 支援自定義屬性

使用

Gradle

// "latestVersion"改為文首徽章後對應的數值
compile 'me.wcy:lrcview:latestVersion'
複製程式碼

屬性

屬性 描述
lrcTextSize 歌詞文字字型大小
lrcNormalTextColor 非當前行歌詞字型顏色
lrcCurrentTextColor 當前行歌詞字型顏色
lrcTimelineTextColor 拖動歌詞時選中歌詞的字型顏色
lrcDividerHeight 歌詞間距
lrcAnimationDuration 歌詞滾動動畫時長
lrcLabel 沒有歌詞時螢幕中央顯示的文字,如“暫無歌詞”
lrcPadding 歌詞文字的左右邊距
lrcTimelineColor 拖動歌詞時時間線的顏色
lrcTimelineHeight 拖動歌詞時時間線的高度
lrcPlayDrawable 拖動歌詞時左側播放按鈕圖片
lrcTimeTextColor 拖動歌詞時右側時間字型顏色
lrcTimeTextSize 拖動歌詞時右側時間字型大小

方法

方法 描述
loadLrc(File) 載入歌詞檔案
loadLrc(String) 載入歌詞文字
hasLrc() 歌詞是否有效
setLabel(String) 設定歌詞為空時檢視中央顯示的文字,如“暫無歌詞”
updateTime(long) 重新整理歌詞
onDrag(long) 將歌詞滾動到指定時間,已棄用,請使用 updateTime(long) 代替
setOnPlayClickListener(OnPlayClickListener) 設定拖動歌詞時,播放按鈕點選監聽器。如果為非 null ,則啟用歌詞拖動功能,否則將將禁用歌詞拖動功能
setNormalColor(int) 設定非當前行歌詞字型顏色
setCurrentColor(int) 設定當前行歌詞字型顏色
setTimelineTextColor 設定拖動歌詞時選中歌詞的字型顏色
setTimelineColor 設定拖動歌詞時時間線的顏色
setTimeTextColor 設定拖動歌詞時右側時間字型顏色

思路分析

正常播放時,當前播放的那一行應該在檢視中央,首先計算出每一行位於中央時畫布應該滾動的距離。
將所有歌詞按順序畫出,然後將畫布滾動的相應的距離,將正在播放的歌詞置於螢幕中央。
歌詞滾動時要有動畫,使用屬性動畫即可,我們可以使用當前行和上一行的滾動距離作為動畫的起止值。
多行歌詞繪製採用StaticLayout。

上下拖動時,歌詞跟隨手指滾動,繪製時間線。
手指離開螢幕時,一段時間內,如果沒有下一步操作,則隱藏時間線,同時將歌詞滾動到實際位置,回到正常播放狀態;
如果點選播放按鈕,則跳轉到指定位置,回到正常播放狀態。

程式碼實現

onDraw 中將歌詞文字繪出,mOffset 是當前應該滾動的距離

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int centerY = getHeight() / 2;

    // 無歌詞檔案
    if (!hasLrc()) {
        mLrcPaint.setColor(mCurrentTextColor);
        @SuppressLint("DrawAllocation")
        StaticLayout staticLayout = new StaticLayout(mDefaultLabel, mLrcPaint, (int) getLrcWidth(),
                Layout.Alignment.ALIGN_CENTER, 1f, 0f, false);
        drawText(canvas, staticLayout, centerY);
        return;
    }

    int centerLine = getCenterLine();

    if (isShowTimeline) {
        mPlayDrawable.draw(canvas);

        mTimePaint.setColor(mTimelineColor);
        canvas.drawLine(mTimeTextWidth, centerY, getWidth() - mTimeTextWidth, centerY, mTimePaint);

        mTimePaint.setColor(mTimeTextColor);
        String timeText = LrcUtils.formatTime(mLrcEntryList.get(centerLine).getTime());
        float timeX = getWidth() - mTimeTextWidth / 2;
        float timeY = centerY - (mTimeFontMetrics.descent + mTimeFontMetrics.ascent) / 2;
        canvas.drawText(timeText, timeX, timeY, mTimePaint);
    }

    canvas.translate(0, mOffset);

    float y = 0;
    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 GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        if (hasLrc() && mOnPlayClickListener != null) {
            mScroller.forceFinished(true);
            removeCallbacks(hideTimelineRunnable);
            isTouching = true;
            isShowTimeline = true;
            invalidate();
            return true;
        }
        return super.onDown(e);
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (hasLrc()) {
            mOffset += -distanceY;
            mOffset = Math.min(mOffset, getOffset(0));
            mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1));
            invalidate();
            return true;
        }
        return super.onScroll(e1, e2, distanceX, distanceY);
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        if (hasLrc()) {
            mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0, (int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0));
            isFling = true;
            return true;
        }
        return super.onFling(e1, e2, velocityX, velocityY);
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        if (hasLrc() && isShowTimeline && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) {
            int centerLine = getCenterLine();
            long centerLineTime = mLrcEntryList.get(centerLine).getTime();
            // onPlayClick 消費了才更新 UI
            if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) {
                isShowTimeline = false;
                removeCallbacks(hideTimelineRunnable);
                mCurrentLine = centerLine;
                invalidate();
                return true;
            }
        }
        return super.onSingleTapConfirmed(e);
    }
};
複製程式碼

滾動動畫

private void scrollTo(int line, long duration) {
    float offset = getOffset(line);
    endAnimation();

    mAnimator = ValueAnimator.ofFloat(mOffset, offset);
    mAnimator.setDuration(duration);
    mAnimator.setInterpolator(new LinearInterpolator());
    mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mOffset = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    mAnimator.start();
}
複製程式碼

程式碼比較簡單,大家根據原始碼和註釋很容易就能看懂。到這裡,我們已經實現了可拖動的歌詞控制元件了。
截圖看比較簡單,大家可以執行原始碼或下載波尼音樂檢視詳細效果。

關於作者

掘金:juejin.im/user/58abd9…
微博:weibo.com/wangchenyan…

License

Copyright 2017 wangchenyan

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
複製程式碼

遷移自我的簡書 2016.06.09

相關文章