去年婦聯4上映後,谷歌迅速推出了一個彩蛋,以至敬婦聯計生辦主任-滅霸。安全
鑑於新冠疫情在國外的爆發,國家爲了保障咱們的安全,限制了大部分危險的通道,我冒死替你們搬來了這個彩蛋。 bash
其實這個彩蛋早就被你們玩壞了,看了各路大神的實現方式,心中也就有了思路。下面就開始在Flutter中實現這個效果。dom
這個彩蛋本質上就是一個動畫,而要實現一個動畫效果,首先要作的就是拆解,而後在簡單的效果上豐富元素。async
問:滅霸實現他的計劃須要幾步?ide
答:三步。1.戴上手套 2.打個響指 3.看特效佈局
問:那麼在Flutter中實現這個效果須要幾步呢?優化
答:也是三步。1.圖像化 2.分離像素 3.看動畫動畫
圖像化 就是將範圍內的Widget
轉換爲一個Image
對象,能夠理解爲截圖。ui
分離像素 這是最爲關鍵的一步,也是較爲複雜的一步。this
要理解這一步,你須要靜下心,我幫你好好捋一捋。
心中默默回答如下幾個問題:
今年幾歲了?
工做多少年了?
手頭存款有多少?
沒有女友的你,錢都去哪兒了?
是的,多年的積蓄,聽了個響兒,就煙消雲散不知所蹤了。遊戲充值?吃吃喝喝?數碼設備?打賞主播?會員費?
你懂了嗎?你懂了吧!
多年的積蓄,變成了一筆筆的支出,歸入種種消費類型,隨着時間的推移慢慢遺忘,遺忘。
把第一步生成的圖像比做多年的積蓄,生活中的每一筆支出就對應了圖像上的每個像素點,而消費類型是一個個重疊的空白透明圖層。把圖像上的每一個像素點,隨機分配到這些圖層上,而後將這些圖層慢慢向不一樣方向抽離,淡化,就消失了,消失了。
用一張圖強化一下理解:
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中簡單實現這個效果仍是比較輕鬆的,有清晰的思路,不須要複雜的計算就能夠完成。
掉根頭髮,掌握這項技能,它可香?