Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions lib/src/animate_to_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class AnimateToItem {
required this.position,
required this.duration,
required this.curve,
required this.whenCompleteOrCancel,
});

final ExtentManager extentManager;
Expand All @@ -22,10 +23,27 @@ class AnimateToItem {
final ScrollPosition position;
final Duration Function(double estimatedDistance) duration;
final Curve Function(double estimatedDistance) curve;
final void Function() whenCompleteOrCancel;

double lastPosition = 0.0;

late AnimationController _controller;
late CurvedAnimation _animation;

bool _mustDispose = false;

void dispose() {
if (_mustDispose) {
_mustDispose = false;
_controller.dispose();
_animation.dispose();
whenCompleteOrCancel();
}
}

void animate() {
_mustDispose = true;

final index = this.index();
if (index == null) {
return;
Expand All @@ -38,25 +56,19 @@ class AnimateToItem {
estimationOnly: true,
);
final estimatedDistance = (estimatedTarget - start).abs();
final controller = AnimationController(
_controller = AnimationController(
vsync: position.context.vsync,
duration: duration(estimatedDistance),
);
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.dispose();
}
});
final animation = CurvedAnimation(
parent: controller,
_animation = CurvedAnimation(
parent: _controller,
curve: curve(estimatedDistance),
);
animation.addListener(() {
final value = animation.value;
_animation.addListener(() {
final value = _animation.value;
final index = this.index();
if (index == null) {
controller.stop();
controller.dispose();
dispose();
return;
}
var targetPosition = extentManager.getOffsetToReveal(
Expand All @@ -83,6 +95,6 @@ class AnimateToItem {
}
position.jumpTo(jumpPosition);
});
controller.forward();
_controller.forward().whenCompleteOrCancel(dispose);
}
}
20 changes: 18 additions & 2 deletions lib/src/super_sliver_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,20 @@ class ListController extends ChangeNotifier {
}) {
assert(_delegate != null, "ListController is not attached.");
for (final position in scrollController.positions) {
AnimateToItem(
late final AnimateToItem animation;
animation = AnimateToItem(
Comment thread
Zekfad marked this conversation as resolved.
extentManager: _delegate!,
index: index,
alignment: alignment,
rect: rect,
position: position,
curve: curve,
duration: duration,
).animate();
// clean added handle if it's completed normally
whenCompleteOrCancel: () => _runningAnimations.remove(animation)
);
_runningAnimations.add(animation);
animation.animate();
}
}

Expand Down Expand Up @@ -249,6 +254,11 @@ class ListController extends ChangeNotifier {
super.dispose();
}

/// Keeps track of created [AnimateToItem] so we could later dispose
/// [AnimationController]s and animations in case list controller is suddenly
/// unattached.
final List<AnimateToItem> _runningAnimations = [];

ExtentManager? _delegate;

void setDelegate(ExtentManager delegate) {
Expand All @@ -270,6 +280,12 @@ class ListController extends ChangeNotifier {
if (_delegate == delegate) {
_delegate?.removeListener(notifyListeners);
_delegate = null;
// because list can be modified from callback that can be called from
// [AnimateToItem.dispose] we must iterate over copy
for (final controller in _runningAnimations.toList()) {
Comment thread
Zekfad marked this conversation as resolved.
controller.dispose();
}
_runningAnimations.clear();
onDetached?.call();
}
}
Expand Down
47 changes: 47 additions & 0 deletions test/super_sliver_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,53 @@ void main() async {
expect(detached, 2);
expect(controller.isAttached, false);
});
testWidgets("remove widget mid animation", (tester) async {
final scrollController = ScrollController();

int attached = 0;
int detached = 0;
final controller = ListController(
onAttached: () {
++attached;
},
onDetached: () {
++detached;
},
);
final configuration = SliverListConfiguration.generate(
slivers: 1,
itemsPerSliver: (_) => 20,
itemHeight: (_, __) => 300,
viewportHeight: 500,
addGlobalKey: true,
);
await tester.pumpWidget(_buildSliverList(
configuration,
listController: controller,
preciseLayout: false,
controller: scrollController,
));
await tester.pumpAndSettle();
expect(attached, 1);
expect(detached, 0);
expect(controller.isAttached, isTrue);

controller.animateToItem(
index: () => 10,
scrollController: scrollController,
alignment: 0.5,
duration: (estimatedDistance) => const Duration(milliseconds: 1000),
curve: (estimatedDistance) => Curves.linear,
);

await tester.pump(const Duration(milliseconds: 500));

// will throw if there are any leaked Tickers
await tester.pumpWidget(const SizedBox.shrink());
expect(attached, 1);
expect(detached, 1);
expect(controller.isAttached, false);
});
testWidgets("replace controller", (tester) async {
int attached1 = 0;
int detached1 = 0;
Expand Down