Flutter Karaoke-Style Lyric Gradient Animation Tutorial
This article explains how to create a karaoke‑style per‑word gradient highlight for song lyrics in a Flutter app, covering the visual effect analysis, gradient positioning tricks, ShaderMask usage, animation with Tween and AnimationController, per‑character timing, and progress synchronization.
The author continues a Flutter project and shares the challenges faced while implementing a lyric‑highlight effect that gradually colors text as the song plays.
Effect analysis : Instead of coloring each character individually, a gradient is applied across the whole line, similar to many music players. The goal is to keep the unplayed part gray and animate the colored portion.
Problem : A simple linear gradient applied directly to the text colors the entire line, not the desired progressive effect.
clever idea : By moving the start position of the gradient mask farther left (approximately double the original range), the gradient can be made to appear as if it is filling the text progressively.
Implementation – Gradient : The ShaderMask widget is used to apply a custom shader to the text. A basic example:
ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (Rect bounds) {
return LinearGradient(colors: [Colors.red, Colors.green])
.createShader(bounds);
},
child: Text("要做神仙,驾鹤飞天。点石成金,妙不可言。"),
)The blendMode must be BlendMode.srcATop for text coloring, and shaderCallback provides the gradient mask based on the widget bounds.
Adjusting the start position : By creating a custom rectangle with Rect.fromLTWH , the gradient start can be shifted left:
LinearGradient(colors: [Colors.red, Colors.green])
.createShader(Rect.fromLTWH(-bounds.width, 0.0, bounds.width, bounds.height));This moves the gradient’s origin two widths to the left, making the visible part appear as a moving highlight.
Animation : To animate the highlight over a fixed duration (e.g., 3 seconds), a Tween combined with an AnimationController is used. The tween animates from 0 to bounds.width * 2 , and the animated value is fed into the Rect.fromLTWH start coordinate.
class _MyHomePageState extends State
with TickerProviderStateMixin {
late Animation
_animation;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 3000));
_animation = Tween
(begin: 0, end: 0).animate(_controller);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget? child) {
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (Rect bounds) {
_animation = Tween
(
begin: 0,
end: bounds.width * 2,
).animate(_controller);
return const LinearGradient(colors: [Colors.red, Colors.green])
.createShader(Rect.fromLTWH(
(_animation.value) - bounds.width, 0.0, bounds.width, bounds.height));
},
child: Text("要做神仙,驾鹤飞天。点石成金,妙不可言。"),
);
},
),
),
);
}
}Per‑character coloring : To highlight individual characters with different durations, the animation controller is recreated for each character. The start position is calculated as (bounds.width / lycs.length) * count and the end as the next segment, while the mask width remains 1 to create a thin moving highlight.
ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (Rect bounds) {
if (_animation.isCompleted) {
if (count < lycs.length) {
_controller.dispose();
_controller = AnimationController(
duration: Duration(milliseconds: lycs.elementAt(count)['duration'] as int),
vsync: this,
);
_animation = Tween(
begin: (bounds.width / lycs.length) * count,
end: (bounds.width / lycs.length) * (count + 1),
).animate(_controller);
_animation.addListener(() { setState(() {}); });
count++;
_controller.forward();
}
}
return const LinearGradient(colors: [Colors.red, Colors.green])
.createShader(Rect.fromLTWH(_animation.value, 0.0, 1, bounds.height));
},
child: Text("要做神仙驾鹤飞天"),
)Progress control : To synchronize lyric highlighting with external playback progress, the total lyric time, current playback time, and lyric start/end times are used to compute a ratio, which is then applied to the animation controller’s value.
The article concludes with a note about a potential performance issue: updating _animation inside shaderCallback may cause rapid re‑invocations and should be reviewed.
Overall, the tutorial provides a complete walkthrough—from visual analysis to gradient tricks, animation setup, per‑character timing, and progress synchronization—for achieving a karaoke‑style lyric highlight in Flutter.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.