記一下自己在項目中用到的歌詞控件實現思路
控件效果類似于目前網易云播放器的歌詞顯示,大概是這樣:
控件支持:
- 正在播放的歌詞高亮顯示
- 隨進度自動滾動
- 可以手動滑動歌詞,顯示indicator(該句進度,橫線,播放按鈕)
- 點擊indicator的播放按鈕可以跳轉至所選中行播放
下面開始
一、 歌詞的處理
歌詞的處理這里不多介紹,網上有很多分析的文章,感興趣的朋友可以去看一下。這一步驟主要是將服務器返回給我們的歌詞字符串解析為我們可用的List,然后設置給我們的控件:
其中,LrcRow是我們解析好的每一行歌詞的實體類,該類包含一句歌詞的內容,起始時間,總時長等信息
二、 控件分析
觀察上面效果,可以看出,控件主要由以下三部分組成:
- 高亮歌詞
- 普通歌詞
- indicator
那么我們先不考慮實現滑動效果之類,先考慮如何將這幾部分成功畫出?
為了畫出以上三部分,需要的對象:
代碼很簡單,就不細說了
下面就是開始在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坐標
y = y + eachLineHeight;
}
}
邏輯很清晰,就是遍歷所有歌詞,如果該歌詞當前正在播放,就調用drawHighlightText()方法繪制高亮歌詞,否則,就drawNormalText()繪制一般歌詞。
因為上面兩個方法邏輯類似,就以其中一個來說明其邏輯。
核心就是調用canvas.setText();代碼中設置起始坐標x,是業務需求原因,當歌詞長度超出一行時,需要水平滾動展示,這里可以不用關注
分為三部分:進度,橫線和播放按鈕,也很簡單,到這里,繪制的部分就結束了
三、 設置進度及自動滾動
控件中,使用Scroller+View的computeScroll實現彈性滑動
設置進度:
設置進度時,首先根據傳入的進度,計算得到該進度所對應的行數,然后由行數計算得到在y方向上的offset,最后調用smoothScrollTo()讓歌詞開始滾動。
這里區分了一下是否是用戶拖動進度條而導致的進度變化,邏輯稍微不一樣,主要是涉及到indicator的顯示控制邏輯,具體見代碼
四、 處理滑動及點擊事件
-
播放按鈕的點擊處理
重寫onTouchEvent()方法,當down事件時,判斷該down事件的坐標是否落在playBtn的區域內(該區域可以從 畫該按鈕時的坐標得到)
image.png
再在up事件中,再次判斷一下該坐標是否滿足條件,若滿足,則將該句歌詞所對應的progress,通過回調的方式回調給外部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) {
// 處理上滑邊界,如果已經滑動至頂端,則限制其繼續上滑
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坐標,和move事件時的y坐標的差值deltaY,調用view的scrollBy()實現歌詞的拖動滑動
這里需要注意,當down和move的y坐標值大于touchSlop,即可以認為是一次滑動事件時,需要設置顯示indicator。并且,當上滑至最上/下方時,需要限制deltaY的值,否則,會出現可以無限上下滑動的情況
到這里,歌詞控件的主要邏輯都已經理了一遍
歡迎溝通和提意見