first commit
This commit is contained in:
202
unionflow-mobile-apps/lib/shared/widgets/badges/count_badge.dart
Normal file
202
unionflow-mobile-apps/lib/shared/widgets/badges/count_badge.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
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<CountBadge> createState() => _CountBadgeState();
|
||||
}
|
||||
|
||||
class _CountBadgeState extends State<CountBadge>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _bounceAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.elasticOut),
|
||||
));
|
||||
|
||||
_bounceAnimation = Tween<double>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
enum BadgeType {
|
||||
success,
|
||||
warning,
|
||||
error,
|
||||
info,
|
||||
neutral,
|
||||
premium,
|
||||
new_,
|
||||
}
|
||||
|
||||
enum BadgeSize {
|
||||
small,
|
||||
medium,
|
||||
large,
|
||||
}
|
||||
|
||||
enum BadgeVariant {
|
||||
filled,
|
||||
outlined,
|
||||
ghost,
|
||||
gradient,
|
||||
}
|
||||
|
||||
class StatusBadge extends StatelessWidget {
|
||||
final String text;
|
||||
final BadgeType type;
|
||||
final BadgeSize size;
|
||||
final BadgeVariant variant;
|
||||
final IconData? icon;
|
||||
final VoidCallback? onTap;
|
||||
final bool animated;
|
||||
final String? tooltip;
|
||||
final Widget? customIcon;
|
||||
final bool showPulse;
|
||||
|
||||
const StatusBadge({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.type = BadgeType.neutral,
|
||||
this.size = BadgeSize.medium,
|
||||
this.variant = BadgeVariant.filled,
|
||||
this.icon,
|
||||
this.onTap,
|
||||
this.animated = true,
|
||||
this.tooltip,
|
||||
this.customIcon,
|
||||
this.showPulse = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = _getBadgeConfig();
|
||||
|
||||
Widget badge = AnimatedContainer(
|
||||
duration: animated ? const Duration(milliseconds: 200) : Duration.zero,
|
||||
padding: _getPadding(),
|
||||
decoration: _getDecoration(config),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null || customIcon != null) ...[
|
||||
_buildIcon(config),
|
||||
SizedBox(width: _getIconSpacing()),
|
||||
],
|
||||
if (showPulse) ...[
|
||||
_buildPulseIndicator(config.primaryColor),
|
||||
SizedBox(width: _getIconSpacing()),
|
||||
],
|
||||
Text(
|
||||
text,
|
||||
style: _getTextStyle(config),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
badge = Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(_getBorderRadius()),
|
||||
child: badge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip != null) {
|
||||
badge = Tooltip(
|
||||
message: tooltip!,
|
||||
child: badge,
|
||||
);
|
||||
}
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
_BadgeConfig _getBadgeConfig() {
|
||||
switch (type) {
|
||||
case BadgeType.success:
|
||||
return _BadgeConfig(
|
||||
primaryColor: AppTheme.successColor,
|
||||
backgroundColor: AppTheme.successColor.withOpacity(0.1),
|
||||
borderColor: AppTheme.successColor.withOpacity(0.3),
|
||||
);
|
||||
case BadgeType.warning:
|
||||
return _BadgeConfig(
|
||||
primaryColor: AppTheme.warningColor,
|
||||
backgroundColor: AppTheme.warningColor.withOpacity(0.1),
|
||||
borderColor: AppTheme.warningColor.withOpacity(0.3),
|
||||
);
|
||||
case BadgeType.error:
|
||||
return _BadgeConfig(
|
||||
primaryColor: AppTheme.errorColor,
|
||||
backgroundColor: AppTheme.errorColor.withOpacity(0.1),
|
||||
borderColor: AppTheme.errorColor.withOpacity(0.3),
|
||||
);
|
||||
case BadgeType.info:
|
||||
return _BadgeConfig(
|
||||
primaryColor: AppTheme.infoColor,
|
||||
backgroundColor: AppTheme.infoColor.withOpacity(0.1),
|
||||
borderColor: AppTheme.infoColor.withOpacity(0.3),
|
||||
);
|
||||
case BadgeType.premium:
|
||||
return _BadgeConfig(
|
||||
primaryColor: const Color(0xFFFFD700),
|
||||
backgroundColor: const Color(0xFFFFD700).withOpacity(0.1),
|
||||
borderColor: const Color(0xFFFFD700).withOpacity(0.3),
|
||||
);
|
||||
case BadgeType.new_:
|
||||
return _BadgeConfig(
|
||||
primaryColor: const Color(0xFFFF6B6B),
|
||||
backgroundColor: const Color(0xFFFF6B6B).withOpacity(0.1),
|
||||
borderColor: const Color(0xFFFF6B6B).withOpacity(0.3),
|
||||
);
|
||||
default:
|
||||
return _BadgeConfig(
|
||||
primaryColor: AppTheme.textSecondary,
|
||||
backgroundColor: AppTheme.textSecondary.withOpacity(0.1),
|
||||
borderColor: AppTheme.textSecondary.withOpacity(0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets _getPadding() {
|
||||
switch (size) {
|
||||
case BadgeSize.small:
|
||||
return const EdgeInsets.symmetric(horizontal: 8, vertical: 2);
|
||||
case BadgeSize.medium:
|
||||
return const EdgeInsets.symmetric(horizontal: 12, vertical: 4);
|
||||
case BadgeSize.large:
|
||||
return const EdgeInsets.symmetric(horizontal: 16, vertical: 8);
|
||||
}
|
||||
}
|
||||
|
||||
double _getBorderRadius() {
|
||||
switch (size) {
|
||||
case BadgeSize.small:
|
||||
return 12;
|
||||
case BadgeSize.medium:
|
||||
return 16;
|
||||
case BadgeSize.large:
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
double _getFontSize() {
|
||||
switch (size) {
|
||||
case BadgeSize.small:
|
||||
return 10;
|
||||
case BadgeSize.medium:
|
||||
return 12;
|
||||
case BadgeSize.large:
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
double _getIconSize() {
|
||||
switch (size) {
|
||||
case BadgeSize.small:
|
||||
return 12;
|
||||
case BadgeSize.medium:
|
||||
return 14;
|
||||
case BadgeSize.large:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
|
||||
double _getIconSpacing() {
|
||||
switch (size) {
|
||||
case BadgeSize.small:
|
||||
return 4;
|
||||
case BadgeSize.medium:
|
||||
return 6;
|
||||
case BadgeSize.large:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
Decoration _getDecoration(_BadgeConfig config) {
|
||||
switch (variant) {
|
||||
case BadgeVariant.filled:
|
||||
return BoxDecoration(
|
||||
color: config.primaryColor,
|
||||
borderRadius: BorderRadius.circular(_getBorderRadius()),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: config.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
case BadgeVariant.outlined:
|
||||
return BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.all(color: config.borderColor, width: 1),
|
||||
borderRadius: BorderRadius.circular(_getBorderRadius()),
|
||||
);
|
||||
case BadgeVariant.ghost:
|
||||
return BoxDecoration(
|
||||
color: config.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(_getBorderRadius()),
|
||||
);
|
||||
case BadgeVariant.gradient:
|
||||
return BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
config.primaryColor,
|
||||
config.primaryColor.withOpacity(0.7),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(_getBorderRadius()),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: config.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle(_BadgeConfig config) {
|
||||
Color textColor;
|
||||
switch (variant) {
|
||||
case BadgeVariant.filled:
|
||||
case BadgeVariant.gradient:
|
||||
textColor = Colors.white;
|
||||
break;
|
||||
default:
|
||||
textColor = config.primaryColor;
|
||||
}
|
||||
|
||||
return TextStyle(
|
||||
fontSize: _getFontSize(),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
letterSpacing: 0.2,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(_BadgeConfig config) {
|
||||
Color iconColor;
|
||||
switch (variant) {
|
||||
case BadgeVariant.filled:
|
||||
case BadgeVariant.gradient:
|
||||
iconColor = Colors.white;
|
||||
break;
|
||||
default:
|
||||
iconColor = config.primaryColor;
|
||||
}
|
||||
|
||||
if (customIcon != null) {
|
||||
return customIcon!;
|
||||
}
|
||||
|
||||
return Icon(
|
||||
icon,
|
||||
size: _getIconSize(),
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPulseIndicator(Color color) {
|
||||
if (!showPulse) {
|
||||
return Container(
|
||||
width: _getIconSize() * 0.6,
|
||||
height: _getIconSize() * 0.6,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _PulseWidget(
|
||||
size: _getIconSize() * 0.6,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BadgeConfig {
|
||||
final Color primaryColor;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
|
||||
_BadgeConfig({
|
||||
required this.primaryColor,
|
||||
required this.backgroundColor,
|
||||
required this.borderColor,
|
||||
});
|
||||
}
|
||||
|
||||
// Pulse animation widget
|
||||
class _PulseWidget extends StatefulWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const _PulseWidget({
|
||||
required this.size,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PulseWidget> createState() => _PulseWidgetState();
|
||||
}
|
||||
|
||||
class _PulseWidgetState extends State<_PulseWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_controller.repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: 0.8 + (_animation.value * 0.4),
|
||||
child: Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color.withOpacity(1.0 - _animation.value * 0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Extension for easy badge creation
|
||||
extension BadgeBuilder on String {
|
||||
StatusBadge toBadge({
|
||||
BadgeType type = BadgeType.neutral,
|
||||
BadgeSize size = BadgeSize.medium,
|
||||
BadgeVariant variant = BadgeVariant.filled,
|
||||
IconData? icon,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return StatusBadge(
|
||||
text: this,
|
||||
type: type,
|
||||
size: size,
|
||||
variant: variant,
|
||||
icon: icon,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user