去年婦聯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中簡單實現這個效果還是比較輕鬆的,有清晰的思路,不需要複雜的計算就可以完成。
掉根頭髮,掌握這項技能,它可香?