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

愛新覺羅狗剩兒發表於2020-03-29

去年婦聯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佈局,兩部分內容: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中簡單實現這個效果還是比較輕鬆的,有清晰的思路,不需要複雜的計算就可以完成。

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

相關文章