Flutter 烟花/礼炮粒子动画效果

1,296 阅读2分钟

前言

实用的酷炫烟花/礼炮粒子动画效果,主要处理好随机轨迹路线问题,其他的靠draw绘制即可。

不多逼逼,先看效果图。

烟花动效gif.gif

代码如下,开袋即食,具体可看代码注释。

import 'dart:math' as Math;
import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

class ExplosionWidget extends StatefulWidget {
  final Rect? bound;
  int? animateTime;

  ExplosionWidget({Key? key, this.bound, this.animateTime}) : super(key: key);

  @override
  _ExplosionWidgetState createState() => _ExplosionWidgetState();
}

class _ExplosionWidgetState extends State<ExplosionWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    //初始化AnimationController
    _animationController = AnimationController(
        duration: Duration(milliseconds: widget.animateTime ?? 1200),
        vsync: this);
    //页面初始化后才执行动画,不然不生效
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _animationController.value = 0;
    _animationController.forward();
    setState(() {});
    });
  }

  @override
  void dispose() {
    super.dispose();
    //注销动画
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        alignment: Alignment.center,
        child: AnimatedBuilder(
            animation: _animationController,
            builder: (context, child) {
              return ExplosionRenderObjectWidget(
                child: Container(
                    alignment: Alignment.center,
                    child: const RepaintBoundary()),
                progress: _animationController.value,
              );
            }));
  }
}

class ExplosionRenderObjectWidget extends RepaintBoundary {
  final double? progress;
  final Rect? bound;

  const ExplosionRenderObjectWidget(
      {Key? key, Widget? child, this.progress, this.bound})
      : super(key: key, child: child);

  @override
  _ExplosionRenderObject createRenderObject(BuildContext context) =>
      _ExplosionRenderObject(bound: bound);

  @override
  void updateRenderObject(
      BuildContext context, _ExplosionRenderObject renderObject) {
    //跟随progress变化而重新绘制
    renderObject.update(progress);
  }
}

class _ExplosionRenderObject extends RenderRepaintBoundary {
  double? progress;
  List<_Particle>? particles;
  Rect? bound;

  _ExplosionRenderObject({this.bound, RenderBox? child}) : super(child: child);

  void update(double? progress) {
    this.progress = progress;
    markNeedsPaint();
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (progress != null && progress != 0 && progress != 1) {
      if (particles == null) {
        bound ??= Rect.fromLTWH(0, 0, size.width, size.height * 2);
        particles = initParticleList(bound!);
      }
      //绘制动画
      draw(context.canvas, particles!, progress!);
    } else {
      if (child != null) {
        context.paintChild(child!, offset);
      }
    }
  }
}

const double END_VALUE = 1.4;
const double V = 3;
const double X = 5;
const double Y = 20;
const double W = 1;

List<_Particle> initParticleList(Rect bound) {
  int partLen = 15;
  List<_Particle> particles =
      List<_Particle>.filled(partLen * partLen, _Particle(), growable: true);
   ///使用随机数,用于制造自由轨迹
  Math.Random random = Math.Random(DateTime.now().millisecondsSinceEpoch);
  for (int i = 0; i < partLen; i++) {
    for (int j = 0; j < partLen; j++) {
      //生成15*15个圆圈/五角星
      particles[(i * partLen) + j] =
          generateParticle(getColorByPixel(j), random, bound);
    }
  }
  return particles;
}

//绘制样式
bool draw(Canvas canvas, List<_Particle> particles, double progress) {
  Paint paint = Paint();
  for (int i = 0; i < particles.length; i++) {
    _Particle particle = particles[i];
    particle.advance(progress);
    if (particle.alpha > 0) {
      if (i % 2 == 0) {
        ///圆圈
        paint.color = particle.color
            .withAlpha((particle.color.alpha * particle.alpha).toInt());
        canvas.drawCircle(
            Offset(particle.cx, particle.cy), particle.radius, paint);
      } else {
        ///五角星
        paint
          ..color = particle.color
              .withAlpha((particle.color.alpha * particle.alpha).toInt())
          ..style = PaintingStyle.fill;

        var initDegreen = 180;
        // 连接五角星的五个顶点,360/5,每个是72度
        final path = Path();
        var posOne = getOffsetPosition(initDegreen, particle.radius * 2);
        var posTwo = getOffsetPosition(72 + initDegreen, particle.radius * 2);
        var posThree =
            getOffsetPosition(144 + initDegreen, particle.radius * 2);
        var posfour = getOffsetPosition(216 + initDegreen, particle.radius * 2);
        var posFive = getOffsetPosition(288 + initDegreen, particle.radius * 2);
        path.moveTo(posOne.dx + particle.cx, posOne.dy + particle.cy);
        path.lineTo(posfour.dx + particle.cx, posfour.dy + particle.cy);
        path.lineTo(posTwo.dx + particle.cx, posTwo.dy + particle.cy);
        path.lineTo(posFive.dx + particle.cx, posFive.dy + particle.cy);
        path.lineTo(posThree.dx + particle.cx, posThree.dy + particle.cy);
        //最后用close的方式把path封闭起来
        path.close();

        canvas.drawPath(path, paint);
      }
    }
  }
  return true;
}

Offset getOffsetPosition(int degreen, double radius) {
  //角度转成弧度
  var radian = degreen * pi / 180;
  var dx = sin(radian) * radius;
  var dy = cos(radian) * radius;
  return Offset(dx + radius, dy + radius);
}

//生成_Partice,主要靠random.nextDouble()随机生成下一个轨迹
_Particle generateParticle(Color color, Math.Random random, Rect bound) {
  _Particle particle = _Particle();
  particle.color = color;
  particle.radius = V;
  if (random.nextDouble() < 0.2) {
    particle.baseRadius = V + ((X - V) * random.nextDouble());
  } else {
    particle.baseRadius = W + ((V - W) * random.nextDouble());
  }
  double nextDouble = random.nextDouble();
  particle.top = bound.height * ((0.18 * random.nextDouble()) + 0.2);
  particle.top = nextDouble < 0.2
      ? particle.top
      : particle.top + ((particle.top * 0.2) * random.nextDouble());
  particle.bottom = (bound.height * (random.nextDouble() - 0.5)) * 1.8;
  double f = nextDouble < 0.2
      ? particle.bottom
      : nextDouble < 0.8
          ? particle.bottom * 0.6
          : particle.bottom * 0.3;
  particle.bottom = f;
  particle.mag = 4.0 * particle.top / particle.bottom;
  particle.neg = (-particle.mag) / particle.bottom;
  f = bound.center.dx + (Y * (random.nextDouble() - 0.5));
  particle.baseCx = f;
  particle.cx = f;
  f = bound.center.dy + (Y * (random.nextDouble() - 0.5));
  particle.baseCy = f;
  particle.cy = f;
  particle.life = END_VALUE / 10 * random.nextDouble();
  particle.overflow = 0.4 * random.nextDouble();
  particle.alpha = 1;
  return particle;
}

class _Particle {
  late double alpha;
  late Color color;
  late double cx;
  late double cy;
  late double radius;
  late double baseCx;
  late double baseCy;
  late double baseRadius;
  late double top;
  late double bottom;
  late double mag;
  late double neg;
  late double life;
  late double overflow;

  void advance(double factor) {
    double f = 0;
    double normalization = factor / END_VALUE;
    if (normalization < life || normalization > 1 - overflow) {
      alpha = 0;
      return;
    }
    normalization = (normalization - life) / (1 - life - overflow);
    double f2 = normalization * END_VALUE;
    if (normalization >= 0.7) {
      f = (normalization - 0.7) / 0.3;
    }
    alpha = 1 - f;
    f = bottom * f2;
    cx = baseCx + f;
    cy = (baseCy - neg * Math.pow(f, 2.0)) - f * mag;
    radius = V + (baseRadius - V) * f2;
  }
}

Color getColorByPixel(int i) {
  //生成红黄蓝三种颜色
  if (i % 3 == 0) {
    return const Color.fromARGB(255, 255, 201, 70);
  } else if (i % 3 == 1) {
    return const Color.fromARGB(255, 241, 49, 49);
  } else {
    return const Color.fromARGB(255, 63, 67, 243);
  }
}