import 'dart:math'; import 'package:flutter/material.dart'; const _emojis = ['💲', '🤑', '💸', '💰', '💵', '💶', '💷']; const _dropCount = 160; const _duration = Duration(seconds: 3); /// Lets money emojis rain down over the whole screen for three seconds, for /// when a tab gets paid. Taps go straight through to the app underneath. void showMoneyRain(BuildContext context) { final overlay = Overlay.of(context, rootOverlay: true); late final OverlayEntry entry; entry = OverlayEntry( builder: (_) => _MoneyRain(onDone: () => entry.remove()), ); overlay.insert(entry); } class _MoneyRain extends StatefulWidget { final VoidCallback onDone; const _MoneyRain({required this.onDone}); @override State<_MoneyRain> createState() => _MoneyRainState(); } class _MoneyRainState extends State<_MoneyRain> with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( vsync: this, duration: _duration, )..forward().whenComplete(widget.onDone); final _drops = List.generate(_dropCount, (_) => _Drop.random(Random())); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return IgnorePointer( child: CustomPaint( size: Size.infinite, painter: _MoneyRainPainter(_controller, _drops), ), ); } } /// One falling emoji. Times are fractions of the whole rain, so every drop /// has left the screen by the time the rain ends. class _Drop { final int emoji; final double x; final double start; final double fall; final double size; final double spin; final double wobble; const _Drop({ required this.emoji, required this.x, required this.start, required this.fall, required this.size, required this.spin, required this.wobble, }); factory _Drop.random(Random random) { final fall = 0.3 + random.nextDouble() * 0.17; return _Drop( emoji: random.nextInt(_emojis.length), x: random.nextDouble(), start: random.nextDouble() * (1 - fall), fall: fall, size: 28 + random.nextDouble() * 36, spin: (random.nextDouble() - 0.5) * 4 * pi, wobble: random.nextDouble() * 2 * pi, ); } } class _MoneyRainPainter extends CustomPainter { static const _baseSize = 64.0; final Animation animation; final List<_Drop> drops; final List _glyphs = [ for (final emoji in _emojis) TextPainter( text: TextSpan( text: emoji, style: const TextStyle(fontSize: _baseSize), ), textDirection: TextDirection.ltr, )..layout(), ]; _MoneyRainPainter(this.animation, this.drops) : super(repaint: animation); @override void paint(Canvas canvas, Size size) { final t = animation.value; for (final drop in drops) { final progress = (t - drop.start) / drop.fall; if (progress < 0 || progress > 1) continue; final glyph = _glyphs[drop.emoji]; final scale = drop.size / _baseSize; final dx = drop.x * size.width + sin(progress * 3 * pi + drop.wobble) * 24; final dy = -drop.size + progress * (size.height + drop.size * 2); canvas ..save() ..translate(dx, dy) ..rotate(drop.spin * progress) ..scale(scale); glyph.paint(canvas, Offset(-glyph.width / 2, -glyph.height / 2)); canvas.restore(); } } @override bool shouldRepaint(_MoneyRainPainter oldDelegate) => oldDelegate.drops != drops; }