記一下自己在項目中用到的歌詞控件實現(xiàn)思路
控件效果類似于目前網(wǎng)易云播放器的歌詞顯示,大概是這樣:

控件支持:
- 正在播放的歌詞高亮顯示
- 隨進度自動滾動
- 可以手動滑動歌詞,顯示indicator(該句進度,橫線,播放按鈕)
- 點擊indicator的播放按鈕可以跳轉(zhuǎn)至所選中行播放
下面開始
一、 歌詞的處理
歌詞的處理這里不多介紹,網(wǎng)上有很多分析的文章,感興趣的朋友可以去看一下。這一步驟主要是將服務(wù)器返回給我們的歌詞字符串解析為我們可用的List,然后設(shè)置給我們的控件:

其中,LrcRow是我們解析好的每一行歌詞的實體類,該類包含一句歌詞的內(nèi)容,起始時間,總時長等信息
二、 控件分析
觀察上面效果,可以看出,控件主要由以下三部分組成:
- 高亮歌詞
- 普通歌詞
- indicator
那么我們先不考慮實現(xiàn)滑動效果之類,先考慮如何將這幾部分成功畫出?
為了畫出以上三部分,需要的對象:

代碼很簡單,就不細說了
下面就是開始在onDraw()中繪制的過程了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isLoadingLrc) {
drawHintText(canvas, LOADING_LRC_TEXT);
return;
}
if (!hasLrc()) {
drawHintText(canvas, DEFAULT_TEXT);
return;
}
if (needDrawIndicator) {
drawIndicator(canvas);
}
float y = getHeight() / 2;
for (int i = 0; i < lrcRowList.size(); i++) {
String lrc = getLrc(i);
if (i == curLine) {
drawHighlightText(canvas, lrc, y);
} else {
drawNormalText(canvas, i, y);
}
// 計算得到y(tǒng)坐標(biāo)
y = y + eachLineHeight;
}
}
邏輯很清晰,就是遍歷所有歌詞,如果該歌詞當(dāng)前正在播放,就調(diào)用drawHighlightText()方法繪制高亮歌詞,否則,就drawNormalText()繪制一般歌詞。
因為上面兩個方法邏輯類似,就以其中一個來說明其邏輯。

核心就是調(diào)用canvas.setText();代碼中設(shè)置起始坐標(biāo)x,是業(yè)務(wù)需求原因,當(dāng)歌詞長度超出一行時,需要水平滾動展示,這里可以不用關(guān)注

分為三部分:進度,橫線和播放按鈕,也很簡單,到這里,繪制的部分就結(jié)束了
三、 設(shè)置進度及自動滾動
控件中,使用Scroller+View的computeScroll實現(xiàn)彈性滑動


設(shè)置進度:

設(shè)置進度時,首先根據(jù)傳入的進度,計算得到該進度所對應(yīng)的行數(shù),然后由行數(shù)計算得到在y方向上的offset,最后調(diào)用smoothScrollTo()讓歌詞開始滾動。
這里區(qū)分了一下是否是用戶拖動進度條而導(dǎo)致的進度變化,邏輯稍微不一樣,主要是涉及到indicator的顯示控制邏輯,具體見代碼
四、 處理滑動及點擊事件
-
播放按鈕的點擊處理
重寫onTouchEvent()方法,當(dāng)down事件時,判斷該down事件的坐標(biāo)是否落在playBtn的區(qū)域內(nèi)(該區(qū)域可以從 畫該按鈕時的坐標(biāo)得到)
image.png
再在up事件中,再次判斷一下該坐標(biāo)是否滿足條件,若滿足,則將該句歌詞所對應(yīng)的progress,通過回調(diào)的方式回調(diào)給外部activity
- 滑動歌詞的處理
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actionDown(event);
break;
case MotionEvent.ACTION_MOVE:
if (!hasLrc()) {
return false;
}
if (!isDragingLrc) {
if (Math.abs(event.getY() - downY) > touchSlop) {
isDragingLrc = true;
stopHorizontalScrollWithTimer();
scroller.forceFinished(true);
lastY = event.getY();
}
}
if (isDragingLrc) {
isClickEvent = false;
float deltaY = event.getY() - lastY;
if ((getScrollY() - deltaY) < -eachLineHeight) {
// 處理上滑邊界,如果已經(jīng)滑動至頂端,則限制其繼續(xù)上滑
deltaY = deltaY > 0 ? 0 : deltaY;
} else if ((getScrollY() - deltaY) > lrcRowList.size() * eachLineHeight) {
// 處理下滑邊界
deltaY = deltaY < 0 ? 0 : deltaY;
}
scrollBy(getScrollX(), -(int)deltaY);
curLine = calculateLineNo();
lastY = event.getY();
return true;
}
lastY = event.getY();
break;
case MotionEvent.ACTION_UP:
actionUp(event);
break;
default:
break;
}
return true;
}
通過計算down事件時的y坐標(biāo),和move事件時的y坐標(biāo)的差值deltaY,調(diào)用view的scrollBy()實現(xiàn)歌詞的拖動滑動
這里需要注意,當(dāng)down和move的y坐標(biāo)值大于touchSlop,即可以認為是一次滑動事件時,需要設(shè)置顯示indicator。并且,當(dāng)上滑至最上/下方時,需要限制deltaY的值,否則,會出現(xiàn)可以無限上下滑動的情況
到這里,歌詞控件的主要邏輯都已經(jīng)理了一遍
歡迎溝通和提意見
