likes
comments
collection
share

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

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

如下图所示,最近通过群友的问题在 codepen.io 上看到了一个文本「抽动」的动画实现,看起来就像是生活中常见的「霓虹灯招牌」故障时的「抽动」效果,而本篇的目标通过「抄袭」这个实现,帮助大家理解 Flutter 里的一些实现小技巧。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

这个效果在 codepen 上是通过 CSS 实现的,实现思路 codepen 上的 Glitch Walkthrough 大致有提示,但是 Flutter 没有强大的 CSS,那么如何将它「复刻」到 Flutter 上就是本篇的核心要点。

不得不说 CSS 很强大,要在 Flutter 上实现类似的效果还是比较「折腾」。

而要在 Flutter 上实现类似 Glitch Walkthrough 的效果,大致上我们需要处理:

  • 类似霓虹灯效果的文本
  • 文本内容撕裂的效果
  • 文本变形闪动的效果

那么接下来我们就按照这个流程来实现一个 Flutter 上的 Glitch Walkthrough 。

霓虹灯文本

这一步其实相对简单,Flutter 的 TextStyle 提供了 shadows 配置,通过它可以快速实现一个「会发光」的文本。

我们这里通过两个 Shadow 来实现「发光」的视觉效果,核心就是利用 ShadowblurRadius 来让背景出现一定程度的模糊发散,然后两个 Shadow 形成不一样的颜色深度和发散效果,从而达到看起来「发亮」的效果。

如下图是没有填充文本颜色时 Shadow 的效果。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

最后,如下代码所示,我们只需要通过 foreground 给文本补充下颜色,就可以看到如下图所示的类似「霓虹灯」效果的文本。

当然这里你不想用 foreground ,只用简单的 color 也可以。

Text(
 widget.text,
 styleTextStyle(
   fontSize48,
   fontWeight: FontWeight.bold,
   foregroundPaint()
    ..style = PaintingStyle.fill
    ..strokeWidth = 5
    ..color = Colors.white,
   shadows: [
     Shadow(
       blurRadius10,
       color: Colors.white,
       offsetOffset(00),
    ),
     Shadow(
       blurRadius20,
       color: Colors.white30,
       offsetOffset(00),
    ),
  ],
),
)

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

这里提个题外话,其实类似的思路用在图片上也可以实现「发光」的效果,如下代码所示,通过 Stack 嵌套两个 Image ,然后中间通过 BackdropFilterImageFilter 做一层模糊,让底下的图片模糊后发散产生类似「发光」的效果。

var child = Image.asset(
  'static/test_logo.png',
  width250,
);
return Stack(
     children: [
       child,
       Positioned.fill(
         childBackdropFilter(
           filter: ImageFilter.blur(
             sigmaX: blurRadius,
             sigmaY: blurRadius,
          ),
           childContainer(color: Colors.transparent),
        ),
      ),
       child,
    ],
  )
);

如下图所示,图片最终可以通过自己的色彩产生类似「发光」的效果,当然这部分只是额外的拓展内容,和我们要实现的效果无关。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

文本撕裂

这部分可以说是需求效果的核心,这里我们需要用到 ClipPathPolygon ,通过 Polygon 来实现随机的多边形路径,然后利用 ClipPath 对文本内容进行随机的路径裁剪。

虽然说用 Polygon , 但是 Flutter 官方并没有直接提供类似前端 CSS 的 Polygon 多边形 API 支持,但是社区总有「好心人」,我们可以直接使用 Flutter 上类似的第三方库: polygon: ^0.1.0

简单说 Polygon 就是按照 step 对 PathmoveToquadraticBezierTo 等 API 进行了封装。

Flutter 上的 Polygon 取值范围是 -1 ~ 1 ,也就是按照比例决定位置,比如 - 1 就是起始点, 1 就是最大宽高, 更具体如下面的代码所示,这里利用 Polygon 添加了三个点,最终这三个点形成的 Path 会绘制出一个三角形。

List<Offset> generatePoint() {
 List<Offset> points = [];
 points.add(Offset(-1, -1));
 points.add(Offset(-1, 0));
 points.add(Offset(0, -1));
 return points;
}

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

如下代码所示,那如果如果 point 的数量多了,就可以形成一系列不规则的形状,比如下面代码随机添加了 60 个点的位置,可以看到此时屏幕上的白色 Container 被裁剪成「凌乱」的形状。

List<Offset> generatePoint() {
 List<Offset> points = [];

 points.add(Offset(-1.00, -0.76));
 points.add(Offset(0.06, -0.76));
 points.add(Offset(0.06, -0.48));
 points.add(Offset(-0.50, -0.48));
 points.add(Offset(-0.50, 0.72));
 points.add(Offset(-0.38, 0.72));
 points.add(Offset(-0.38, -1.00));
 points.add(Offset(0.06, -1.00));
 points.add(Offset(0.06, 0.67));
 points.add(Offset(0.84, 0.67));
 points.add(Offset(0.84, 0.63));
 points.add(Offset(0.39, 0.63));
 points.add(Offset(0.39, -0.42));
 points.add(Offset(0.56, -0.42));
 points.add(Offset(0.56, 0.30));
 points.add(Offset(0.37, 0.30));
 points.add(Offset(0.37, 0.32));
 points.add(Offset(0.54, 0.32));
 points.add(Offset(0.54, -0.09));
 points.add(Offset(0.70, -0.09));
 points.add(Offset(0.70, -0.48));
 points.add(Offset(0.94, -0.48));
 points.add(Offset(0.94, -0.43));
 points.add(Offset(0.67, -0.43));
 points.add(Offset(0.67, -0.31));
 points.add(Offset(0.08, -0.31));
 points.add(Offset(0.08, 0.78));
 points.add(Offset(-0.40, 0.78));
 points.add(Offset(-0.40, 0.15));
 points.add(Offset(0.65, 0.15));
 points.add(Offset(0.65, 0.00));
 points.add(Offset(0.36, 0.00));
 points.add(Offset(0.36, -0.28));
 points.add(Offset(0.24, -0.28));
 points.add(Offset(0.24, -0.80));
 points.add(Offset(-0.76, -0.80));
 points.add(Offset(-0.76, -0.31));
 points.add(Offset(0.19, -0.31));
 points.add(Offset(0.19, 0.13));
 points.add(Offset(0.96, 0.13));
 points.add(Offset(0.96, 0.65));
 points.add(Offset(-0.80, 0.65));
 points.add(Offset(-0.80, 0.06));
 points.add(Offset(0.82, 0.06));
 points.add(Offset(0.82, 0.67));
 points.add(Offset(0.60, 0.67));
 points.add(Offset(0.60, 0.65));
 points.add(Offset(-0.19, 0.65));
 return points;
}

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

如果这时候把白色 Container 换成文本内容,那么我们就可以如下图所示的效果,看起来像不像一帧状态下文本的「错乱」效果?后面我们只需要每次生成一帧这样的 Path ,就可以实现文本动态「撕裂」的需求。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

我们只需要把这个实现做成随机输出,然后每次生成一个 Path 就可以了。

如下代码所示,我们通过 generatePoint 方法,每次随机生成 60 个点,然后将这些点通过 computePath 转化为 Path,然后继承 CustomClipper 配置到 getClip 方法里,在需要的时候(tear )对 child 按 Path 进行裁剪。

注意这里的 i % 2 ,为的是让上次的 x 或者 y 可以是同一个位置,在连接上能连续。

class RandomTearingClipper extends CustomClipper<Path> {
 bool tear;

 RandomTearingClipper(this.tear);

 List<Offset> generatePoint() {
   List<Offset> points = [];
   var x = -1.0;
   var y = -1.0;
   for (var i = 0; i < 60; i++) {
     if (i % 2 != 0) {
       x = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
    } else {
       y = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
    }
     points.add(Offset(x, y));
  }
   return points;
}

 @override
 Path getClip(Size size) {
   var points = generatePoint();
   var polygon = Polygon(points);
   if (tear)
     return polygon.computePath(rect: Offset.zero & size);
   else
     return Path()..addRect(Offset.zero & size);
}

 @override
 bool shouldReclip(RandomTearingClipper oldClipper) => true;
}

接着,我们只需要设置一个定期器,然后将前面的「霓虹灯文本」和「故障裁剪效果」配置到 ClipPath 上,如下图所示,我们就可以看到文本的随机撕裂效果。

timer = Timer.periodic(Duration(milliseconds400), (timer) {
 tearFunction();
});

return ClipPath(
  childCenter(
    childText(
      widget.text,
      styleTextStyle(
        fontSize48,
        fontWeight: FontWeight.bold,
        foregroundPaint()
          ..style = PaintingStyle.fill
          ..strokeWidth = 1
          ..color = Colors.white,
        shadows: [
          Shadow(
            blurRadius10,
            color: Colors.white,
            offsetOffset(00),
          ),
          Shadow(
            blurRadius20,
            color: Colors.white30,
            offsetOffset(00),
          ),
        ],
      ),
    ),
  ),
  clipperRandomTearingClipper(tear),
);

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

此时看起来还不够形象。

变形闪动

为了达到我们预期的效果,最后我们还需要做一些特殊处理,比如再实现两个形状、颜色和位置不一样「霓虹灯文本」,为的就是实现「变形和闪动」的效果替换。

比如如下代码所示,通过 ShaderMask 可以实现一个渐变效果的的文本,这是用来在闪动的时候,提供一个短暂替换和色彩加深的作用。

ShaderMask(
 blendMode: BlendMode.srcATop,
 shaderCallback: (bounds) {
   return LinearGradient(
     colors: [Colors.blue, Colors.green, Colors.red],
     stops: [0.00.51.0],
  ).createShader(bounds);
},
 child:

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

类似的我们还可以实现一个「变形」的文本,在之前的白色「霓虹灯」文本基础上增加「斜体」和「颜色变淡」等处理,用来闪动的时候提供「变形」的作用。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

最后我们再将之前的 ClipPath添加到它们上面,并增加一个 transform 实现文本四周随意移动的效果支持,如下图所示,此时的效果已经肉眼可见的接近我们的需求。

transform:
   Matrix4.translationValues(randomPosition(4), randomPosition(4), 0),

double randomPosition(position) {
 return Random().nextInt(position).toDouble() *
    (Random().nextBool() ? -1 1);
}
Flutter 小技巧之霓虹灯文本的「故障」效果的实现Flutter 小技巧之霓虹灯文本的「故障」效果的实现

最后我们将这几个文本效果用 Stack 组合起来,然后再在定时器里不停去切换「故障」和「正常」的文本状态,并且随机选择展示不同的 「故障」状态。

timer = Timer.periodic(Duration(milliseconds: 400), (timer) {
  tearFunction();
});
timer2 = Timer.periodic(Duration(milliseconds: 600), (timer) {
  tearFunction();
});

tearFunction() {
  count++;
  tear = count % 2 == 0;
  if (tear == true) {
    setState(() {});
    Future.delayed(Duration(milliseconds: 150), () {
      setState(() {
        tear = false;
      });
    });
  }
}

@override
Widget build(BuildContext context) {
  var status = Random().nextInt(3);
  return Stack(
    children: [
      if (tear && (status == 1)) renderTearText1(RandomTearingClipper(tear)),
      if (!tear || (tear && status != 2))
        renderMainText(RandomTearingClipper(tear)),
      if (tear && status == 2) renderTearText2(RandomTearingClipper(tear)),
    ],
  );
}

最终效果如下图所示,这里还额外对后面两个文本做了一个 ClipRect 处理,闪动切换的时候只展示部分内容,这样在「故障」时的切换不会显得太过生硬,可以看到简单的 CSS 效果在 Flutter 上的实现成本其实并不低。

Flutter 小技巧之霓虹灯文本的「故障」效果的实现

当然,这里的实现没考虑性能问题,所以代码也比较糙,不过这里主要是为了展示了 ClipPathShadow 的使用技巧,相信通过这个例子,可以帮助大家更好地发掘 Flutter 里对于路径绘制和阴影的使用场景,这才是本篇的主要目的。

那么本篇小技巧到这里就结束了,如果你还有什么想说的,欢迎留言评论。

完整代码可见:github.com/CarGuo/gsy_…