aves/lib/widgets/common/behaviour/known_extent_scroll_physics.dart
2022-03-01 10:24:16 +09:00

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,
);
}
}