GSYFlutterBook/Flutter-N21.md

347 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Flutter 小技巧之霓虹灯文本的「故障」效果的实现
如下图所示,最近通过群友的问题在 [codepen.io ](https://codepen.io/mattgrosswork/pen/VwprebG) 上看到了一个文本「抽动」的动画实现,看起来就像是生活中常见的「霓虹灯招牌」故障时的「抽动」效果,而本篇的目标通过「抄袭」这个实现,帮助大家理解 Flutter 里的一些实现小技巧。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image1.gif)
这个效果在 codepen 上是通过 CSS 实现的,实现思路 codepen 上的 [Glitch Walkthrough](https://codepen.io/mattgrosswork/pen/VwprebG) 大致有提示,但是 Flutter 没有强大的 CSS那么如何将它「复刻」到 Flutter 上就是本篇的核心要点。
> 不得不说 CSS 很强大,要在 Flutter 上实现类似的效果还是比较「折腾」。
而要在 Flutter 上实现类似 Glitch Walkthrough 的效果,大致上我们需要处理:
- 类似霓虹灯效果的文本
- 文本内容撕裂的效果
- 文本变形闪动的效果
那么接下来我们就按照这个流程来实现一个 Flutter 上的 Glitch Walkthrough 。
# 霓虹灯文本
这一步其实相对简单Flutter 的 `TextStyle` 提供了 `shadows` 配置,通过它可以快速实现一个「会发光」的文本。
我们这里通过两个 `Shadow` 来实现「发光」的视觉效果,核心就是利用 `Shadow``blurRadius` 来让背景出现一定程度的模糊发散,然后两个 `Shadow` 形成不一样的颜色深度和发散效果,从而达到看起来「发亮」的效果。
> 如下图是没有填充文本颜色时 `Shadow` 的效果。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image2.png)
最后,如下代码所示,我们只需要通过 `foreground` 给文本补充下颜色,就可以看到如下图所示的类似「霓虹灯」效果的文本。
> 当然这里你不想用 `foreground` ,只用简单的 `color` 也可以。
```dart
Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 5
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
)
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image3.png)
这里提个题外话,其实类似的思路用在图片上也可以实现「发光」的效果,如下代码所示,通过 Stack 嵌套两个 `Image` ,然后中间通过 `BackdropFilter``ImageFilter` 做一层模糊,让底下的图片模糊后发散产生类似「发光」的效果。
```dart
var child = Image.asset(
'static/test_logo.png',
width: 250,
);
return Stack(
children: [
child,
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurRadius,
sigmaY: blurRadius,
),
child: Container(color: Colors.transparent),
),
),
child,
],
)
);
```
如下图所示,图片最终可以通过自己的色彩产生类似「发光」的效果,当然这部分只是额外的拓展内容,和我们要实现的效果无关。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image4.png)
# 文本撕裂
这部分可以说是需求效果的核心,这里我们需要用到 `ClipPath``Polygon` ,通过 `Polygon` 来实现随机的多边形路径,然后利用 `ClipPath` 对文本内容进行随机的路径裁剪。
虽然说用 `Polygon` 但是 Flutter 官方并没有直接提供类似前端 CSS 的 `Polygon` 多边形 API 支持,但是社区总有「好心人」,我们可以直接使用 Flutter 上类似的第三方库: `polygon: ^0.1.0`
>简单说 `Polygon` 就是按照 step 对 `Path``moveTo``quadraticBezierTo` 等 API 进行了封装。
Flutter 上的 `Polygon` 取值范围是 -1 1 ,也就是按照比例决定位置,比如 - 1 就是起始点, 1 就是最大宽高, 更具体如下面的代码所示,这里利用 `Polygon` 添加了三个点,最终这三个点形成的 Path 会绘制出一个三角形。
```dart
List<Offset> generatePoint() {
List<Offset> points = [];
points.add(Offset(-1, -1));
points.add(Offset(-1, 0));
points.add(Offset(0, -1));
return points;
}
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image5.png)
如下代码所示,那如果如果 point 的数量多了,就可以形成一系列不规则的形状,比如下面代码随机添加了 60 个点的位置,可以看到此时屏幕上的白色 `Container` 被裁剪成「凌乱」的形状。
```dart
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;
}
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image6.png)
如果这时候把白色 `Container` 换成文本内容,那么我们就可以如下图所示的效果,看起来像不像一帧状态下文本的「错乱」效果?后面我们只需要每次生成一帧这样的 Path ,就可以实现文本动态「撕裂」的需求。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image7.png)
> 我们只需要把这个实现做成随机输出,然后每次生成一个 `Path` 就可以了。
如下代码所示,我们通过 `generatePoint` 方法,每次随机生成 60 个点,然后将这些点通过 `computePath` 转化为 Path然后继承 `CustomClipper` 配置到 `getClip` 方法里,在需要的时候(`tear` )对 child 按 Path 进行裁剪。
> 注意这里的 `i % 2` ,为的是让上次的 x 或者 y 可以是同一个位置,在连接上能连续。
```dart
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` 上,如下图所示,我们就可以看到文本的随机撕裂效果。
```dart
timer = Timer.periodic(Duration(milliseconds: 400), (timer) {
tearFunction();
});
return ClipPath(
child: Center(
child: Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
),
),
clipper: RandomTearingClipper(tear),
);
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image8.gif)
> 此时看起来还不够形象。
# 变形闪动
为了达到我们预期的效果,最后我们还需要做一些特殊处理,比如再实现两个形状、颜色和位置不一样「霓虹灯文本」,为的就是实现「变形和闪动」的效果替换。
比如如下代码所示,通过 `ShaderMask` 可以实现一个渐变效果的的文本,这是用来在闪动的时候,提供一个短暂替换和色彩加深的作用。
```dart
ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return LinearGradient(
colors: [Colors.blue, Colors.green, Colors.red],
stops: [0.0, 0.5, 1.0],
).createShader(bounds);
},
child:
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image9.png)
类似的我们还可以实现一个「变形」的文本,在之前的白色「霓虹灯」文本基础上增加「斜体」和「颜色变淡」等处理,用来闪动的时候提供「变形」的作用。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image10.png)
最后我们再将之前的 ` ClipPath`添加到它们上面,并增加一个 `transform` 实现文本四周随意移动的效果支持,如下图所示,此时的效果已经肉眼可见的接近我们的需求。
```dart
transform:
Matrix4.translationValues(randomPosition(4), randomPosition(4), 0),
double randomPosition(position) {
return Random().nextInt(position).toDouble() *
(Random().nextBool() ? -1 : 1);
}
```
| ![](http://img.cdn.guoshuyu.cn/20230322_N21/image11.gif) | ![](http://img.cdn.guoshuyu.cn/20230322_N21/image12.gif) |
| -------------------------------------------------------- | -------------------------------------------------------- |
最后我们将这几个文本效果用 `Stack` 组合起来,然后再在定时器里不停去切换「故障」和「正常」的文本状态,并且随机选择展示不同的 「故障」状态。
```dart
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 上的实现成本其实并不低。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image13.gif)
当然,这里的实现没考虑性能问题,所以代码也比较糙,不过这里主要是为了展示了 `ClipPath ``Shadow` 的使用技巧,相信通过这个例子,可以帮助大家更好地发掘 Flutter 里对于路径绘制和阴影的使用场景,这才是本篇的主要目的。
那么本篇小技巧到这里就结束了,如果你还有什么想说的,欢迎留言评论。
> 完整代码可见https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/tear_text_demo_page.dart