前言
最近一個月支援某業務比較忙,並沒有比較長的時間週期讓我攻克Flutter一些比較難知識。比如我看到某開源專案自己使用Stream等技術實現了一個稍微複雜的Event Bus,我試著用碎片時間去學,結果發現需要補充的概念太多,碎片化地學習很多東西連貫不起來,於是我就暫時先放棄了,等個稍微長點的假期再來攻克吧。
最終,我覺得用這些碎片化的時間來學習Flutter的動畫,並試著做一些簡單的特效,本文就參考網上的一些教程,寫一個簡單的爆炸效果。
最終效果圖:
原文教程連結:傳送門,本文進行了一定改造
完整程式碼:gitee.com/wenjie2018/…
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();
}
}
複製程式碼
效果預覽?:
幀率?:
瘋狂點選下的幀率會有所下降?:
- 這個問題暫時沒有很好的優化思路,等以後有機會再想辦法優化吧