先说个背景。
上个月我一个做音乐教育 App 的朋友跑来找我,说他们想在 App 里展示一段简单的乐谱,就那种几小节的示例谱,不需要特别复杂的排版,但一定要好看、干净,能跟 App 的设计风格融为一体。
他问我:Flutter 有没有现成的方案?
我当时第一反应是:乐谱渲染这玩意儿,不都是 WebView 套个 VexFlow 或者直接怼图片吗?Flutter 能行吗?
结果一查,发现这事儿比我想象的有意思得多。
这事儿难在哪儿
五线谱看着就是几条线加几个蝌蚪,但真正要程序化地画出来,细节特别多。
音符的位置要对应音高——C4 在哪根线上,D4 在哪个间里,这有一套严格的规则。符干该朝上还是朝下,取决于音符在五线谱上的位置。全音符、二分音符、四分音符的形状都不一样,符尾的走向还有讲究。更别提还有谱号、调号、拍号、连音线、升降号……
用一张图片当然能解决,但图片不能缩放、不能编辑、不能高亮某个音符,在交互式的音乐教学 App 里根本不够用。
所以问题就变成了:在 Flutter 里,到底怎么优雅地画五线谱?
一条最省力的路:直接用库
如果你跟我一样,不想从零开始造轮子,Flutter 生态里其实已经有一些现成的方案了。
我试了一圈,目前感觉最靠谱的是 flutter_music_notation。
这个库的成熟度让我有点意外——160 分的 pub 评分,支持全平台(iOS、Android、Web、Windows、macOS、Linux),所有标准谱号(高音、低音、中音、次中音),15 种调号,常用的拍号也都覆盖了。音符渲染用的是 SMuFL 规范的 Bravura 字体,这是专业乐谱排版的标准字体,画出来的效果非常讲究。
用法也很直观,就是创建一个 Measure(小节),往里塞音符,然后扔给 NotationView:
final measure = Measure(number: 0,timeSignature: TimeSignature.fourFour,keySignature: KeySignature.cMajor,notes: [Note(pitch: Pitch(noteName: NoteName.C, octave: 4),duration: NoteDuration.quarter(), startBeat: 0.0),Note(pitch: Pitch(noteName: NoteName.D, octave: 4),duration: NoteDuration.quarter(), startBeat: 1.0),Note(pitch: Pitch(noteName: NoteName.E, octave: 4),duration: NoteDuration.quarter(), startBeat: 2.0),Note(pitch: Pitch(noteName: NoteName.F, octave: 4),duration: NoteDuration.quarter(), startBeat: 3.0),],endBarline: BarlineType.final_,);NotationView(measures: [measure],config: NotationConfig(staffSpaceSize: 12, clef: ClefType.treble),)
更香的是它还支持 MIDI 同步播放和实时高亮,60fps 的播放帧率。这个功能对音乐教学 App 来说简直是刚需——谱子播到哪个音符,界面上对应的音符就高亮,用户跟着唱或者弹,体验直接拉满。
如果你只是想快速出一个 Demo,或者需求比较标准,这个库是首选。基本上就是配置几个对象,几行代码就能出来一个挺专业的乐谱视图。
但是……库不是万能的
flutter_music_notation 虽好,但它是一个"大而全"的库,引入了不少依赖和字体资源。如果你的 App 本身就比较重,再加一个乐谱渲染库,打包体积可能有点吃不消。
而且库的渲染逻辑是黑盒的——你想自定义某些元素的位置、颜色、间距?不一定能改得动。
这时候,另一个轻量级的库 simple_sheet_music 就值得考虑了。它专注于在 Canvas 上以声明式的方式渲染乐谱,只做一件事,不做 MIDI 也不做动画。代码更轻,侵入性更小,适合只需要"显示五线谱"这一件事的场景。
不过要注意,simple_sheet_music 的乐理模型比较基础,如果你需要处理复杂的谱面,可能就不够用了。
真正硬核的路:自己画
如果现有的库都满足不了你的需求——比如你想要一个完全自定义风格的乐谱,或者只想渲染特定格式的乐谱不想引入整个乐理模型——那最终还是要走到 CustomPaint + Canvas 这条路。
Flutter 的 CustomPaint 本质上就是你跟画布之间的一层薄薄的封装。你写一个 CustomPainter,重写 paint 方法,然后用 Canvas 对象的 API 一条线一条线地画。
画五线谱的基础操作,其实就是画五条平行线:
class StaffPainter extends CustomPainter {@overridevoid paint(Canvas canvas, Size size) {final paint = Paint()..color = Colors.black..strokeWidth = 1.5;final lineSpacing = 10.0; // 线与线的间距final startY = 50.0; // 起始 Y 坐标for (int i = 0; i < 5; i++) {final y = startY + i * lineSpacing;canvas.drawLine(Offset(20, y), Offset(size.width - 20, y), paint);}}@overridebool shouldRepaint(covariant CustomPainter oldDelegate) => false;}
就这么简单。五条线画完,五线谱的骨架就有了。
至于音符怎么画,有几种思路。
思路一:用字体。有些音乐专用字体(比如 Akvo、MusiQwik、Bravura)把音符做成了字符,你直接用 drawText 就能把音符"写"到画布上。优点是简单,缺点是位置和大小不够灵活。
思路二:用 drawPath 手动画。这就是真正硬核的地方了——你得自己定义音符的轮廓路径,符头的椭圆、符干的直线、符尾的曲线,全都得手动算。好处是完全可控,坏处是工作量巨大。符头朝上朝下还得动态判断,音符在第三线以上符干朝下,以下符干朝上,这规则得自己写逻辑。
思路三:混合方案。用字体画符头,用手绘路径画符干和符尾。这是我在一个开源项目里看到的折中方案,既保证了音符形状的标准(靠字体),又保持了符干方向的灵活性(靠手绘)。实际做出来的效果相当不错。
不过,除非你真的有很特殊的需求,否则我不太推荐从零开始手写完整的乐谱渲染逻辑。音符绘制涉及太多细节——符头形状、符干长度、符尾的角度和位置、连音线的贝塞尔曲线控制点、升降号的定位偏移……调起来非常磨人,足够你折腾好几周。
还有一个方向:MusicXML 解析
如果你的 App 需要导入现成的乐谱文件(比如从打谱软件导出的 MusicXML),那画五线谱只是最后一环。前面的工作——解析 MusicXML 文件,提取音符、节奏、谱号、调号等信息——反而更复杂。
好在 Flutter 生态里已经有 music_xml 这个库,专门用来解析 W3C 标准的 MusicXML 文件,能提取出音高、节奏、谱表、装饰音、渐强渐弱甚至歌词映射等完整信息。拿到这些数据之后,再驱动 Canvas 绘制或者 MIDI 播放,逻辑就顺了。
这个方案适合那些需要处理现有乐谱文件的项目,比如从网上导入曲谱、用户上传自己的作品等场景。
我最后选了哪个
说回我朋友的 App。他们的需求其实不复杂:App 内置十几首儿歌的简谱示例,不需要动态导入乐谱,也不需要 MIDI 播放。
我最后帮他选了 simple_sheet_music。理由很简单:轻量,够用,不需要操心底层绘制细节,依赖也少。
两周后他给我发消息,说效果不错,运行流畅,用户反馈也挺好。
我也顺手用 flutter_music_notation 写了个小 Demo,体验了一把高亮播放的功能,确实丝滑。如果以后做更复杂的音乐教学产品,我会优先考虑它。
总结一下
Flutter 里画五线谱,大致有三条路:
第一条:用现成的库。推荐 flutter_music_notation(功能全、支持 MIDI 播放)或 simple_sheet_music(轻量、简单)。
第二条:自己用 Canvas 手绘。适合定制化需求高的场景,但开发成本不低。
第三条:MusicXML 解析 + Canvas 绘制。适合需要处理现有乐谱文件的项目。
我的建议是:除非你有非常特殊的需求,否则先用库。等库确实满足不了的时候,再去考虑手绘或者混搭方案。
毕竟,写代码的快乐在于解决问题,而不是重新发明轮子。五线谱这轮子,已经有人帮你造得挺好了。
如果你有任何问题或更好的实现思路,欢迎在评论区留言交流!
谢谢你读到最后。
若觉得尚可,恳请点赞、在看、转发分享。
山高水长,我们下期再会。
夜雨聆风