前言

最近一个月支持某业务比较忙,并没有比较长的时间周期让我攻克Flutter一些比较难知识。比如我看到某开源项目自己使用Stream等技术实现了一个稍微复杂的Event Bus,我试着用碎片时间去学,结果发现需要补充的概念太多,碎片化地学习很多东西连贯不起来,于是我就暂时先放弃了,等个稍微长点的假期再来攻克吧。

最终,我觉得用这些碎片化的时间来学习Flutter的动画,并试着做一些简单的特效,本文就参考网上的一些教程,写一个简单的爆炸效果。

最终效果图:
middle_img_v2_ba180cff90f14ba6aff880a32fb84cdg.gif

原文教程链接:传送门,本文进行了一定改造
完整代码:https://gitee.com/wenjie2018/flutter_boom_demo
Flutter SDK: 2.2.2
dependencies: simple_animations: ^3.0.1

simple_animations封装了一些动画逻辑,可以让我们更方便的使用动画的API


从一个点开始

先说说整体思路:爆炸就是中间有很多个点,它们在同一时刻往不同方向走了不同的距离,并且过程中不断变小、透明,直到消失👇
image.png

在实现一群点之前,我们先来实现一个点的渐变👇
image.png

代码大致思路:

  • 构建区域循环动画
  • 点击按钮:如果扫描到List中有“点”Widget,则将这个点从左到右推出,并且逐渐变小、透明
  • 每一帧都检查“点”的Widget是否超过动画时间,如超过则从List中删除

关键API:

  • 使用LoopAnimation构建循环动画
  • 使用Color.withAlpha控制透明度
  • 使用Transform.scale控制控件大小
  • 使用Stack让“点”可以重叠,再使用Positioned控制坐标

完整代码👇:


import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';

void main() {
  runApp(new MaterialApp(home: Page()));
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PageState();
}

class PageState extends State<Page> {

  final GlobalKey<MoleState> moleKey = new GlobalKey();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Mole>[
            Mole(key: moleKey)
          ],
        )
        // child: Mole(key: moleKey),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        onPressed: () {
          moleKey.currentState!.hitMole();
        },
      ),
    );
  }
}

class Mole extends StatefulWidget {
  @override
  MoleState createState() => MoleState();

  Mole({
    Key? key,
  }) : super(key: key);
}

class MoleState extends State<Mole> {
  /// "点"的List
  final List<MoleParticle> particles = [];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: _buildMole(),
    );
  }

  Widget _buildMole() {
    return LoopAnimation<int>(
      tween: ConstantTween(1),
      builder: (context, child, value) {
        // builder每一帧都会被调用,所以要再次清理点
        _manageParticleLifecycle();
        return Stack(
          clipBehavior: Clip.none,
          children: [
            ...particles.map((it) => it.buildWidget())
          ],
        );
      },
    );
  }

  /// 向List中插入"点"
  hitMole() {
    Iterable.generate(1).forEach(
            (i) => particles.add(MoleParticle())
    );
  }

  /// 管理颗粒的生命,清除超时的颗粒
  _manageParticleLifecycle() {
    particles.removeWhere((particle) {
      return particle.progress() == 1;
    });
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }
}

enum _MoleProps { x, y, scale }

class MoleParticle {
  late Animatable<MultiTweenValues<_MoleProps>> tween;
  late Duration startTime;
  final duration = 3.seconds;

  MoleParticle() {
    final start_x = 0.0;
    final start_y = 0.0;
    final end_x = window.physicalSize.width / window.devicePixelRatio - 50;
    final end_y = 0.0;

    tween = MultiTween<_MoleProps>()
      ..add(_MoleProps.x, start_x.tweenTo(end_x))
      ..add(_MoleProps.y, start_y.tweenTo(end_y))
      ..add(_MoleProps.scale, 1.0.tweenTo(0.0));

    startTime = DateTime.now().duration();
  }

  Widget buildWidget() {
    final MultiTweenValues<_MoleProps> values = tween.transform(progress());
    var alpha = 255 - (255 * progress()).toInt();
    return Positioned(
      left: values.get(_MoleProps.x),
      top: values.get(_MoleProps.y),
      child: Transform.scale(
        scale: values.get(_MoleProps.scale),
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
              color: Colors.black.withAlpha(alpha),
              borderRadius: BorderRadius.circular(50)
          ),
        ),
      ),
    );
  }

  /// 计算"点"的生命周期
  double progress() {
    return ((DateTime.now().duration() - startTime) / duration)
        .clamp(0.0, 1.0)
        .toDouble();
  }
}

效果预览👇:
middle_img_v2_18d8511115d64ca4806ecef1c68b50fg.gif


实现爆炸

实现了一个“点”的渐变后,之后要实现爆炸就好办了,大致变更如下:

  • 动画周期3秒 -> 0.5秒
  • 一次性塞一个“点” -> 一次性塞50个“点”
  • 控件从靠左居中 -> 屏幕居中
  • 每个点的起始坐标固定但结束坐标改为随机

最终代码如下:


import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:supercharged/supercharged.dart';

void main() {
  runApp(new MaterialApp(home: Page()));
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PageState();
}

class PageState extends State<Page> {

  final GlobalKey<MoleState> moleKey = new GlobalKey();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Mole(key: moleKey),
        )
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        onPressed: () {
          moleKey.currentState!.hitMole();
        },
      ),
    );
  }
}

class Mole extends StatefulWidget {
  @override
  MoleState createState() => MoleState();

  Mole({
    Key? key,
  }) : super(key: key);
}

class MoleState extends State<Mole> {
  /// "点"的List
  final List<MoleParticle> particles = [];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: _buildMole(),
    );
  }

  Widget _buildMole() {
    return LoopAnimation<int>(
      duration: 2.seconds,
      tween: ConstantTween(1),
      builder: (context, child, value) {
        // builder每一帧都会被调用,所以要再次清理点
        _manageParticleLifecycle();
        return Stack(
          clipBehavior: Clip.none,
          children: [
            ...particles.map((it) => it.buildWidget())
          ],
        );
      },
    );
  }

  /// 向List中插入"点"
  hitMole() {
    Iterable.generate(50).forEach(
            (i) => particles.add(MoleParticle())
    );
  }

  /// 管理颗粒的生命,清除超时的颗粒
  _manageParticleLifecycle() {
    particles.removeWhere((particle) {
      return particle.progress() == 1;
    });
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }
}

enum _MoleProps { x, y, scale }

class MoleParticle {
  late Animatable<MultiTweenValues<_MoleProps>> tween;
  late Duration startTime;
  final duration = 0.5.seconds;

  MoleParticle() {
    var random = Random();
    double max_x = 300;
    double max_y = 300;
    final start_x = 0.0;
    final start_y = 0.0;
    final end_x = max_x * random.nextDouble() * (random.nextBool() ? 1 : -1);
    final end_y = max_y * random.nextDouble() * (random.nextBool() ? 1 : -1);

    tween = MultiTween<_MoleProps>()
      ..add(_MoleProps.x, start_x.tweenTo(end_x))
      ..add(_MoleProps.y, start_y.tweenTo(end_y))
      ..add(_MoleProps.scale, 1.0.tweenTo(0.0));

    startTime = DateTime.now().duration();
  }

  Widget buildWidget() {
    final MultiTweenValues<_MoleProps> values = tween.transform(progress());
    var alpha = 255 - (255 * progress()).toInt();
    return Positioned(
      left: values.get(_MoleProps.x),
      top: values.get(_MoleProps.y),
      child: Transform.scale(
        scale: values.get(_MoleProps.scale),
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
              color: Colors.black.withAlpha(alpha),
              borderRadius: BorderRadius.circular(50)
          ),
        ),
      ),
    );
  }

  /// 计算"点"的生命周期
  double progress() {
    return ((DateTime.now().duration() - startTime) / duration)
        .clamp(0.0, 1.0)
        .toDouble();
  }
}

效果预览👇:
middle_img_v2_7f8193e6e63c48f9862e3e28a270d0bg.gif

帧率👇:
QQ20210703151006.gif

疯狂点击下的帧率会有所下降👇:
middle_img_v2_ef772b433998496bab1dd215d7eab69g.gif
QQ20210703-151457

  • 这个问题暂时没有很好的优化思路,等以后有机会再想办法优化吧