import 'package:flutter/material.dart'; import '../../theme/app_theme.dart'; class CountBadge extends StatefulWidget { final int count; final Color? backgroundColor; final Color? textColor; final double? size; final bool showZero; final bool animated; final String? suffix; final int? maxCount; final VoidCallback? onTap; const CountBadge({ super.key, required this.count, this.backgroundColor, this.textColor, this.size, this.showZero = false, this.animated = true, this.suffix, this.maxCount, this.onTap, }); @override State createState() => _CountBadgeState(); } class _CountBadgeState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; late Animation _bounceAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); _scaleAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), )); _bounceAnimation = Tween( begin: 1.0, end: 1.2, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.5, 1.0, curve: Curves.elasticInOut), )); if (widget.animated) { _animationController.forward(); } } @override void didUpdateWidget(CountBadge oldWidget) { super.didUpdateWidget(oldWidget); if (widget.count != oldWidget.count && widget.animated) { _animationController.reset(); _animationController.forward(); } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!widget.showZero && widget.count == 0) { return const SizedBox.shrink(); } final displayText = _getDisplayText(); final size = widget.size ?? 20; final backgroundColor = widget.backgroundColor ?? AppTheme.errorColor; final textColor = widget.textColor ?? Colors.white; Widget badge = Container( constraints: BoxConstraints( minWidth: size, minHeight: size, ), padding: EdgeInsets.symmetric( horizontal: displayText.length > 1 ? size * 0.2 : 0, vertical: 2, ), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(size / 2), boxShadow: [ BoxShadow( color: backgroundColor.withOpacity(0.4), blurRadius: 6, offset: const Offset(0, 2), ), ], border: Border.all( color: Colors.white, width: 1.5, ), ), child: Center( child: Text( displayText, style: TextStyle( color: textColor, fontSize: size * 0.6, fontWeight: FontWeight.bold, height: 1.0, ), textAlign: TextAlign.center, ), ), ); if (widget.animated) { badge = AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value * _bounceAnimation.value, child: child, ); }, child: badge, ); } if (widget.onTap != null) { badge = GestureDetector( onTap: widget.onTap, child: badge, ); } return badge; } String _getDisplayText() { if (widget.maxCount != null && widget.count > widget.maxCount!) { return '${widget.maxCount}+'; } final countText = widget.count.toString(); return widget.suffix != null ? '$countText${widget.suffix}' : countText; } } class NotificationBadge extends StatelessWidget { final Widget child; final int count; final Color? badgeColor; final double? size; final Offset offset; final bool showZero; const NotificationBadge({ super.key, required this.child, required this.count, this.badgeColor, this.size, this.offset = const Offset(0, 0), this.showZero = false, }); @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, children: [ child, if (showZero || count > 0) Positioned( top: offset.dy, right: offset.dx, child: CountBadge( count: count, backgroundColor: badgeColor, size: size, showZero: showZero, ), ), ], ); } }