去年妇联4上映后,谷歌迅速推出了一个彩蛋,以致敬妇联计生办主任-灭霸。
鉴于新冠疫情在国外的爆发,国家为了保障我们的安全,限制了大部分危险的通道,我冒死替大家搬来了这个彩蛋。
其实这个彩蛋早就被大家玩坏了,看了各路大神的实现方式,心中也就有了思路。下面就开始在Flutter中实现这个效果。
实现思路
这个彩蛋本质上就是一个动画,而要实现一个动画效果,首先要做的就是拆解,然后在简单的效果上丰富元素。
问:灭霸实现他的计划需要几步?
答:三步。1.戴上手套 2.打个响指 3.看特效
问:那么在Flutter中实现这个效果需要几步呢?
答:也是三步。1.图像化 2.分离像素 3.看动画
图像化 就是将范围内的Widget
转换为一个Image
对象,可以理解为截图。
分离像素 这是最为关键的一步,也是较为复杂的一步。
要理解这一步,你需要静下心,我帮你好好捋一捋。
心中默默回答以下几个问题:
-
今年几岁了?
-
工作多少年了?
-
手头存款有多少?
-
没有女朋友的你,钱都去哪儿了?
是的,多年的积蓄,听了个响儿,就烟消云散不知所踪了。游戏充值?吃吃喝喝?数码设备?打赏主播?会员费?
你懂了吗?你懂了吧!
多年的积蓄,变成了一笔笔的支出,纳入种种消费类型,随着时间的推移慢慢遗忘,遗忘。
把第一步生成的图像比作多年的积蓄,生活中的每一笔支出就对应了图像上的每一个像素点,而消费类型是一个个重叠的空白透明图层。把图像上的每个像素点,随机分配到这些图层上,然后将这些图层慢慢向不同方向抽离,淡化,就消失了,消失了。
用一张图强化一下理解:
开始造
图像化
Flutter提供了一个组件RepaintBoundary
,通过toImage()
方法可以将包裹的child截图生成一个ui.Image
对象。但是这个Image
对象无法获取到像素点,所以要将其转换为image.Image
对象。
// 手动导入一下iamge包
import 'package:image/image.dart' as image;
// 将一个Widget转为image.Image对象
Future<image.Image> _getImageFromWidget() async {
// _globalKey为需要图像化的widget的key
RenderRepaintBoundary boundary = _globalKey.currentContext.findRenderObject();
// ui.Image => image.Image
var img = await boundary.toImage();
var byteData = await img.toByteData(format: ImageByteFormat.png);
var pngBytes = byteData.buffer.asUint8List();
return image.decodeImage(pngBytes);
}
复制代码
分离像素
首先我们要定义几个最重要的参数,以及初始化操作
class Sandable extends StatefulWidget {
// 将需要沙化的内容包裹起来
final Widget child;
// 吹散动画的时间
final Duration duration;
// 图层数量 图层越多,吹散效果越好但是更耗时
final int numberOfLayers;
Sandable(
{Key key,
@required this.child,
this.duration = const Duration(seconds: 3),
this.numberOfLayers = 10})
: super(key: key);
@override
_SandableState createState() => _SandableState();
}
class _SandableState extends State<Sandable> with TickerProviderStateMixin{
// 吹散动画Controller
AnimationController _mainController;
// key of child
GlobalKey _globalKey = GlobalKey();
// 重叠的分离图层
List<Widget> layers = [];
@override
void initState() {
super.initState();
_mainController =
AnimationController(vsync: this, duration: widget.duration);
}
@override
void dispose() {
_mainController.dispose();
super.dispose();
}
...
}
复制代码
build
方法中的布局非常简单,只需要一个Stack
布局,两部分内容:child
和layers
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
...layers, // 沙化图层
// 可点击的child 用RepaintBoundary包裹以截图
GestureDetector(
onTap: () {
blow();
},
// 当动画开始 本体隐藏
child: _mainController.isAnimating
? Container()
: RepaintBoundary(
key: _globalKey,
child: widget.child,
),
)
],
);
}
复制代码
blow
方法就是最核心的方法了。不废话,直接上代码:
Future<void> blow() async {
// 获取到完整的图像
image.Image fullImage = await _getImageFromWidget();
// 获取原图的宽高
int width = fullImage.width;
int height = fullImage.height;
// 初始化与原图相同大小的空白的图层
List<image.Image> blankLayers =
List.generate(widget.numberOfLayers, (i) => image.Image(width, height));
// 将原图的像素点,分布到layer中
separatePixels(blankLayers, fullImage, width, height);
// 将图层转换为Widget
layers = blankLayers.map((layer) => imageToWidget(layer)).toList();
// 刷新页面
setState(() {});
// 开始动画
_mainController.forward();
}
void separatePixels(List<image.Image> blankLayers, image.Image fullImage,
int width, int height) {
// 遍历所有的像素点
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
// 获取当前的像素点
int pixel = fullImage.getPixel(x, y);
// 如果当前像素点是透明的 则直接continue 减少不必要的浪费
if (0 == pixel) continue;
// 随机生成放入的图层index
int index = Random().nextInt(widget.numberOfLayers);
// 将像素点放入图层
blankLayers[index].setPixel(x, y, pixel);
}
}
}
复制代码
是不是并不复杂!
看动画
动画就是三板斧:控制器AnimationController
+动画过程Curve
+插值Tween
。
在这个效果中,图层有随机的位移动画和渐隐动画,当然也可以加上一丢丢的旋转,可是我懒呀
Widget imageToWidget(image.Image png) {
// 先将image 转换为 Uint8List 格式
Uint8List data = Uint8List.fromList(image.encodePng(png));
// 定义一个先快后慢的动画过程曲线
CurvedAnimation animation = CurvedAnimation(
parent: _mainController, curve: Interval(0, 1, curve: Curves.easeOut));
// 定义位移变化的插值(始末偏移量)
Animation<Offset> offsetAnimation = Tween<Offset>(
begin: Offset.zero,
// 基础偏移量+随机偏移量
end: Offset(50, -20) +
Offset(30, 30).scale((Random().nextDouble() - 0.5) * 2,
(Random().nextDouble() - 0.5) * 2),
).animate(animation);
return AnimatedBuilder(
animation: _mainController,
child: Image.memory(data),
builder: (context, child) {
// 位移动画
return Transform.translate(
offset: offsetAnimation.value,
// 渐隐动画
child: Opacity(
opacity: cos(animation.value * pi / 2), // 1 => 0
child: child,
),
);
},
);
}
复制代码
然后,然后就没有了。。。
跑起来
赶紧写个demo试一下效果!使用非常简单,只需要将需要消灭的控件包裹起来就可以了。
Sandable(
duration: ...,
numOfLayers: ...,
child: ...,
)
复制代码
结语
总的来说,在Flutter中简单实现这个效果还是比较轻松的,有清晰的思路,不需要复杂的计算就可以完成。
掉根头发,掌握这项技能,它可香?