上一節咱們仿照雲音樂實現了黑膠唱片專輯封面,這節咱們該實現歌詞顯示了。固然,歌詞不單單是顯示就完了,做爲一個有素質的音樂播放器,咱們固然還須要根據歌曲進度自動滾動歌詞,而且要支持上下拖動。git
Android歌詞控件,支持上下拖動歌詞,歌詞自動換行,自定義屬性,支持雙語歌詞。github
v 2.1.0
express
v 2.0
apache
v 1.4
canvas
v 1.3
bash
v 1.2
app
v 1.1
less
v 1.0
ide
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) | 刷新歌詞 |
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…
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