本系列可能会伴随大家很长时间,这里我会从0开始搭建一个「网易云音乐」的APP出来。
下面是该APP 功能的思维导图:
前期回顾:
1.Flutter实战 | 从 0 搭建「网易云音乐」APP(一、创建项目、添加插件、通用代码) 2.Flutter实战 | 从 0 搭建「网易云音乐」APP(二、Splash Page、登录页、发现页) 3.Flutter实战 | 从 0 搭建「网易云音乐」APP(三、每日推荐、推荐歌单) 4.Flutter实战 | 从 0 搭建「网易云音乐」APP(四、排行榜、播放页面) 5.Flutter实战 | 从 0 搭建「网易云音乐」APP(五、播放功能逻辑) 6.Flutter实战 | 从 0 搭建「网易云音乐」APP(六、歌词(一))
本篇为第七篇,在这里我们会搭建歌词页面剩余的逻辑。
我们书接上文,上文中说到歌词控件的需求:
一个歌词控件需要什么?
1.展示歌词 2.当前歌词高亮显示 3.跟随当前时间滚动 4.可以拖动 5.拖动时显示时间线 6.可以从时间线上点击播放
上文我们实现了前三个,那这篇文章就带大家来实现后三个功能。
下面我们就开始。
不知道还记不记得,上篇文章中,我们是如何绘制歌词的:
_offsetY + size.height / 2 + lyricPaints[0].height / 2;
该段代码就是获取中间位置的。
其中有个 _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();
}
这样就完成了我们拖动歌词的需求。
这是相对来说比较复杂的功能,涉及到的有:
1.拖拽时显示,不拖拽时不显示 2.拖拽到某一行改变颜色 3.显示拖拽到的那一行的起始时间 4.画时间线
首先不管拖拽的东西,先来显示这个时间线。
因为歌词是使用 CustomPainter
来实现的,那时间线,我们也是,使用 CustomPainter
来实现。
首先看一下样式:
可以看到,这个「时间线」是由三部分组成:
1.播放按钮 2.一条线 3.当前行的时间
播放按钮我们使用的是 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);
}
逻辑如下:
1.首先判断该 Timer 是否为空 2.如果不为空则判断是否在活跃状态 3.如果都满足条件,则取消这个 Timer 的任务,并且置为空 4.最后重新赋值任务
这样就可以达到我们预期的结果:在最后一次拖动结束的一秒钟后,把时间线消失。
时间线的显示和消失,我们也搞定了,那么现在就开始搞拖拽的效果。
拖拽到某一行改变颜色,我们怎么知道是拖拽到了哪一行?
这还不简单,直接使用 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 上的歌词控件,终于我们歌词就全部结束了,歌词的功能真的是不少,写起来也是挺难的,判断的东西有点多。(也可能是因为我第一次写歌词类的东西,比较菜)
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。