【Flutter脱发实录】也来实现一下灭霸效果

5,648 阅读4分钟

去年妇联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中简单实现这个效果还是比较轻松的,有清晰的思路,不需要复杂的计算就可以完成。

掉根头发,掌握这项技能,它可香?