【Flutter脫髮錄】也來實現一下滅霸效果

去年婦聯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佈局,兩部份內容:childlayers

@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: ...,
)
複製代碼

Demo
能夠看到效果已經出來了,固然還有不少能夠優化的地方。好比閃爍,自定義位移,從左到右逐漸散開,動畫結束後的回調等等。

結語

總的來講,在Flutter中簡單實現這個效果仍是比較輕鬆的,有清晰的思路,不須要複雜的計算就能夠完成。

掉根頭髮,掌握這項技能,它可香?

相關文章
相關標籤/搜索