前言
最近一个月支持某业务比较忙,并没有比较长的时间周期让我攻克Flutter一些比较难知识。比如我看到某开源项目自己使用Stream等技术实现了一个稍微复杂的Event Bus,我试着用碎片时间去学,结果发现需要补充的概念太多,碎片化地学习很多东西连贯不起来,于是我就暂时先放弃了,等个稍微长点的假期再来攻克吧。
最终,我觉得用这些碎片化的时间来学习Flutter的动画,并试着做一些简单的特效,本文就参考网上的一些教程,写一个简单的爆炸效果。
最终效果图:
原文教程链接:传送门,本文进行了一定改造
完整代码:https://gitee.com/wenjie2018/flutter_boom_demo
Flutter SDK: 2.2.2
dependencies: simple_animations: ^3.0.1
simple_animations封装了一些动画逻辑,可以让我们更方便的使用动画的API
从一个点开始
先说说整体思路:爆炸就是中间有很多个点,它们在同一时刻往不同方向走了不同的距离,并且过程中不断变小、透明,直到消失👇
在实现一群点之前,我们先来实现一个点的渐变👇
代码大致思路:
- 构建区域循环动画
- 点击按钮:如果扫描到
List
中有“点”Widget,则将这个点从左到右推出,并且逐渐变小、透明 - 每一帧都检查“点”的Widget是否超过动画时间,如超过则从
List
中删除
关键API:
- 使用
LoopAnimation
构建循环动画 - 使用
Color.withAlpha
控制透明度 - 使用
Transform.scale
控制控件大小 - 使用
Stack
让“点”可以重叠,再使用Positioned
控制坐标
完整代码👇:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';
void main() {
runApp(new MaterialApp(home: Page()));
}
class Page extends StatefulWidget {
@override
State<StatefulWidget> createState() => PageState();
}
class PageState extends State<Page> {
final GlobalKey<MoleState> moleKey = new GlobalKey();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Mole>[
Mole(key: moleKey)
],
)
// child: Mole(key: moleKey),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied),
onPressed: () {
moleKey.currentState!.hitMole();
},
),
);
}
}
class Mole extends StatefulWidget {
@override
MoleState createState() => MoleState();
Mole({
Key? key,
}) : super(key: key);
}
class MoleState extends State<Mole> {
/// "点"的List
final List<MoleParticle> particles = [];
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: _buildMole(),
);
}
Widget _buildMole() {
return LoopAnimation<int>(
tween: ConstantTween(1),
builder: (context, child, value) {
// builder每一帧都会被调用,所以要再次清理点
_manageParticleLifecycle();
return Stack(
clipBehavior: Clip.none,
children: [
...particles.map((it) => it.buildWidget())
],
);
},
);
}
/// 向List中插入"点"
hitMole() {
Iterable.generate(1).forEach(
(i) => particles.add(MoleParticle())
);
}
/// 管理颗粒的生命,清除超时的颗粒
_manageParticleLifecycle() {
particles.removeWhere((particle) {
return particle.progress() == 1;
});
}
@override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
}
enum _MoleProps { x, y, scale }
class MoleParticle {
late Animatable<MultiTweenValues<_MoleProps>> tween;
late Duration startTime;
final duration = 3.seconds;
MoleParticle() {
final start_x = 0.0;
final start_y = 0.0;
final end_x = window.physicalSize.width / window.devicePixelRatio - 50;
final end_y = 0.0;
tween = MultiTween<_MoleProps>()
..add(_MoleProps.x, start_x.tweenTo(end_x))
..add(_MoleProps.y, start_y.tweenTo(end_y))
..add(_MoleProps.scale, 1.0.tweenTo(0.0));
startTime = DateTime.now().duration();
}
Widget buildWidget() {
final MultiTweenValues<_MoleProps> values = tween.transform(progress());
var alpha = 255 - (255 * progress()).toInt();
return Positioned(
left: values.get(_MoleProps.x),
top: values.get(_MoleProps.y),
child: Transform.scale(
scale: values.get(_MoleProps.scale),
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.black.withAlpha(alpha),
borderRadius: BorderRadius.circular(50)
),
),
),
);
}
/// 计算"点"的生命周期
double progress() {
return ((DateTime.now().duration() - startTime) / duration)
.clamp(0.0, 1.0)
.toDouble();
}
}
效果预览👇:
实现爆炸
实现了一个“点”的渐变后,之后要实现爆炸就好办了,大致变更如下:
- 动画周期3秒 -> 0.5秒
- 一次性塞一个“点” -> 一次性塞50个“点”
- 控件从靠左居中 -> 屏幕居中
- 每个点的起始坐标固定但结束坐标改为随机
最终代码如下:
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';
void main() {
runApp(new MaterialApp(home: Page()));
}
class Page extends StatefulWidget {
@override
State<StatefulWidget> createState() => PageState();
}
class PageState extends State<Page> {
final GlobalKey<MoleState> moleKey = new GlobalKey();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Mole(key: moleKey),
)
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied),
onPressed: () {
moleKey.currentState!.hitMole();
},
),
);
}
}
class Mole extends StatefulWidget {
@override
MoleState createState() => MoleState();
Mole({
Key? key,
}) : super(key: key);
}
class MoleState extends State<Mole> {
/// "点"的List
final List<MoleParticle> particles = [];
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: _buildMole(),
);
}
Widget _buildMole() {
return LoopAnimation<int>(
duration: 2.seconds,
tween: ConstantTween(1),
builder: (context, child, value) {
// builder每一帧都会被调用,所以要再次清理点
_manageParticleLifecycle();
return Stack(
clipBehavior: Clip.none,
children: [
...particles.map((it) => it.buildWidget())
],
);
},
);
}
/// 向List中插入"点"
hitMole() {
Iterable.generate(50).forEach(
(i) => particles.add(MoleParticle())
);
}
/// 管理颗粒的生命,清除超时的颗粒
_manageParticleLifecycle() {
particles.removeWhere((particle) {
return particle.progress() == 1;
});
}
@override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
}
enum _MoleProps { x, y, scale }
class MoleParticle {
late Animatable<MultiTweenValues<_MoleProps>> tween;
late Duration startTime;
final duration = 0.5.seconds;
MoleParticle() {
var random = Random();
double max_x = 300;
double max_y = 300;
final start_x = 0.0;
final start_y = 0.0;
final end_x = max_x * random.nextDouble() * (random.nextBool() ? 1 : -1);
final end_y = max_y * random.nextDouble() * (random.nextBool() ? 1 : -1);
tween = MultiTween<_MoleProps>()
..add(_MoleProps.x, start_x.tweenTo(end_x))
..add(_MoleProps.y, start_y.tweenTo(end_y))
..add(_MoleProps.scale, 1.0.tweenTo(0.0));
startTime = DateTime.now().duration();
}
Widget buildWidget() {
final MultiTweenValues<_MoleProps> values = tween.transform(progress());
var alpha = 255 - (255 * progress()).toInt();
return Positioned(
left: values.get(_MoleProps.x),
top: values.get(_MoleProps.y),
child: Transform.scale(
scale: values.get(_MoleProps.scale),
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.black.withAlpha(alpha),
borderRadius: BorderRadius.circular(50)
),
),
),
);
}
/// 计算"点"的生命周期
double progress() {
return ((DateTime.now().duration() - startTime) / duration)
.clamp(0.0, 1.0)
.toDouble();
}
}
效果预览👇:
帧率👇:
疯狂点击下的帧率会有所下降👇:
- 这个问题暂时没有很好的优化思路,等以后有机会再想办法优化吧