import 'dart:math'; import 'package:flutter/material.dart'; class FavButton extends StatefulWidget { final bool initialValue; final ValueChanged? onChanged; final double size; const FavButton({ super.key, this.initialValue = false, this.onChanged, this.size = 40, }); @override State createState() => _FavButtonState(); } class _FavButtonState extends State with SingleTickerProviderStateMixin { late bool _isFav; late AnimationController _ctrl; late Animation _scale; late Animation _particleProgress; @override void initState() { super.initState(); _isFav = widget.initialValue; _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 450), ); _scale = TweenSequence([ TweenSequenceItem( tween: Tween(begin: 1.0, end: 1.4) .chain(CurveTween(curve: Curves.easeOut)), weight: 40, ), TweenSequenceItem( tween: Tween(begin: 1.4, end: 1.0) .chain(CurveTween(curve: Curves.elasticOut)), weight: 60, ), ]).animate(_ctrl); _particleProgress = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut); } @override void dispose() { _ctrl.dispose(); super.dispose(); } void _toggle() { setState(() => _isFav = !_isFav); widget.onChanged?.call(_isFav); if (_isFav) _ctrl.forward(from: 0); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _toggle, child: AnimatedBuilder( animation: _ctrl, builder: (context, child) => CustomPaint( painter: _isFav ? _BurstPainter(_particleProgress.value) : null, child: Transform.scale( scale: _isFav ? _scale.value : 1.0, child: Container( width: widget.size, height: widget.size, decoration: BoxDecoration( color: _isFav ? Colors.red.withOpacity(0.1) : Colors.transparent, shape: BoxShape.circle, ), child: Icon( _isFav ? Icons.favorite_rounded : Icons.favorite_border_rounded, size: widget.size * 0.5, color: _isFav ? Colors.redAccent : Colors.grey, ), ), ), ), ), ); } } class _BurstPainter extends CustomPainter { final double progress; _BurstPainter(this.progress); @override void paint(Canvas canvas, Size size) { if (progress == 0) return; final center = Offset(size.width / 2, size.height / 2); final paint = Paint() ..color = Colors.redAccent.withOpacity(1 - progress); const count = 7; for (int i = 0; i < count; i++) { final angle = (i / count) * 2 * pi; final dist = progress * 24; final pos = center + Offset(dist * cos(angle), dist * sin(angle)); canvas.drawCircle(pos, 3 * (1 - progress * 0.5), paint); } } @override bool shouldRepaint(_BurstPainter old) => old.progress != progress; }