本系列可能會伴隨你們很長時間,這裏我會從0開始搭建一個「網易雲音樂」的APP出來。前端
下面是該APP 功能的思惟導圖:git
前期回顧:github
本篇爲第七篇,在這裏咱們會搭建歌詞頁面剩餘的邏輯。canvas
咱們書接上文,上文中說到歌詞控件的需求:微信
一個歌詞控件須要什麼?佈局
- 展現歌詞
- 當前歌詞高亮顯示
- 跟隨當前時間滾動
- 能夠拖動
- 拖動時顯示時間線
- 能夠從時間線上點擊播放
上文咱們實現了前三個,那這篇文章就帶你們來實現後三個功能。字體
下面咱們就開始。動畫
不知道還記不記得,上篇文章中,咱們是如何繪製歌詞的:ui
_offsetY + size.height / 2 + lyricPaints[0].height / 2;
複製代碼
該段代碼就是獲取中間位置的。spa
其中有個 _offsetY ,在上篇文章中,咱們使用它來作自動滾動效果,那在本功能中,咱們就可使用它來作拖動的效果。
直接在 CustomPaint
控件上套一個 GestureDetector
:
onVerticalDragUpdate: (e) {
_lyricWidget.offsetY += e.delta.dy;
}
複製代碼
而後在 onVerticalDragUpdate
中使這個 offsetY
加上偏移量就好了。
可是關於歌詞拖動這裏有個細節:不能拖動到極限(上、下)。
這裏的極限是什麼?
上極限爲 _offsetY.abs() < lyricPaints[0].height + ScreenUtil().setWidth(30)
,
下極限爲 _offsetY.abs() > (totalHeight + lyricPaints[0].height + ScreenUtil().setWidth(30))
,
也就是咱們第一行和最後一行文字的地方。
賦值 _offsetY
方法所有代碼以下:
set offsetY(double value) {
// 判斷若是是在拖動狀態下
if (isDragging) {
// 不能小於最開始的位置
if (_offsetY.abs() < lyricPaints[0].height + ScreenUtil().setWidth(30)) {
_offsetY = (lyricPaints[0].height + ScreenUtil().setWidth(30)) * -1;
} else if (_offsetY.abs() > (totalHeight + lyricPaints[0].height + ScreenUtil().setWidth(30))) {
// 不能大於最大位置
_offsetY = (totalHeight + lyricPaints[0].height + ScreenUtil().setWidth(30)) * -1;
} else {
_offsetY = value;
}
} else {
_offsetY = value;
}
notifyListeners();
}
複製代碼
這樣就完成了咱們拖動歌詞的需求。
這是相對來講比較複雜的功能,涉及到的有:
首先無論拖拽的東西,先來顯示這個時間線。
由於歌詞是使用 CustomPainter
來實現的,那時間線,咱們也是,使用 CustomPainter
來實現。
首先看一下樣式:
能夠看到,這個「時間線」是由三部分組成:
播放按鈕咱們使用的是 icon,如何在 CustomPainter
中畫 icon?
使用 Paragraph
:
// 畫 icon
final icon = Icons.play_arrow;
var builder = prefix0.ParagraphBuilder(prefix0.ParagraphStyle(
fontFamily: icon.fontFamily,
fontSize: ScreenUtil().setWidth(60),
))
..addText(String.fromCharCode(icon.codePoint));
var para = builder.build();
para.layout(prefix0.ParagraphConstraints(
width: ScreenUtil().setWidth(60),
));
canvas.drawParagraph(
para,
Offset(ScreenUtil().setWidth(10),
size.height / 2 - ScreenUtil().setWidth(60)));
複製代碼
其實這裏是把 icon 當作字體來設置的,設置大小使用 fontSize
就行了。
線相對來講是好畫的了:
// 畫線
canvas.drawLine(
Offset(ScreenUtil().setWidth(80),
size.height / 2 - ScreenUtil().setWidth(30)),
Offset(size.width - ScreenUtil().setWidth(120),
size.height / 2 - ScreenUtil().setWidth(30)),
linePaint);
複製代碼
這其實也沒什麼好說的,就是畫個文字,算好偏移量就好了:
draggingLineTimeTextPainter = TextPainter(
text: TextSpan(
text: DateUtil.formatDateMs(dragLineTime,
format: "mm:ss"),
style: smallGrayTextStyle),
textDirection: TextDirection.ltr,
);
draggingLineTimeTextPainter.layout();
draggingLineTimeTextPainter.paint(
canvas,
Offset(size.width - ScreenUtil().setWidth(80),
size.height / 2 - ScreenUtil().setWidth(45)));
複製代碼
時間線畫完了,就該來到拖拽環節,這個時候同窗確定會想到,咱們剛纔套了一層 GestureDetector
。
沒錯,那在什麼條件下顯示和不顯示?
咱們首先想到的確定是 onVerticalDragDown
+ onVerticalDragEnd
,由於畢竟是在按下時顯示和擡起時消失嘛,
這就錯了,咱們不該該在手指按下的時候就顯示時間線,而應該是在拖動的時候顯示時間線!
咱們給 CustomPainter
一個變量:isDragging
-> 是否正在拖動中。
而後在 GestureDetector
的 onVerticalDragUpdate
方法中作操做:
onVerticalDragUpdate: (e) {
if (!_lyricWidget.isDragging) {
setState(() {
_lyricWidget.isDragging = true;
});
}
_lyricWidget.offsetY += e.delta.dy;
}
複製代碼
若是不是在拖動中,那麼則改變它的狀態。
而且在 CustomPainter
的 paint
方法中:
// 拖動狀態下顯示的東西
if (isDragging) {
// 畫 icon
xxx;
// 畫線
xxx;
// 畫當前行的時間
xxx;
}
複製代碼
這樣就完成了咱們顯示的問題,那何時不顯示?
咱們能夠經過查看網易雲官方APP來看一下,拖動結束後大約一兩秒鐘的時間纔會消失,這個時間差是爲了給用戶點擊時間線上的播放按鈕準備的。
那咱們也來實現一下。
首先咱們設置延遲消失時間是一秒,消失的動做其實就是把 isDragging
設置爲 false:
dragEndFunc = () {
if (_lyricWidget.isDragging) {
setState(() {
_lyricWidget.isDragging = false;
});
}
};
複製代碼
這裏學過前端的同窗應該都據說過一個詞:節流與防抖。
沒錯,若是這裏咱們在結束拖動的一秒內,再次拖動,那麼這個延遲的方法就會再次運行,這樣確定是有問題的,因此咱們也要進行節流與防抖。
如何進行防抖?
其實上一篇文章中自動滾動歌詞效果就帶了防抖,可是那個是使用的動畫,這裏咱們就要使用 Timer
來進行防抖。
首先定義好方法和延遲時間:
dragEndDuration = Duration(milliseconds: 1000);
dragEndFunc = () {
if (_lyricWidget.isDragging) {
setState(() {
_lyricWidget.isDragging = false;
});
}
};
複製代碼
接着在拖動結束後的方法中調用:
void cancelDragTimer() {
if (dragEndTimer != null) {
if (dragEndTimer.isActive) {
dragEndTimer.cancel();
dragEndTimer = null;
}
}
dragEndTimer = Timer(dragEndDuration, dragEndFunc);
}
複製代碼
邏輯以下:
這樣就能夠達到咱們預期的結果:在最後一次拖動結束的一秒鐘後,把時間線消失。
時間線的顯示和消失,咱們也搞定了,那麼如今就開始搞拖拽的效果。
拖拽到某一行改變顏色,咱們怎麼知道是拖拽到了哪一行?
這還不簡單,直接使用 offsetY
來判斷就行了呀:
if (isDragging &&
i ==
(_offsetY / (lyricPaints[0].height + ScreenUtil().setWidth(30)))
.abs()
.round() - 1) {
// 若是是拖動狀態中的當前行
lyricPaints[i].text =
TextSpan(text: lyric[i].lyric, style: commonWhite70TextStyle);
lyricPaints[i].layout();
}
複製代碼
若是 i == 正在拖動中 而且 用**當前偏移量 / 每行的偏移量 獲得的值的絕對值的四捨五入的值,**那麼就表明是當前拖動中的行。(說的有點亂)
由於總長度就是用每行的偏移量加起來的,最大的偏移量也就是這麼多,因此用偏移量除以每行的偏移量就能獲得咱們當前拖動到的行了。
而後設置不一樣顏色的字體就ok了。
既然咱們能獲得當前是哪一行,那獲取這一行的起始時間也不是難事:
dragLineTime = lyric[
(_offsetY / (lyricPaints[0].height + ScreenUtil().setWidth(30)))
.abs()
.round() -1]
.startTime.inMilliseconds;
複製代碼
到這咱們全部拖拽的功能算是結束了,就剩下一個點擊事件。
寫這個功能的時候,上來就遇到了一個問題,怎麼樣纔算點擊了這個 icon???
CustomPainter
裏面也沒有給這個佈局設置點擊事件的地方,wdnmd,這咋整?
苦思冥想,大不了我判斷點擊的座標!
說幹咱就幹,在 onTap 中沒有返回這個座標,那我先在 onPanDown 裏試試:
onPanDown: (e){
print(e.localPosition);
},
複製代碼
當我運行到手機,而且點擊的時候,整我的都很差了!
座標確實打印出來了,可是直接給我返回到碟片那個頁面了!!!
我居然忘了還有這個操做!點擊頁面是 「歌詞 」和 「碟片」 來回跳轉的!
這可咋整,如何才能讓他不跳轉?也就是不走父組件的 onTap()
方法。
這裏有一點,若是子組件有點擊事件,而且父組件沒有設置相對應的 behavior,那麼事件是不會冒泡到父組件的。
因此,咱們只須要進行相對應的設置:
onTapDown: _lyricWidget.isDragging
? (e) {
if (e.localPosition.dx > 0 &&
e.localPosition.dx < ScreenUtil().setWidth(100) &&
e.localPosition.dy >
_lyricWidget.canvasSize.height / 2 -
ScreenUtil().setWidth(100) &&
e.localPosition.dy <
_lyricWidget.canvasSize.height / 2 +
ScreenUtil().setWidth(100)) {
widget.model.seekPlay(_lyricWidget.dragLineTime);
}
}
: null,
複製代碼
若是是在拖動狀態中,那麼設置上點擊事件,若是不是的話,設置爲null 就行了,這也能解釋咱們上面給 isDragging
賦值的時候爲何會 setState() ,就是由於要設置這個點擊事件。
最後判斷點擊的位置就ok了,也是很是簡單的。
參考了不少 Android 上的歌詞控件,終於咱們歌詞就所有結束了,歌詞的功能真的是很多,寫起來也是挺難的,判斷的東西有點多。(也多是由於我第一次寫歌詞類的東西,比較菜)
固然仍是那句話,該項目是我本人本身在工做之餘寫的,因此進度不會很快,可是會一直寫下去。
你們若是有好的建議的話,歡迎提 issue,我會在第一時間回覆。
該系列文章代碼已傳至 GitHub:github.com/wanglu1209/…
另我我的建立了一個「Flutter 交流羣」,能夠添加我我的微信 「17610912320」來入羣。