作为酷狗音乐的忠实运用者,一直都觉得酷狗App的UI交互很酷炫,在业界中当属前列。这次我研究上了他的歌词翻滚作用,逐字变色假如用Flutter来完成,应该是个很风趣的工作。
作用图
动效分析
研究酷狗的歌词翻滚作用可以发现:
- 歌词是逐字变色的;
- 针对单个字,是依据单个歌词的演唱耗时来上色;
开发思路
- 首先需求清晰的是每行代表一句歌词,这句歌词播映的总时长是固定的,然后每个字播映的时长也是固定的。每个字上色的动画连接起来,便是这一句歌词的总体上色动画。因而咱们把颗粒度详尽到“字”上。
- 歌词咱们运用文本烘托即可,上色的作用咱们可以用文字的前景色特点
foreground
来叠加渐变,从而完成一段文字不同颜色的作用。
完成计划
1. mock数据
常规的开发第一步:界说好数据模型。这儿我结构的数据特别简略,只服务于完成逐字上色的作用。
class Model {
int totalTime = 12284; // 歌词总时长
String content = '我要去看那最远的地方,和你手舞足蹈聊梦想'; // 歌词内容
List<Word> lyric = [
Word('我', 500),
Word('要', 500),
Word('去', 400),
Word('看', 280),
Word('那', 3320),
Word('最', 310),
Word('远', 390),
Word('的', 420),
Word('地', 400),
Word('方', 380),
Word(',', 700),
Word('和', 620),
Word('你', 580),
Word('手', 340),
Word('舞', 356),
Word('足', 356),
Word('蹈', 420),
Word('聊', 580),
Word('梦', 552),
Word('想', 780),
];
}
class Word {
final String text;
final int duration;
Word(this.text, this.duration);
}
2. 布局烘托
要完成逐字上色作用,关键知识点是:利用TextStyle的foreground
特点,界说一个渐变,根据渐变创建着色器Shader
杂乱给foreground
即可。
Gradient gradient = const LinearGradient(colors: [Colors.red, Colors.grey]);
// ......
Text(
key: _key,
currLyric.content,
style: TextStyle(
fontSize: 16.0,
foreground: Paint()
..shader = gradient.createShader(
Rect.fromLTWH(
animation.value,
textOffset.dy,
textOffset.dx,
textSize.height,
),
textDirection: TextDirection.ltr,
),
),
),
3. 单字上色动画(附上完整代码)
- 要完成单字上色的动画,需求先了解createShader中的Rect特点,根据传入的左上坐标点,绘制一个矩形,矩形巨细也是咱们传进去的;
- 涉及到方位和巨细,就需求先拿到这句歌词的烘托信息,这儿咱们很自然的运用
GlobalKey
,通过RenderObject
获取烘托信息;
- 每行歌词的长度是固定的,每个字的宽度也是固定的。因而咱们只要在单字的播映时间内,着色器烘托的起始点从单字前的方位,陡峭的移动到单字后的方位即可。 这儿就现已给出了Animation和Controller所需求的变量了。
class Lyric extends StatefulWidget {
const Lyric({Key? key}) : super(key: key);
@override
State<Lyric> createState() => _LyricState();
}
class _LyricState extends State<Lyric> with TickerProviderStateMixin {
Gradient gradient = const LinearGradient(colors: [Colors.red, Colors.grey]);
Model currLyric = Model();
final GlobalKey _key = GlobalKey();
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(milliseconds: currLyric.lyric[count].duration),
vsync: this,
);
animation = Tween(
begin: 0.0,
end: 0.0,
).animate(controller);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('歌词动效'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 0),
child: AnimatedBuilder(
animation: animation,
builder: (_, __) => Text(
key: _key,
currLyric.content,
style: TextStyle(
fontSize: 16.0,
foreground: Paint()
..shader = gradient.createShader(
Rect.fromLTWH(
animation.value,
textOffset.dy,
textOffset.dx,
textSize.height,
),
textDirection: TextDirection.ltr,
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _starPlay,
tooltip: 'Increment',
child: const Icon(Icons.play_circle_outline),
), // T
);
}
int count = 0;
Size textSize = Size.zero;
Offset textOffset = Offset.zero;
_starPlay() {
if (_key.currentContext != null) {
RenderBox renderBox =
_key.currentContext!.findRenderObject() as RenderBox;
// offset.dx , offset.dy 便是控件的左上角坐标
textOffset = renderBox.localToGlobal(Offset.zero);
textSize = renderBox.size; // 获取size
debugPrint(
'offset=$textOffset, size=$textSize, width=${MediaQuery.of(context).size.width}');
singlePlay();
}
}
singlePlay() async {
debugPrint('当前播映歌词的时长=${currLyric.lyric[count].duration}');
controller = AnimationController(
duration: Duration(milliseconds: currLyric.lyric[count].duration),
vsync: this,
);
double width = textSize.width + textOffset.dx;
animation = Tween(
begin: width * (count / currLyric.lyric.length),
end: width * ((count + 1) / currLyric.lyric.length),
).animate(controller);
animation.addListener(() {
setState(() {});
});
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
debugPrint('播映完第${count + 1}个歌词');
count++;
controller.dispose();
if (count < currLyric.lyric.length) {
singlePlay();
} else {
count = 0;
}
}
});
controller.forward();
}
}
写在最后
至此,歌词逐字上色的作用就编写完啦。总体是很简略的,但是作用拆解完的细节也不少。
对于酷狗的作用,此前我也曾写过酷狗的 流畅Tabbar,我会继续往下完成更多酷炫的作用,当然也不止于酷狗~