342 lines
8.6 KiB
Dart
342 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
|
/// Notification badge widget that can be placed on icons
|
|
class NotificationBadge extends StatelessWidget {
|
|
final Widget child;
|
|
final int count;
|
|
final Color badgeColor;
|
|
final Color textColor;
|
|
final double? size;
|
|
final bool showBadge;
|
|
|
|
const NotificationBadge({
|
|
super.key,
|
|
required this.child,
|
|
this.count = 0,
|
|
this.badgeColor = const Color(0xFFD32F2F),
|
|
this.textColor = Colors.white,
|
|
this.size,
|
|
this.showBadge = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
child,
|
|
if (showBadge && count > 0)
|
|
Positioned(
|
|
right: -4.w,
|
|
top: -4.h,
|
|
child: _buildBadge(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBadge() {
|
|
final badgeSize = size ?? 18.w;
|
|
final fontSize = (size ?? 18) * 0.55;
|
|
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.elasticOut,
|
|
builder: (context, value, child) {
|
|
return Transform.scale(
|
|
scale: value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.all(count > 99 ? 2.w : 3.w),
|
|
decoration: BoxDecoration(
|
|
color: badgeColor,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: badgeColor.withOpacity(0.4),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
constraints: BoxConstraints(
|
|
minWidth: badgeSize,
|
|
minHeight: badgeSize,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
count > 99 ? '99+' : count.toString(),
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: fontSize.sp,
|
|
fontWeight: FontWeight.bold,
|
|
height: 1.0,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Dot badge for simple notifications
|
|
class DotBadge extends StatelessWidget {
|
|
final Widget child;
|
|
final bool showDot;
|
|
final Color dotColor;
|
|
final double dotSize;
|
|
|
|
const DotBadge({
|
|
super.key,
|
|
required this.child,
|
|
this.showDot = true,
|
|
this.dotColor = const Color(0xFFD32F2F),
|
|
this.dotSize = 8.0,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
child,
|
|
if (showDot)
|
|
Positioned(
|
|
right: 0,
|
|
top: 0,
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.elasticOut,
|
|
builder: (context, value, child) {
|
|
return Transform.scale(
|
|
scale: value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
width: dotSize.w,
|
|
height: dotSize.h,
|
|
decoration: BoxDecoration(
|
|
color: dotColor,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: dotColor.withOpacity(0.4),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Pulsing notification badge with animation
|
|
class PulsingBadge extends StatefulWidget {
|
|
final Widget child;
|
|
final int count;
|
|
final Color badgeColor;
|
|
|
|
const PulsingBadge({
|
|
super.key,
|
|
required this.child,
|
|
this.count = 0,
|
|
this.badgeColor = const Color(0xFFD32F2F),
|
|
});
|
|
|
|
@override
|
|
State<PulsingBadge> createState() => _PulsingBadgeState();
|
|
}
|
|
|
|
class _PulsingBadgeState extends State<PulsingBadge>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _scaleAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
)..repeat(reverse: true);
|
|
|
|
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.count == 0) {
|
|
return widget.child;
|
|
}
|
|
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
widget.child,
|
|
Positioned(
|
|
right: -4.w,
|
|
top: -4.h,
|
|
child: AnimatedBuilder(
|
|
animation: _scaleAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.all(4.w),
|
|
decoration: BoxDecoration(
|
|
color: widget.badgeColor,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.badgeColor.withOpacity(0.5),
|
|
blurRadius: 8,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
constraints: BoxConstraints(
|
|
minWidth: 18.w,
|
|
minHeight: 18.h,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
widget.count > 99 ? '99+' : widget.count.toString(),
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Badge with custom content
|
|
class CustomBadge extends StatelessWidget {
|
|
final Widget child;
|
|
final String text;
|
|
final Color backgroundColor;
|
|
final Color textColor;
|
|
final EdgeInsets? padding;
|
|
|
|
const CustomBadge({
|
|
super.key,
|
|
required this.child,
|
|
required this.text,
|
|
this.backgroundColor = const Color(0xFFD32F2F),
|
|
this.textColor = Colors.white,
|
|
this.padding,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
child,
|
|
Positioned(
|
|
right: -8.w,
|
|
top: -8.h,
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.elasticOut,
|
|
builder: (context, value, child) {
|
|
return Transform.scale(
|
|
scale: value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: padding ?? EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
|
|
decoration: BoxDecoration(
|
|
color: backgroundColor,
|
|
borderRadius: BorderRadius.circular(10.r),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: backgroundColor.withOpacity(0.4),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Badge for bottom navigation items
|
|
class BottomNavBadge extends StatelessWidget {
|
|
final IconData icon;
|
|
final int count;
|
|
final bool isSelected;
|
|
final Color? selectedColor;
|
|
final Color? unselectedColor;
|
|
|
|
const BottomNavBadge({
|
|
super.key,
|
|
required this.icon,
|
|
this.count = 0,
|
|
this.isSelected = false,
|
|
this.selectedColor,
|
|
this.unselectedColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = isSelected
|
|
? (selectedColor ?? Theme.of(context).primaryColor)
|
|
: (unselectedColor ?? Colors.grey.shade600);
|
|
|
|
return NotificationBadge(
|
|
count: count,
|
|
child: Icon(
|
|
icon,
|
|
color: color,
|
|
size: 24.w,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|