系列文章
- Android開源在線音樂播放器——波尼音樂
- Android開源音樂播放器之播放器基本功能
- Android開源音樂播放器之高仿云音樂黑膠唱片
- Android開源音樂播放器之自動滾動歌詞
- Android開源音樂播放器之在線音樂列表自動加載更多
前言
上一節我們仿照云音樂實現了黑膠唱片專輯封面,這節我們該實現歌詞顯示了。當然,歌詞不僅僅是顯示就完了,作為一個有素質的音樂播放器,我們當然還需要根據歌曲進度自動滾動歌詞,并且要支持上下拖動。
- 項目地址:https://github.com/wangchenyan/lrcview
- 有問題請提Issues
- 如果喜歡,歡迎Star!
簡介
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) | 刷新歌詞 |
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.