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

系列文章

前言

上一節咱們仿照雲音樂實現了黑膠唱片專輯封面,這節咱們該實現歌詞顯示了。固然,歌詞不單單是顯示就完了,做爲一個有素質的音樂播放器,咱們固然還須要根據歌曲進度自動滾動歌詞,而且要支持上下拖動。git

簡介

Android歌詞控件,支持上下拖動歌詞,歌詞自動換行,自定義屬性,支持雙語歌詞。github

更新說明

v 2.1.0express

  • 新增支持雙語歌詞
  • 修復橫豎屏切換問題

v 2.0apache

  • 新增上下拖動歌詞功能

v 1.4canvas

  • 解析歌詞放在工做線程中
  • 優化多行歌詞時動畫不流暢

v 1.3bash

  • 支持多個時間標籤

v 1.2app

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

v 1.1less

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

v 1.0ide

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

使用

Gradlepost

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

屬性

屬性 描述
lrcTextSize 歌詞文本字體大小
lrcNormalTextColor 非當前行歌詞字體顏色
lrcCurrentTextColor 當前行歌詞字體顏色
lrcTimelineTextColor 拖動歌詞時選中歌詞的字體顏色
lrcTextGravity 歌詞對齊方向,center:居中對齊,left:靠左對齊,right:靠右對齊,默認爲 center
lrcDividerHeight 歌詞間距
lrcAnimationDuration 歌詞滾動動畫時長
lrcLabel 沒有歌詞時屏幕中央顯示的文字,如「暫無歌詞」
lrcPadding 歌詞文字的左右邊距
lrcTimelineColor 拖動歌詞時時間線的顏色
lrcTimelineHeight 拖動歌詞時時間線的高度
lrcPlayDrawable 拖動歌詞時左側播放按鈕圖片
lrcTimeTextColor 拖動歌詞時右側時間字體顏色
lrcTimeTextSize 拖動歌詞時右側時間字體大小

方法

方法 描述
loadLrc(File) 加載歌詞文件
loadLrc(File, File) 加載雙語歌詞文件,兩種語言的歌詞時間戳須要一致
loadLrc(String) 加載歌詞文本
loadLrc(String, String) 加載雙語歌詞文本,兩種語言的歌詞時間戳須要一致
loadLrcByUrl(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

相關文章
相關標籤/搜索