likes
comments
collection
share

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

作者站长头像
站长
· 阅读数 14

👋前言

上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出之前,我们先来一点前瞻,分享一下在这段时间开发中遇到的问题。

因为最近原生那边在学习车载APP通信和多屏幕开发的内容,也导致了这个APP的开发进展缓慢,等回头基本功能完善后再分享整个开发过程。

👁️效果分析

在开始之前我们先考虑实现单行的歌词动画,看看这个,有意思的是这种上色并不是一个字一个字的变色,而是多个字体颜色缓缓改变,就像是其他播放器那样,可能看完这个效果大家已经猜到实现方法啦?

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

这种情况一次直接“点亮”一个字是不太合理的,我们得考虑对一个行,或者一个字进行渐变,从而达到这个效果。

❓问题

正常情况下,线性渐变是如下图情况:

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

那对应的字体就是:

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

这显然不是我们想要的,看看前面的歌词效果,我们发现,歌词默认状态下全部是灰色,播放了的部分才有渐变效果。

💡巧妙思路

如果大家用PS会发现,这个渐变的起始位置可以改变的,我们只需要让开始渐变的位置变得更远,大概是原来范围的2倍,就可以让所有的部分都是绿了,听上去有一些抽象?我们看看下面的图。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

啊哈,这太棒了,看上去我们达到目的了,接下来我们利用这个方法显示渐变上色!

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

怎么样!看上去是不是就像是我们的歌词上色啦?

🔧开发实现

渐变实现

ShaderMask可以对我们的组件设置着色,灵活度很高,不过今天我们不详细看它,如果你希望了解这控件更多,可以先看看其他文章。

让我们先看看简单的例子:

ShaderMask(
  blendMode: BlendMode.srcATop,
  shaderCallback: (Rect bounds) {
  return LinearGradient(colors: [
    Colors.red,
    Colors.green
  ],).createShader(bounds);
},
  child: Text("要做神仙,驾鹤飞天。点石成金,妙不可言。"),
)

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

要注意的是blendMode是着色模式,如果你需要为文字着色,那么就选择BlendMode.srcATopshaderCallback则是着色蒙版,比如这里回调函数传回的bounds就是子ShaderMask包裹子组件的矩形区域。

重点在于这个shaderCallback,我们创建了一个线性渐变LinearGradient,默认情况下它就是垂直居中且自左向右渐变的,通过createShader我们就创建了一个以bounds为区域且带有渐变的蒙版,最后应用到组件上就是这样的啦。

设置渐变起始位置

从前面的gif动画示例来看,我们需要调整渐变的起始位置,在这里我们就相当于修改蒙版区域了,这里我们使用Rect.fromLTW来创建渐变区域,例如:

LinearGradient(colors: [
  Colors.red,
  Colors.green
],).createShader(Rect.fromLTWH(-bounds.width, 0.0, bounds.width, bounds.height));

我们修改下刚刚的代码,我们自定义一个矩形区域,Rect.fromLTWH接收四个参数,分别是左上角的X左边,左上角Y坐标,矩形宽度,矩形高度,这个坐标是相当于父容器而言的。

我们这里定义左上角坐标是(-bounds.width, 0.0),然后这个矩形的宽高和父容器一样的,现在我们看看效果。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

哦?居然是纯绿色了?我们还是回忆前面的gif。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

就像是这样,相当于我们修改了渐变的开始位置,但是由于宽度没变就那么点,所以相当于渐变的结束位置也跟着改变了,上面之所以是-bounds.width,就是为了让起始位置的x坐标是在文字的更左边,这样我们通过修改蒙版左上角X的坐标就能实现文章开篇的效果啦。

如果你不熟悉Rect就看下文:

Rect class - dart:ui library - Dart API (flutter.dev)

动画实现

现在我们试试看加上动画,我们假设这句歌词花费了3秒,那么我们就需要一个线性的动画,在3秒中变化起始位置~

我们需要让起始的x坐标从-bounds.width变化到bounds.width,这样才能做到前面gif的效果。

这里我们使用Tween配合AnimationController来完成,不过Tween的变化区间必须是正数,所以我们只能让它从0变化到bounds.width * 2,在Rect.fromLTWH中我们减去bounds.width就可以了。

这里是一个完整的代码,你可以参考这个内容。

class _MyHomePageState extends State<MyHomePage>
    with TickerProviderStateMixin {

  late Animation<double> _animation;
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 3000));

    _animation = Tween<double>(
      begin: 0,
      end: 0, // 使用 0 到 1 的范围
    ).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<double>(
                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("要做神仙,驾鹤飞天。点石成金,妙不可言。"),
          );
        }, )

      ),
    );
  }
}

我们需要重点关注的是shaderCallback函数中对_animation的赋值,因为我们设置动画的结束值是这个文本的宽度,但是我们其实是没办法在外面直接拿到的,这个时候大家可能想用GobalKeyBuildr的context来拿,但是GobalKey是昂贵的,我们看看有没有其他方案,我突然想到bounds不就是有文本控件的宽度吗?

那我们直接写在shaderCallback回调里,设置_animation,不过由于AnimatedBuilderBlendMode的外面,我们必须得在init里先初始化它,不然下面就出错啦。

这是我目前的方案了,但是现在仍然有问题,由于动画更新,会导致_animation的赋值一直执行,也许可以考虑下其他的方案或者加个标志变量进行限制?其他佬要是有其他想法还请在评论区告诉我。

现在,我们看看效果如何:

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

可能有人觉得这个渐变让正在播放的字不太明显,对比太弱了,尽管看上去过渡很平滑,接下来我们看看怎么优化。

🦄效果进阶

让我们看看这个,现在,我们将渐变蒙版的宽度调小一些,小到几乎看不见渐变的部分,这样我们发现两边都是纯色,这样的对比是相当明显的。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

这样的效果给到歌词上就有相当明显的对比啦。

在修改动画之前我们先理清楚这次效果会导致什么改变:

  • 起始位置变化了,不再是 -bounds.width 而是 -1
  • 结束位置变化了,不再是 bounds.width * 2 而是 bounds.width + 1
  • 渐变蒙版宽度变了,不再是 bounds.width 而是 1

不知道大家发现规律了吗?如果你希望修改渐变蒙版的范围,那么就得修改起始位置,起始位置和蒙板宽度是相反数关系,由于起始位置在文字以外,因此结束时得补上这个宽度,天呐,我感觉好久没用过这个词语了!😭😭😭

不过你仍然可以让它是0开始,bounds.width结束,这也不会有问题。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

最终的效果就是这样啦,看上去效果是挺不错的,但又缺少了前面渐变的那种感觉,这个就看大家选择啦。

_animation = Tween<double>(
  begin: 0,
  end: bounds.width,
).animate(_controller);

Rect.fromLTWH(_animation.value - 1, 0.0, 1, bounds.height)

这个是我们刚才代码的调整了,你可以试试看,让这个渐变的宽度再提升一些,比如20,那么在歌词的交界处也会有淡淡的渐变效果。

单字上色

对于单子上色而言,我们就需要额外的操作动画控制器了,就像是下面这样,我们先定义8个字,前四个字每个字都是500毫秒,后四个字每个字100毫秒,作为对照。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

接下来我们需要修改一下之前写的代码,将每个字的动画时间拆开,但是渲染进度是连贯的:

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("要做神仙驾鹤飞天"),
)

这里我们需要监听动画是否播放完了,假如播放完成的话,就判断是不是播放到了最后一个字,如果不是,那么我们就销毁之前的动画控制器。

然后重新设置动画控制器,lycs.elementAt(count)['duration']更新动画播放器的时间,相当于控制了每个字的播放时间。

现在起始的蒙版位置从(bounds.width / lycs.length) * count变化到(bounds.width / lycs.length) * (count + 1),蒙版宽度仍然是1,这样做的目的是确保渐变的进度是连贯起来的,我们确定每一个字的宽度,乘以目前上色字的索引就可以做到啦。

这里我们必须使用setState,否则动画控制器的变动无法被Flutter感知到。

此外,我们实际上是改变了控制器和动画对象本身的,我们每次都是new了一个新的对象进去,希望读者了解这点。

我们看看效果:

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

🎢进度管控

现在,我们来看一点点逻辑的问题,事实上,如果我们拖动播放进度,歌词也需要改变,而此时歌词的绘制进度需要进行控制,就像是现在的播放器软件那样。

我们这里的例子是针对于一整句歌词而言的,因此我们得明确这一句的歌词内容(lyric),我们的动画是匀速的,因此我们需要知道这句歌词经过的时间(totalTime),最后为了我们外部可以控制动画的进度,我们需要外部传入动画控制器(animationController),这样我们就可以写出下面的组件。

class SingleLineLyric extends StatefulWidget {
  final String lyric;
  final int totalTime;
  final AnimationController? animationController;

  const SingleLineLyric({
    Key? key,
    required this.lyric,
    required this.totalTime,
    this.animationController,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => _SingleLineLyric();
}

现在,我们看看如何确定animationController的动画进度,在播放器播放过程中,我们已知这些内容:

  • 正在播放的歌词内容
  • 已经播放的时间
  • 歌词出现和结束时间
  • 歌词总时间

那么这对于我们而言就相当好办了,我们需要在进度变动的回调函数里控制动画。

歌词结束时间 - 当前播放过的时间 = 歌词已经经过的时间

歌词已经经过的时间 / 歌词总时间 = 歌词已经经过的时间占歌词总时间比例

歌词已经经过的时间占歌词总时间比例 = 动画已经播放的进度

这样我们设置最后的结果给动画控制器就OK啦,当我们切换播放进度,歌词的渐变进度也会变化。

Flutter歌词渐变上色原理与实现 | 实验分享上一篇文章提及到我正在试着使用Flutter开发程序,在APP公开放出

这是我的例子,其中also参考上篇文章,这是模仿了Kotlin的内置函数。

由于篇幅问题,这里就不过多考虑其他问题了,回头我们再详细说进度管控。

😱遗留问题

事实上,本文只是带大家实现了这个歌词的效果,但是并没有考虑到代码在性能上的影响,我们在代码里做了一步危险的行为,那就是在shaderCallback中设置_animation。要知道,动画的值一旦更新shaderCallback就会被回调,然而动画的更新肯定是很快的,我不确定这是不是会出现问题,我不太懂这块,特别是在界面刷新上,希望有佬可以解答。

🥳文末

呼~终于结束啦,这是一篇长文,完整阅读不太轻松,但很高兴你读到这里了,现在让我们长松一口气!

希望这篇文章的内容能给大家一些思考,如果文章有错误的地方请在评论区告诉我。

参考文章

转载自:https://juejin.cn/post/7367620233140207627
评论
请登录