作为酷狗音乐的忠实运用者,一直都觉得酷狗App的UI交互很酷炫,在业界中当属前列。这次我研究上了他的歌词翻滚作用,逐字变色假如用Flutter来完成,应该是个很风趣的工作。

作用图

Flutter完成酷狗歌词逐字上色的作用

动效分析

研究酷狗的歌词翻滚作用可以发现:

  • 歌词是逐字变色的
  • 针对单个字,是依据单个歌词的演唱耗时来上色

开发思路

  1. 首先需求清晰的是每行代表一句歌词,这句歌词播映的总时长是固定的,然后每个字播映的时长也是固定的。每个字上色的动画连接起来,便是这一句歌词的总体上色动画。因而咱们把颗粒度详尽到“字”上
  2. 歌词咱们运用文本烘托即可,上色的作用咱们可以用文字的前景色特点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,我会继续往下完成更多酷炫的作用,当然也不止于酷狗~