Skip to content

Instantly share code, notes, and snippets.

@oravecz
Created August 4, 2020 00:59
Show Gist options
  • Save oravecz/664814708cf8c69c0aae19883df2d088 to your computer and use it in GitHub Desktop.
Save oravecz/664814708cf8c69c0aae19883df2d088 to your computer and use it in GitHub Desktop.
Skyost MagnetScrollPhysics class to add snap to ListView
// https://imgur.com/a/S7ekfb6 demonstrates a problem with the simulator below
// https://github.com/Skyost
/// A scroll physics that always lands on specific points.
class MagnetScrollPhysics extends ScrollPhysics {
/// The fixed item size.
final double itemSize;
/// Creates a new magnet scroll physics instance.
MagnetScrollPhysics({
ScrollPhysics parent,
@required this.itemSize,
}) : super(parent: parent);
@override
MagnetScrollPhysics applyTo(ScrollPhysics ancestor) {
return MagnetScrollPhysics(
parent: buildParent(ancestor),
itemSize: itemSize,
);
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// 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 && position.pixels <= position.minScrollExtent) || (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
// Create a test simulation to see where it would have ballistically fallen
// naturally without settling onto items.
final Simulation testFrictionSimulation = super.createBallisticSimulation(position, 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) == position.minScrollExtent || testFrictionSimulation.x(double.infinity) == position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
// From the natural final position, find the nearest item it should have
// settled to.
final int settlingItemIndex = _getItemFromOffset(
offset: testFrictionSimulation?.x(double.infinity) ?? position.pixels,
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
);
final double settlingPixels = settlingItemIndex * itemSize;
// 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 - position.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 ==
_getItemFromOffset(
offset: position.pixels,
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
)) {
return SpringSimulation(
spring,
position.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(
position.pixels,
settlingPixels,
velocity,
tolerance.velocity * velocity.sign,
);
}
/// Returns the item index from the specified offset.
int _getItemFromOffset({
double offset,
double minScrollExtent,
double maxScrollExtent,
}) =>
(_clipOffsetToScrollableRange(offset, minScrollExtent, maxScrollExtent) / itemSize).round();
/// Clips the specified offset to the scrollable range.
double _clipOffsetToScrollableRange(
double offset,
double minScrollExtent,
double maxScrollExtent,
) =>
Math.min(Math.max(offset, minScrollExtent), maxScrollExtent);
}
@oravecz
Copy link
Author

oravecz commented Aug 4, 2020

@Skyost, made this into a gist in case you wanted to discuss

I posted a video demonstrating a small problem with the (very cool) physics simulator you modified.

@Skyost
Copy link

Skyost commented Aug 4, 2020

@oravecz Thanks, but I don't get what's the problem. The scroll physics on your gif seem to work as expected, isn't it ?

@oravecz
Copy link
Author

oravecz commented Aug 4, 2020

@Skyost, not quite, notice that last drag I make. The tile aligns to the left edge as if it isn't the last tile in the list. Then it snaps back to the right-hand side (maxExtent) when it finishes.

It may be hard to tell where I let off the mouse. I press it down and nudge the last tile just a few pixels (until it is over the threshold) and let go. Instead of sliding directly to the right, it slides left first, then swings back to the right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment