83 lines
3.2 KiB
Dart
83 lines
3.2 KiB
Dart
import 'package:flutter/physics.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
// adapted from Flutter `FixedExtentScrollPhysics` in `/widgets/list_wheel_scroll_view.dart`
|
|
class KnownExtentScrollPhysics extends ScrollPhysics {
|
|
final double Function(int index) indexToScrollOffset;
|
|
final int Function(double offset) scrollOffsetToIndex;
|
|
|
|
const KnownExtentScrollPhysics({
|
|
required this.indexToScrollOffset,
|
|
required this.scrollOffsetToIndex,
|
|
ScrollPhysics? parent,
|
|
}) : super(parent: parent);
|
|
|
|
@override
|
|
KnownExtentScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
|
return KnownExtentScrollPhysics(
|
|
indexToScrollOffset: indexToScrollOffset,
|
|
scrollOffsetToIndex: scrollOffsetToIndex,
|
|
parent: buildParent(ancestor),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
|
|
final ScrollMetrics metrics = position;
|
|
|
|
// Scenario 1:
|
|
// If we're out of range and not headed back in range, defer to the parent
|
|
// ballistics, which should put us back in range at the scrollable's boundary.
|
|
if ((velocity <= 0.0 && metrics.pixels <= metrics.minScrollExtent) || (velocity >= 0.0 && metrics.pixels >= metrics.maxScrollExtent)) {
|
|
return super.createBallisticSimulation(metrics, velocity);
|
|
}
|
|
|
|
// Create a test simulation to see where it would have ballistically fallen
|
|
// naturally without settling onto items.
|
|
final Simulation? testFrictionSimulation = super.createBallisticSimulation(metrics, velocity);
|
|
|
|
// Scenario 2:
|
|
// If it was going to end up past the scroll extent, defer back to the
|
|
// parent physics' ballistics again which should put us on the scrollable's
|
|
// boundary.
|
|
if (testFrictionSimulation != null && (testFrictionSimulation.x(double.infinity) == metrics.minScrollExtent || testFrictionSimulation.x(double.infinity) == metrics.maxScrollExtent)) {
|
|
return super.createBallisticSimulation(metrics, velocity);
|
|
}
|
|
|
|
// From the natural final position, find the nearest item it should have
|
|
// settled to.
|
|
final offset = (testFrictionSimulation?.x(double.infinity) ?? metrics.pixels).clamp(metrics.minScrollExtent, metrics.maxScrollExtent);
|
|
final int settlingItemIndex = scrollOffsetToIndex(offset);
|
|
final double settlingPixels = indexToScrollOffset(settlingItemIndex);
|
|
|
|
// Scenario 3:
|
|
// If there's no velocity and we're already at where we intend to land,
|
|
// do nothing.
|
|
if (velocity.abs() < tolerance.velocity && (settlingPixels - metrics.pixels).abs() < tolerance.distance) {
|
|
return null;
|
|
}
|
|
|
|
// Scenario 4:
|
|
// If we're going to end back at the same item because initial velocity
|
|
// is too low to break past it, use a spring simulation to get back.
|
|
if (settlingItemIndex == scrollOffsetToIndex(metrics.pixels)) {
|
|
return SpringSimulation(
|
|
spring,
|
|
metrics.pixels,
|
|
settlingPixels,
|
|
velocity,
|
|
tolerance: tolerance,
|
|
);
|
|
}
|
|
|
|
// Scenario 5:
|
|
// Create a new friction simulation except the drag will be tweaked to land
|
|
// exactly on the item closest to the natural stopping point.
|
|
return FrictionSimulation.through(
|
|
metrics.pixels,
|
|
settlingPixels,
|
|
velocity,
|
|
tolerance.velocity * velocity.sign,
|
|
);
|
|
}
|
|
}
|