Enhanced material design 3 implementation
This commit is contained in:
@@ -3,18 +3,25 @@ import 'package:flutter/material.dart';
|
||||
/// Native Flutter profile avatar that displays either:
|
||||
/// 1. User's avatar image URL (if provided)
|
||||
/// 2. Initials derived from full name (fallback)
|
||||
///
|
||||
/// Pass [heroTag] to participate in a Hero transition to/from a destination
|
||||
/// that uses the same tag (e.g., the profile screen's large avatar).
|
||||
class ProfileAvatar extends StatelessWidget {
|
||||
const ProfileAvatar({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
this.avatarUrl,
|
||||
this.radius = 18,
|
||||
this.heroTag,
|
||||
});
|
||||
|
||||
final String fullName;
|
||||
final String? avatarUrl;
|
||||
final double radius;
|
||||
|
||||
/// When non-null, wraps the avatar in a [Hero] with this tag.
|
||||
final Object? heroTag;
|
||||
|
||||
String _getInitials() {
|
||||
final trimmed = fullName.trim();
|
||||
if (trimmed.isEmpty) return 'U';
|
||||
@@ -28,37 +35,37 @@ class ProfileAvatar extends StatelessWidget {
|
||||
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
|
||||
}
|
||||
|
||||
Color _getInitialsColor(String initials) {
|
||||
// Generate a deterministic color based on initials
|
||||
/// Returns a (background, foreground) pair from the M3 tonal palette.
|
||||
///
|
||||
/// Uses a deterministic hash of the initials to cycle through the scheme's
|
||||
/// semantic container colors so every avatar is theme-aware and accessible.
|
||||
(Color, Color) _getTonalColors(String initials, ColorScheme cs) {
|
||||
final hash =
|
||||
initials.codeUnitAt(0) +
|
||||
(initials.length > 1 ? initials.codeUnitAt(1) * 256 : 0);
|
||||
final colors = [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
|
||||
// Six M3-compliant container pairs (background / on-color text).
|
||||
final pairs = [
|
||||
(cs.primaryContainer, cs.onPrimaryContainer),
|
||||
(cs.secondaryContainer, cs.onSecondaryContainer),
|
||||
(cs.tertiaryContainer, cs.onTertiaryContainer),
|
||||
(cs.errorContainer, cs.onErrorContainer),
|
||||
(cs.primary, cs.onPrimary),
|
||||
(cs.secondary, cs.onSecondary),
|
||||
];
|
||||
return colors[hash % colors.length];
|
||||
|
||||
return pairs[hash % pairs.length];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final initials = _getInitials();
|
||||
|
||||
Widget avatar;
|
||||
|
||||
// If avatar URL is provided, attempt to load the image
|
||||
if (avatarUrl != null && avatarUrl!.isNotEmpty) {
|
||||
return CircleAvatar(
|
||||
avatar = CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundImage: NetworkImage(avatarUrl!),
|
||||
onBackgroundImageError: (_, _) {
|
||||
@@ -66,20 +73,30 @@ class ProfileAvatar extends StatelessWidget {
|
||||
},
|
||||
child: null, // Image will display if loaded successfully
|
||||
);
|
||||
} else {
|
||||
final (bg, fg) = _getTonalColors(
|
||||
initials,
|
||||
Theme.of(context).colorScheme,
|
||||
);
|
||||
|
||||
// Fallback to initials
|
||||
avatar = CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: bg,
|
||||
child: Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
color: fg,
|
||||
fontSize: radius * 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to initials
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: _getInitialsColor(initials),
|
||||
child: Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: radius * 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (heroTag != null) {
|
||||
return Hero(tag: heroTag!, child: avatar);
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user