Skip to content

Commit 30d58c8

Browse files
committed
Avatar Circles
1 parent 1c25d7a commit 30d58c8

10 files changed

+373
-1
lines changed

ios/Podfile.lock

+6
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ PODS:
88
- sqflite_darwin (0.0.4):
99
- Flutter
1010
- FlutterMacOS
11+
- url_launcher_ios (0.0.1):
12+
- Flutter
1113

1214
DEPENDENCIES:
1315
- Flutter (from `Flutter`)
1416
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
1517
- sensors (from `.symlinks/plugins/sensors/ios`)
1618
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
19+
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
1720

1821
EXTERNAL SOURCES:
1922
Flutter:
@@ -24,12 +27,15 @@ EXTERNAL SOURCES:
2427
:path: ".symlinks/plugins/sensors/ios"
2528
sqflite_darwin:
2629
:path: ".symlinks/plugins/sqflite_darwin/darwin"
30+
url_launcher_ios:
31+
:path: ".symlinks/plugins/url_launcher_ios/ios"
2732

2833
SPEC CHECKSUMS:
2934
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
3035
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
3136
sensors: 84eb7a30e47a649e4172b71d6e81be614c280336
3237
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
38+
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
3339

3440
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
3541

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// avatar_circles.dart
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:fx_2_folder/stacked-cards/stacked_card.dart';
5+
import 'package:url_launcher/url_launcher.dart';
6+
7+
// avatar_circles.dart
8+
9+
import 'package:flutter/material.dart';
10+
import 'package:url_launcher/url_launcher.dart';
11+
12+
/// Represents an individual avatar with its image and profile URLs
13+
class Avatar {
14+
final String imageUrl;
15+
final String profileUrl;
16+
17+
const Avatar({
18+
required this.imageUrl,
19+
required this.profileUrl,
20+
});
21+
}
22+
23+
/// A widget that displays a row of overlapping circular avatars with an optional
24+
/// count indicator for additional people
25+
class AvatarCircles extends StatelessWidget {
26+
/// List of avatars to display
27+
final List<Avatar> avatars;
28+
29+
/// Number of additional people to show in the count indicator
30+
final int? additionalPeople;
31+
32+
/// Size of each avatar circle
33+
final double size;
34+
35+
/// Border width around each avatar
36+
final double borderWidth;
37+
38+
/// Background color for the count indicator
39+
final Color? countBackgroundColor;
40+
41+
/// Text color for the count indicator
42+
final Color? countTextColor;
43+
44+
/// Border color for the avatars
45+
final Color? borderColor;
46+
47+
/// Overlap amount between avatars (positive value)
48+
final double overlap;
49+
50+
const AvatarCircles({
51+
Key? key,
52+
required this.avatars,
53+
this.additionalPeople,
54+
this.size = 40,
55+
this.borderWidth = 2,
56+
this.countBackgroundColor,
57+
this.countTextColor,
58+
this.borderColor,
59+
this.overlap = 16,
60+
}) : super(key: key);
61+
62+
@override
63+
Widget build(BuildContext context) {
64+
final theme = Theme.of(context);
65+
final isDark = theme.brightness == Brightness.dark;
66+
67+
// Calculate total width needed
68+
final totalAvatars = avatars.length + (additionalPeople != null ? 1 : 0);
69+
final totalWidth = size + (totalAvatars - 1) * (size - overlap);
70+
71+
return SizedBox(
72+
width: totalWidth,
73+
height: size,
74+
child: Stack(
75+
children: [
76+
// Render avatars
77+
...avatars.asMap().entries.map((entry) {
78+
final index = entry.key;
79+
final avatar = entry.value;
80+
81+
return Positioned(
82+
left: index * (size - overlap),
83+
child: GestureDetector(
84+
onTap: () async {
85+
final url = Uri.parse(avatar.profileUrl);
86+
if (await canLaunchUrl(url)) {
87+
await launchUrl(url);
88+
}
89+
},
90+
child: Container(
91+
width: size,
92+
height: size,
93+
decoration: BoxDecoration(
94+
shape: BoxShape.circle,
95+
border: Border.all(
96+
color: borderColor ??
97+
(isDark
98+
? theme.colorScheme.surface
99+
: theme.colorScheme.background),
100+
width: borderWidth,
101+
),
102+
),
103+
child: ClipRRect(
104+
borderRadius: BorderRadius.circular(size / 2),
105+
child: Image.network(
106+
avatar.imageUrl,
107+
width: size,
108+
height: size,
109+
fit: BoxFit.cover,
110+
),
111+
),
112+
),
113+
),
114+
);
115+
}),
116+
117+
// Render additional people counter if needed
118+
if (additionalPeople != null && additionalPeople! > 0)
119+
Positioned(
120+
left: avatars.length * (size - overlap),
121+
child: Container(
122+
width: size,
123+
height: size,
124+
decoration: BoxDecoration(
125+
shape: BoxShape.circle,
126+
color: countBackgroundColor ??
127+
(isDark ? theme.colorScheme.surface : Colors.black),
128+
border: Border.all(
129+
color: borderColor ??
130+
(isDark
131+
? theme.colorScheme.surface
132+
: theme.colorScheme.background),
133+
width: borderWidth,
134+
),
135+
),
136+
child: Center(
137+
child: Text(
138+
'+${additionalPeople}',
139+
style: theme.textTheme.labelSmall?.copyWith(
140+
color: countTextColor ??
141+
(isDark
142+
? Colors.black
143+
: theme.colorScheme.background),
144+
fontWeight: FontWeight.w500,
145+
),
146+
),
147+
),
148+
),
149+
),
150+
],
151+
),
152+
);
153+
}
154+
}
155+
156+
class AvatarCirclesShowcase extends StatelessWidget {
157+
final List<Avatar> sampleAvatars = [
158+
const Avatar(
159+
imageUrl:
160+
'https://cdn.bsky.app/img/avatar/plain/did:plc:34qtss66g2so6rudozx7iri7/bafkreiay2zk2ivksxufahyoewrfnpk2ne4urcm4alkgy3zsum7qlgmnh6y@jpeg',
161+
profileUrl: 'https://bsky.app/profile/escamoteur.bsky.social',
162+
),
163+
const Avatar(
164+
imageUrl:
165+
'https://cdn.bsky.app/img/avatar/plain/did:plc:vnq2ph2haugwbb4xbksqnvci/bafkreigzrqcabzfwpzmftrh3seitqr7djdd6jop35ltc4lxev5bsw5muxa@jpeg',
166+
profileUrl: 'https://bsky.app/profile/dariadroid.flutter.community',
167+
),
168+
const Avatar(
169+
imageUrl:
170+
'https://cdn.bsky.app/img/avatar/plain/did:plc:yfqc4hzvri2nmaiifrnhs2y2/bafkreiasb3wslfq5nbwo6et5q3rrysoxj3qbpk6hdqrjsewyktcn2zpqwq@jpeg',
171+
profileUrl: 'https://bsky.app/profile/sethladd.com',
172+
),
173+
const Avatar(
174+
imageUrl:
175+
'https://cdn.bsky.app/img/avatar/plain/did:plc:qibza37i7hkd7phfymqief2l/bafkreih6enfkkwl5ejcm27mzkri7kjfxpm2kxqskaep56nadztvgpait74@jpeg',
176+
profileUrl: 'https://bsky.app/profile/eseidel.com',
177+
),
178+
];
179+
180+
AvatarCirclesShowcase({Key? key}) : super(key: key);
181+
182+
@override
183+
Widget build(BuildContext context) {
184+
return Scaffold(
185+
body: Stack(
186+
children: [
187+
CustomPaint(
188+
painter: GridPatternPainter(isDarkMode: true),
189+
size: Size.infinite,
190+
),
191+
Center(
192+
child: Padding(
193+
padding: const EdgeInsets.all(16.0),
194+
child: Column(
195+
crossAxisAlignment: CrossAxisAlignment.center,
196+
mainAxisAlignment: MainAxisAlignment.center,
197+
children: [
198+
_buildShowcaseSection(
199+
'Default Style',
200+
AvatarCircles(
201+
avatars: sampleAvatars.take(3).toList(),
202+
additionalPeople: 5,
203+
),
204+
),
205+
_buildShowcaseSection(
206+
'Large Size with Custom Colors',
207+
AvatarCircles(
208+
avatars: sampleAvatars.take(2).toList(),
209+
additionalPeople: 3,
210+
size: 60,
211+
borderWidth: 3,
212+
overlap: 24,
213+
borderColor: Colors.blue[300],
214+
countBackgroundColor: Colors.blue,
215+
countTextColor: Colors.white,
216+
),
217+
),
218+
_buildShowcaseSection(
219+
'Small Size with More Overlap',
220+
AvatarCircles(
221+
avatars: sampleAvatars,
222+
size: 32,
223+
overlap: 20,
224+
borderWidth: 1.5,
225+
),
226+
),
227+
_buildShowcaseSection(
228+
'Custom Themed',
229+
Container(
230+
padding: const EdgeInsets.all(16),
231+
color: Colors.grey[900],
232+
child: AvatarCircles(
233+
avatars: sampleAvatars.take(4).toList(),
234+
additionalPeople: 2,
235+
borderColor: Colors.grey[800],
236+
countBackgroundColor: Colors.purple,
237+
countTextColor: Colors.white,
238+
),
239+
),
240+
),
241+
_buildShowcaseSection(
242+
'Minimal Overlap',
243+
AvatarCircles(
244+
avatars: sampleAvatars.take(4).toList(),
245+
overlap: 8,
246+
borderWidth: 2,
247+
size: 40,
248+
),
249+
),
250+
],
251+
),
252+
),
253+
),
254+
],
255+
),
256+
);
257+
}
258+
259+
Widget _buildShowcaseSection(String title, Widget content) {
260+
return Padding(
261+
padding: const EdgeInsets.symmetric(vertical: 16.0),
262+
child: Column(
263+
crossAxisAlignment: CrossAxisAlignment.center,
264+
children: [
265+
Text(
266+
title,
267+
style: const TextStyle(
268+
fontSize: 18,
269+
fontWeight: FontWeight.bold,
270+
),
271+
),
272+
const SizedBox(height: 12),
273+
content,
274+
],
275+
),
276+
);
277+
}
278+
}

lib/main.dart

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
3+
import 'package:fx_2_folder/avatar-circles/avatar_circles.dart';
34
import 'package:fx_2_folder/background-aurora/aurora_widget.dart';
45
import 'package:fx_2_folder/background-aurora/aurora_widget_demo.dart';
56
import 'package:fx_2_folder/background-beam/background_beam_demo.dart';
@@ -460,7 +461,13 @@ class HomeScreen extends StatelessWidget {
460461
appBarColor: Colors.black,
461462
isFullScreen: true,
462463
),
463-
];
464+
AnimationExample(
465+
title: "Avatar Circles",
466+
builder: (context) => AvatarCirclesShowcase(),
467+
appBarColor: Colors.black,
468+
isFullScreen: true,
469+
),
470+
]; //AvatarCirclesShowcase
464471

465472
HomeScreen({super.key});
466473

linux/flutter/generated_plugin_registrant.cc

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
#include "generated_plugin_registrant.h"
88

9+
#include <url_launcher_linux/url_launcher_plugin.h>
910

1011
void fl_register_plugins(FlPluginRegistry* registry) {
12+
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
13+
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
14+
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
1115
}

linux/flutter/generated_plugins.cmake

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44

55
list(APPEND FLUTTER_PLUGIN_LIST
6+
url_launcher_linux
67
)
78

89
list(APPEND FLUTTER_FFI_PLUGIN_LIST

macos/Flutter/GeneratedPluginRegistrant.swift

+2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import Foundation
77

88
import path_provider_foundation
99
import sqflite_darwin
10+
import url_launcher_macos
1011

1112
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
1213
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
1314
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
15+
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
1416
}

0 commit comments

Comments
 (0)