@@ -20,32 +20,59 @@ class PannableCircleGrid extends StatefulWidget {
20
20
_PannableCircleGridState createState () => _PannableCircleGridState ();
21
21
}
22
22
23
- class _PannableCircleGridState extends State <PannableCircleGrid > {
23
+ class _PannableCircleGridState extends State <PannableCircleGrid >
24
+ with TickerProviderStateMixin {
24
25
static const double _circleSize = 80 ;
25
26
static const double _selectedCircleMultiplier = 2 ;
26
27
static const double _spacing = 10 ;
27
28
static const int _columns = 1000 ;
28
29
29
30
Offset _offset = Offset .zero;
31
+ Map <int , AnimationController > _animationControllers = {};
30
32
int ? _selectedIndex;
31
33
34
+ @override
35
+ void dispose () {
36
+ for (var controller in _animationControllers.values) {
37
+ controller.dispose ();
38
+ }
39
+ super .dispose ();
40
+ }
41
+
42
+ // void _handleAnimationStatus(AnimationStatus status) {
43
+ // if (status == AnimationStatus.completed ||
44
+ // status == AnimationStatus.dismissed) {
45
+ // setState(() {
46
+ // if (_animationController.status == AnimationStatus.dismissed) {
47
+ // _deselectedIndex = null;
48
+ // }
49
+ // }); // Trigger a rebuild to ensure final state is painted
50
+ // }
51
+ // }
52
+
32
53
@override
33
54
Widget build (BuildContext context) {
34
55
return GestureDetector (
35
56
onPanUpdate: _handlePan,
36
57
child: ClipRect (
37
- child: CustomPaint (
38
- painter: CircleGridPainter (
39
- offset: _offset,
40
- circleSize: _circleSize,
41
- selectedCircleMultiplier: _selectedCircleMultiplier,
42
- spacing: _spacing,
43
- selectedIndex: _selectedIndex,
44
- columns: _columns,
45
- ),
46
- child: GestureDetector (
47
- onTapUp: _handleTap,
48
- ),
58
+ child: AnimatedBuilder (
59
+ animation: Listenable .merge (_animationControllers.values.toList ()),
60
+ builder: (context, child) {
61
+ return CustomPaint (
62
+ painter: CircleGridPainter (
63
+ offset: _offset,
64
+ circleSize: _circleSize,
65
+ selectedCircleMultiplier: _selectedCircleMultiplier,
66
+ spacing: _spacing,
67
+ selectedIndex: _selectedIndex,
68
+ columns: _columns,
69
+ animationControllers: _animationControllers,
70
+ ),
71
+ child: GestureDetector (
72
+ onTapUp: _handleTap,
73
+ ),
74
+ );
75
+ },
49
76
),
50
77
),
51
78
);
@@ -63,10 +90,73 @@ class _PannableCircleGridState extends State<PannableCircleGrid> {
63
90
((tapPosition.dx - _offset.dx) / (_circleSize + _spacing)).floor ();
64
91
int row =
65
92
((tapPosition.dy - _offset.dy) / (_circleSize + _spacing)).floor ();
66
- int index = row * _columns + col;
93
+ int tappedIndex = row * _columns + col;
94
+
67
95
setState (() {
68
- _selectedIndex = (_selectedIndex == index) ? null : index;
96
+ if (_selectedIndex == tappedIndex) {
97
+ // Case 1: Tapping the same circle
98
+ _animationControllers[tappedIndex]? .reverse ().then ((_) {
99
+ if (mounted) {
100
+ setState (() {
101
+ _animationControllers.remove (tappedIndex);
102
+ _selectedIndex = null ;
103
+ });
104
+ }
105
+ });
106
+ } else {
107
+ // Case 2: Tapping a different circle
108
+ if (_selectedIndex != null ) {
109
+ int ? __selectedIndex = _selectedIndex;
110
+ // Collapse the previously selected circle
111
+ _animationControllers[_selectedIndex]? .reverse ().then ((_) {
112
+ if (mounted) {
113
+ setState (() {
114
+ _animationControllers.remove (__selectedIndex);
115
+ });
116
+ }
117
+ });
118
+ }
119
+
120
+ // Expand the newly tapped circle
121
+ _animationControllers[tappedIndex] = AnimationController (
122
+ duration: const Duration (milliseconds: 300 ),
123
+ vsync: this ,
124
+ )..forward ();
125
+
126
+ _selectedIndex = tappedIndex;
127
+ }
69
128
});
129
+
130
+ // if (_animationControllers.containsKey(tappedIndex)) {
131
+ // // Collapse the tapped circle
132
+ // _animationControllers[tappedIndex]!.reverse().then((_) {
133
+ // setState(() {
134
+ // _animationControllers.remove(tappedIndex);
135
+ // if (_selectedIndex == tappedIndex) {
136
+ // _selectedIndex = null;
137
+ // }
138
+ // });
139
+ // });
140
+ // } else {
141
+ // // Expand the tapped circle
142
+ // _animationControllers[tappedIndex] = AnimationController(
143
+ // duration: const Duration(milliseconds: 300),
144
+ // vsync: this,
145
+ // )..forward();
146
+
147
+ // // Collapse the previously selected circle, if any
148
+ // if (_selectedIndex != null && _selectedIndex != tappedIndex) {
149
+ // _animationControllers[_selectedIndex!]?.reverse().then((_) {
150
+ // setState(() {
151
+ // _animationControllers.remove(_selectedIndex);
152
+ // });
153
+ // });
154
+ // }
155
+
156
+ // setState(() {
157
+ // _selectedIndex = tappedIndex;
158
+ // });
159
+ // }
70
160
}
71
161
}
72
162
@@ -77,6 +167,7 @@ class CircleGridPainter extends CustomPainter {
77
167
final double spacing;
78
168
final int ? selectedIndex;
79
169
final int columns;
170
+ final Map <int , AnimationController > animationControllers;
80
171
81
172
static const double maxDisplacementDistance =
82
173
6.0 ; // Maximum distance for displacement effect
@@ -92,6 +183,7 @@ class CircleGridPainter extends CustomPainter {
92
183
required this .spacing,
93
184
required this .columns,
94
185
this .selectedIndex,
186
+ required this .animationControllers,
95
187
});
96
188
97
189
@override
@@ -126,10 +218,14 @@ class CircleGridPainter extends CustomPainter {
126
218
127
219
Map <Point <int >, Offset > _calculateDisplacements (_VisibleArea visibleArea) {
128
220
Map <Point <int >, Offset > displacements = {};
129
- if (selectedIndex != null ) {
130
- int selectedCol = selectedIndex! % columns;
131
- int selectedRow = selectedIndex! ~ / columns;
132
- double expansionAmount = circleSize * (selectedCircleMultiplier - 1 ) / 2 ;
221
+ for (var entry in animationControllers.entries) {
222
+ int selectedIndex = entry.key;
223
+ double animationValue = entry.value.value;
224
+
225
+ int selectedCol = selectedIndex % columns;
226
+ int selectedRow = selectedIndex ~ / columns;
227
+ double expansionAmount =
228
+ circleSize * (selectedCircleMultiplier - 1 ) / 2 * animationValue;
133
229
double defaultSpacing = circleSize + spacing;
134
230
135
231
for (int row = visibleArea.startRow - 2 ;
@@ -176,10 +272,17 @@ class CircleGridPainter extends CustomPainter {
176
272
}
177
273
178
274
if (displacement > 0 ) {
179
- displacements[Point (col, row)] = Offset (
275
+ Point <int > point = Point (col, row);
276
+ Offset newDisplacement = Offset (
180
277
cos (angle) * displacement,
181
278
sin (angle) * displacement,
182
279
);
280
+
281
+ if (displacements.containsKey (point)) {
282
+ displacements[point] = displacements[point]! + newDisplacement;
283
+ } else {
284
+ displacements[point] = newDisplacement;
285
+ }
183
286
}
184
287
}
185
288
@@ -214,24 +317,44 @@ class CircleGridPainter extends CustomPainter {
214
317
for (int row = visibleArea.startRow; row <= visibleArea.endRow; row++ ) {
215
318
for (int col = visibleArea.startCol; col <= visibleArea.endCol; col++ ) {
216
319
int index = row * columns + col;
217
- bool isSelected = selectedIndex == index ;
320
+ bool isSelected = index == selectedIndex ;
218
321
219
322
Offset circleOffset = Offset (
220
323
col * (circleSize + spacing) + offset.dx,
221
324
row * (circleSize + spacing) + offset.dy,
222
325
);
223
326
224
- if (! isSelected && displacements.containsKey (Point (col, row))) {
327
+ // if (!isSelected && displacements.containsKey(Point(col, row))) {
328
+ // circleOffset += displacements[Point(col, row)]! * animationValue;
329
+ // }
330
+ if (displacements.containsKey (Point (col, row))) {
225
331
circleOffset += displacements[Point (col, row)]! ;
226
332
}
227
333
228
- double currentCircleSize =
229
- isSelected ? circleSize * selectedCircleMultiplier : circleSize;
334
+ double currentCircleSize = circleSize;
335
+ Paint currentPaint = paint;
336
+
337
+ if (animationControllers.containsKey (index)) {
338
+ double animationValue = animationControllers[index]? .value ?? 0.0 ;
339
+ currentCircleSize +=
340
+ (circleSize * (selectedCircleMultiplier - 1 ) * animationValue);
341
+ currentPaint = Paint ()
342
+ ..color =
343
+ Color .lerp (paint.color, selectedPaint.color, animationValue)!
344
+ ..style = PaintingStyle .fill;
345
+ }
346
+
347
+ // double currentCircleSize = isSelected
348
+ // ? circleSize +
349
+ // (circleSize *
350
+ // (selectedCircleMultiplier - 1) *
351
+ // animationControllers[index]!.value)
352
+ // : circleSize;
230
353
231
354
canvas.drawCircle (
232
355
circleOffset,
233
356
currentCircleSize / 2 ,
234
- isSelected ? selectedPaint : paint ,
357
+ currentPaint ,
235
358
);
236
359
237
360
_drawText (
@@ -255,9 +378,10 @@ class CircleGridPainter extends CustomPainter {
255
378
}
256
379
257
380
@override
258
- bool shouldRepaint (covariant CircleGridPainter oldDelegate) =>
259
- offset != oldDelegate.offset ||
260
- selectedIndex != oldDelegate.selectedIndex;
381
+ bool shouldRepaint (covariant CircleGridPainter oldDelegate) => true ;
382
+ // =>
383
+ // offset != oldDelegate.offset ||
384
+ // animationControllers != oldDelegate.animationControllers;
261
385
}
262
386
263
387
class _VisibleArea {
0 commit comments