这是一个让widget粒子化的效果,文章借鉴自掘金的博客。
首先,大家一定要思考,如何去做才能实现这样一个效果,如何去实现最为简单。因为任何一个功能的实现方式一定是多样的,举一反三也是一件很有趣的事儿。
首先考虑,一个widget里面肯定有多个child,那么最优的做法,一定是把widget的画面直接截下来,这样就无需管理它里面子child的事儿了。有了截图之后,要让它粒子化,那么我是需要去获取它每个pixel的颜色,之后让它们进行无规则运动。那怎么样才能拆分他们呢?
答案是把他们放到不同的layer上,然后让这些layer载体,向各个方向进行运动,就可以模拟出四散的效果了。
所以步骤是这样的:
1.获取widget的截图
2.将截图上的像素点分配到不同的layer上
3.让layer进行无规则运动。
看上去是不是很简单!没错就是非常简单。
首先第一步,要拿到widget的截图。flutter的显示就是一个大型绘制现场,所有widget都会变成render。
class ParticleBody extends StatefulWidget {
Widget child;
//动画时间
var duration;
//图层数量
var numberLayers;
ParticleBody({this.duration = const Duration(seconds: 2),
this.child,
this.numberLayers = 10});
@override
_ParticleBodyState createState() => _ParticleBodyState();
}
class _ParticleBodyState extends State<ParticleBody>
with TickerProviderStateMixin {
GlobalKey _globalKey = GlobalKey();
List<Widget> layers = [];
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
...layers,
GestureDetector(
onTap: () {
_grain();
},
child: Opacity(
opacity: (1 - _controller.value),
child: RepaintBoundary(
key: _globalKey,
child: widget.child,
),
),
)
],
);
}
}
使用GestureDetector监听点击手势,用RepaintBoundary去截取图像。
第一步要去获取相应的图片信息
//获取图片信息
Future<image.Image> _getImgInfo() async {
RenderRepaintBoundary repaintBoundary =
_globalKey.currentContext.findRenderObject();
var img = await repaintBoundary.toImage();
var byteData = await img.toByteData(format: ImageByteFormat.png);
var uint8list = byteData.buffer.asUint8List();
return image.decodeImage(uint8list);
}
这里使用Image类的解码类,获得相应的图片信息。
获得图片信息后我们就需要将图片上的像素点,分配到一张张的layer上,然后再让它进行随机运动,模拟无规则。
//分配像素点
_separate(List<image.Image> blankImage, width, height, image.Image imgInfo) {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
var pixel = imgInfo.getPixel(x, y);
if (pixel == 0) continue;
int index = Random().nextInt(widget.numberLayers);
blankImage[index].setPixel(x, y, pixel);
}
}
}
大家可以想一下,让它展示出粒子效果需要几个动画?又要几种方式?
这了我选择两种动画,移动+消失,如果要爆炸效果,可以选用移动+缩放
//把图片画到图层上
Widget _getLayer(image.Image imgInfo) {
Uint8List uint8list = Uint8List.fromList(image.encodePng(imgInfo));
CurvedAnimation curvedAnimation =
CurvedAnimation(parent: _controller, curve: Curves.easeIn);
Animation<Offset> animation = Tween(
begin: Offset.zero,
end: Offset(30, -50) +
Offset(30, 30).scale((Random().nextDouble() - 0.5) * 2,
(Random().nextDouble() - 0.5) * 2))
.animate(_controller)
..addListener(() {
setState(() {});
});
return AnimatedBuilder(
child: Image.memory(uint8list),
builder: (context, child) {
return Transform.translate(
offset: animation.value,
child: animation.isCompleted ? null : Opacity(
opacity: cos(curvedAnimation.value * pi / 2), //1->0
child: child,
),
);
},
animation: _controller,
);
}
使用动画记得要混入动画,然后使用AnimationController。
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration)
..addListener(() {
setState(() {});
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
最后增加相应的点击事件,就OK了
//点击事件
_grain() async {
var imgInfo = await _getImgInfo();
var width = imgInfo.width;
var height = imgInfo.height;
List<image.Image> blankImage = List.generate(
widget.numberLayers, (index) => image.Image(width, height));
_separate(blankImage, width, height, imgInfo);
layers = blankImage.map((e) => _getLayer(e)).toList();
setState(() {});
_controller.forward(from: 0);
}
代码行数应该在130左右,是不是非常的简单而且易懂~如果需要一些调整,可以自行增加