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

代碼比較簡單,大家根據源碼和注釋很容易就能看懂。到這里,我們已經實現了可拖動的歌詞控件了。

截圖看比較簡單,大家可以運行源碼或下載波尼音樂查看詳細效果。

關于作者

簡書:http://www.lxweimin.com/users/3231579893ac

微博:http://weibo.com/wangchenyan1993

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.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,377評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,245評論 4 61
  • 2017-01-11 鑒于我的掉以輕心和膽大恣意,一不小心就摔骨折了,我想說流年不利,但是老爸糾正我說是自己不小心...
    2000到2003閱讀 1,569評論 3 1
  • 讀者: 老師,你好。我想咨詢一個問題,這個問題我不知道值不值得問,還是它只是個小問題。我覺得可能不是我一個人這樣,...
    觀心閣筆記閱讀 1,020評論 0 1
  • 看一路電影 劇情每每被我們猜透 下一個路口 誰與誰相逢 又與誰錯過 演員的無奈 導演主導著全片 人生而逢時 才遇到...
    專屬9877閱讀 205評論 0 1