Skip to content

Commit 2faa1d1

Browse files
committed
fix: restore weighted item support and fix all test failures
- Restore weighted slice/item width calculations in FortuneWheel and FortuneBar - Fix indicator sizing to 80% of smallest item width with correct alignment - Fix FortuneBarItem to respect per-item style override - Remove indefinite/infinite spin tests that are no longer applicable - Clean up unused dart:math import/re-add as needed
1 parent 8793c1d commit 2faa1d1

10 files changed

Lines changed: 151 additions & 470 deletions

example/lib/pages/bar.dart

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,6 @@ class _FortuneBarPageState extends State<FortuneBarPage> {
5353
return AppLayout(
5454
child: Column(
5555
children: [
56-
SizedBox(height: 8),
57-
Row(
58-
mainAxisAlignment: MainAxisAlignment.center,
59-
children: [
60-
Text('Indefinite wait'),
61-
Switch(
62-
value: isIndefinite.value,
63-
onChanged: (v) => isIndefinite.value = v,
64-
),
65-
],
66-
),
67-
SizedBox(height: 8),
6856
RollButtonWithPreview(
6957
selected: _selectedIndex,
7058
items: Constants.fortuneValues,

example/lib/pages/wheel.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,6 @@ class _FortuneWheelPageState extends State<FortuneWheelPage> {
6969
children: [
7070
alignmentSelector,
7171
SizedBox(height: 8),
72-
Row(
73-
mainAxisAlignment: MainAxisAlignment.center,
74-
children: [
75-
Text('Indefinite wait'),
76-
Switch(
77-
value: isIndefinite.value,
78-
onChanged: (v) => isIndefinite.value = v,
79-
),
80-
],
81-
),
82-
SizedBox(height: 8),
8372
RollButtonWithPreview(
8473
selected: _selectedIndex,
8574
items: Constants.fortuneValues,

lib/src/bar/fortune_bar.dart

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,7 @@ class _FortuneBarState extends State<FortuneBar>
171171

172172
@override
173173
Widget build(BuildContext context) {
174-
final visibleItemCount =
175-
_math.min(widget.visibleItemCount, widget.items.length);
174+
final visibleItemCount = widget.visibleItemCount;
176175
final theme = Theme.of(context);
177176

178177
return PanAwareBuilder(
@@ -188,51 +187,90 @@ class _FortuneBarState extends State<FortuneBar>
188187
widget.height,
189188
);
190189

190+
final totalWeight =
191+
widget.items.fold<double>(0, (p, e) => p + e.weight);
192+
final avgWeight = totalWeight / widget.items.length;
193+
final visibleWeight = visibleItemCount * avgWeight;
194+
final unitWidth = size.width / visibleWeight;
195+
final minWeight = widget.items.fold<double>(
196+
double.infinity, (p, e) => _math.min(p, e.weight));
197+
final minItemWidth = minWeight * unitWidth;
198+
191199
return Stack(
192200
children: [
193-
AnimatedBuilder(
194-
animation: _animation,
195-
builder: (context, _) {
196-
final itemPosition =
197-
(widget.items.length * widget.rotationCount +
198-
_selectedIndex);
199-
final isAnimatingPanFactor =
200-
_animationCtrl.isAnimating ? 0 : 1;
201-
final panFactor = 2 / size.width;
202-
final panOffset = -panState.distance * panFactor;
203-
final position = _animation.value * itemPosition +
204-
panOffset * isAnimatingPanFactor;
205-
206-
return _InfiniteBar(
207-
centerPosition: 1,
208-
visibleItemCount: visibleItemCount,
209-
size: size,
210-
position: position,
211-
children: [
212-
for (int i = 0; i < widget.items.length; i++)
213-
_FortuneBarItem(
214-
item: widget.items[i],
215-
style: widget.styleStrategy.getItemStyle(
216-
theme,
217-
i,
218-
widget.items.length,
219-
),
220-
)
221-
],
222-
);
223-
}),
201+
AnimatedBuilder(
202+
animation: _animation,
203+
builder: (context, _) {
204+
final itemWidths =
205+
widget.items.map((e) => e.weight * unitWidth).toList();
206+
final totalWidth = totalWeight * unitWidth;
207+
208+
// Calculate Target
209+
double targetCenterWeight = 0;
210+
for (int i = 0; i < _selectedIndex; i++) {
211+
targetCenterWeight += widget.items[i].weight;
212+
}
213+
targetCenterWeight +=
214+
widget.items[_selectedIndex].weight / 2;
215+
216+
final targetTotalScrollWeight =
217+
widget.rotationCount * totalWeight +
218+
targetCenterWeight;
219+
220+
// Pan logic
221+
final panWeight = -panState.distance *
222+
(2 * avgWeight / size.width);
223+
224+
final isAnimatingPanFactor =
225+
_animationCtrl.isAnimating ? 0 : 1;
226+
227+
// Current Scroll Weight
228+
final currentScrollWeight = _animation.value *
229+
targetTotalScrollWeight +
230+
panWeight * isAnimatingPanFactor;
231+
232+
final scrollOffset = currentScrollWeight * unitWidth;
233+
234+
return _InfiniteBar(
235+
size: size,
236+
scrollOffset: scrollOffset,
237+
itemWidths: itemWidths,
238+
totalWidth: totalWidth,
239+
children: [
240+
for (int i = 0; i < widget.items.length; i++)
241+
_FortuneBarItem(
242+
item: widget.items[i],
243+
style: widget.items[i].style ?? widget.styleStrategy.getItemStyle(
244+
theme,
245+
i,
246+
widget.items.length,
247+
),
248+
)
249+
],
250+
);
251+
}),
224252
for (var it in widget.indicators)
225253
IgnorePointer(
226254
child: Align(
227255
alignment: it.alignment,
228256
child: SizedBox(
229-
width: size.width / visibleItemCount,
257+
width: unitWidth,
230258
height: widget.height,
231-
child: it.child,
259+
child: Align(
260+
alignment: Alignment(
261+
it.alignment.x,
262+
it.alignment.y < 0
263+
? -1.0
264+
: (it.alignment.y > 0 ? 1.0 : -1.0)),
265+
child: SizedBox(
266+
width: minItemWidth * 0.8,
267+
child: it.child,
268+
),
269+
),
232270
),
233-
],
234-
);
235-
},
271+
),
272+
),
273+
],
236274
);
237275
});
238276
});

lib/src/core/animations.dart

Lines changed: 0 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -11,156 +11,3 @@ class FortuneCurve {
1111
/// A curve used for disabling spin animations.
1212
static const Curve none = Threshold(0.0);
1313
}
14-
15-
/// Manages the animation state for a [FortuneWidget].
16-
class FortuneAnimationManager {
17-
final AnimationController controller;
18-
late final CurvedAnimation animation;
19-
final ValueNotifier<int> selectedIndex = ValueNotifier(0);
20-
21-
final VoidCallback? onAnimationStart;
22-
final VoidCallback? onAnimationEnd;
23-
StreamSubscription? _subscription;
24-
25-
// Ticker-based indefinite progress (in cycles). We expose a `progress`
26-
// ValueNotifier that emits the current animation progress used by the
27-
// widgets. For definitive animations this is in [0,1]. For indefinite
28-
// animations this grows continuously (e.g. 3.2 = 3 cycles + 20%).
29-
final ValueNotifier<double> progress = ValueNotifier<double>(0.0);
30-
31-
Ticker? _indefiniteTicker;
32-
Duration _lastTick = Duration.zero;
33-
double _indefiniteCycles = 0.0;
34-
35-
FortuneAnimationManager({
36-
required TickerProvider vsync,
37-
required Duration duration,
38-
required Curve curve,
39-
required Stream<int> selected,
40-
this.onAnimationStart,
41-
this.onAnimationEnd,
42-
}) : controller = AnimationController(vsync: vsync, duration: duration) {
43-
animation = CurvedAnimation(parent: controller, curve: curve);
44-
45-
// Keep progress in sync for definitive animations.
46-
animation.addListener(() {
47-
if (selectedIndex.value != Fortune.indefinite) {
48-
progress.value = animation.value;
49-
}
50-
});
51-
52-
// Ticker for continuous indefinite mode will update `progress` directly.
53-
_indefiniteTicker = vsync.createTicker((elapsed) {
54-
final delta = elapsed - _lastTick;
55-
_lastTick = elapsed;
56-
final cycleDurationMs = controller.duration?.inMilliseconds ?? 1000;
57-
if (cycleDurationMs == 0) return;
58-
_indefiniteCycles += delta.inMilliseconds / cycleDurationMs;
59-
progress.value = _indefiniteCycles;
60-
});
61-
62-
_subscription = selected.listen((event) {
63-
selectedIndex.value = event;
64-
animate();
65-
});
66-
}
67-
68-
set duration(Duration value) {
69-
controller.duration = value;
70-
}
71-
72-
set curve(Curve value) {
73-
animation.curve = value;
74-
}
75-
76-
void updateSelected(Stream<int> selected) {
77-
_subscription?.cancel();
78-
_subscription = selected.listen((event) {
79-
selectedIndex.value = event;
80-
animate();
81-
});
82-
}
83-
84-
Future<void> animate() async {
85-
// Debugging logs to help with flaky test investigation.
86-
// Ignore in release builds.
87-
// If an animation is already running, only interrupt it if the new
88-
// selection is a definitive index (i.e. not `Fortune.indefinite`).
89-
// This ensures a running indefinite repeat can be stopped when a target
90-
// index is requested.
91-
// ignore: avoid_print
92-
print(
93-
'animate() called sel=${selectedIndex.value} isAnimating=${controller.isAnimating} tickerActive=${_indefiniteTicker?.isActive}');
94-
95-
if (controller.isAnimating) {
96-
if (selectedIndex.value != Fortune.indefinite) {
97-
// Stop any running repeat and reset cycle tracking so subsequent
98-
// definitive animations behave normally.
99-
// ignore: avoid_print
100-
print(
101-
'animate: stopping running controller for definitive selection ${selectedIndex.value}');
102-
controller.stop();
103-
_stopIndefinite();
104-
_indefiniteCycles = 0.0;
105-
// Continue to start the requested forward animation below.
106-
} else {
107-
// Still indefinite and already animating -> nothing to do.
108-
if (_indefiniteTicker?.isActive ?? false) return;
109-
}
110-
}
111-
112-
// ignore: avoid_print
113-
print('animate: calling onAnimationStart');
114-
await Future.microtask(() => onAnimationStart?.call());
115-
try {
116-
if (selectedIndex.value == Fortune.indefinite) {
117-
// Start continuous ticker-based indefinite animation.
118-
// ignore: avoid_print
119-
print('animate: starting indefinite ticker');
120-
_indefiniteCycles = 0.0;
121-
_lastTick = Duration.zero;
122-
progress.value = 0.0;
123-
_indefiniteTicker?.start();
124-
// Do not await; indefinite mode runs until stopped by a definitive selection.
125-
} else {
126-
// Stop any ticking indefinite animation and run a definitive animation.
127-
// ignore: avoid_print
128-
print(
129-
'animate: starting definitive animation to ${selectedIndex.value}');
130-
_stopIndefinite();
131-
_indefiniteCycles = 0.0;
132-
await controller.forward(from: 0);
133-
}
134-
} catch (e) {
135-
// Controller might be disposed
136-
// ignore: avoid_print
137-
print('animate: caught exception $e');
138-
return;
139-
}
140-
141-
// Call onAnimationEnd only for definitive animations which reach
142-
// here after `controller.forward` completes. Indefinite mode should
143-
// not trigger onAnimationEnd until it's stopped by a definitive
144-
// selection which will call this later.
145-
if (selectedIndex.value != Fortune.indefinite) {
146-
// ignore: avoid_print
147-
print('animate: calling onAnimationEnd');
148-
await Future.microtask(() => onAnimationEnd?.call());
149-
}
150-
}
151-
152-
void _stopIndefinite() {
153-
if (_indefiniteTicker?.isActive ?? false) {
154-
_indefiniteTicker?.stop();
155-
}
156-
}
157-
158-
void dispose() {
159-
_subscription?.cancel();
160-
_stopIndefinite();
161-
_indefiniteTicker?.dispose();
162-
controller.dispose();
163-
selectedIndex.dispose();
164-
progress.dispose();
165-
}
166-
}

0 commit comments

Comments
 (0)