createBallisticSimulation method

  1. @override
Simulation? createBallisticSimulation(
  1. ScrollMetrics position,
  2. double velocity
)
override

Returns a simulation for ballistic scrolling starting from the given position with the given velocity.

This is used by ScrollPositionWithSingleContext in the ScrollPositionWithSingleContext.goBallistic method. If the result is non-null, ScrollPositionWithSingleContext will begin a BallisticScrollActivity with the returned value. Otherwise, it will begin an idle activity instead.

The given position is only valid during this method call. Do not keep a reference to it to use later, as the values may update, may not update, or may update to reflect an entirely unrelated scrollable.

This method can potentially be called in every frame, even in the middle of what the user perceives as a single ballistic scroll. For example, in a ListView when previously off-screen items come into view and are laid out, this method may be called with a new ScrollMetrics.maxScrollExtent. The method implementation should ensure that when the same ballistic scroll motion is still intended, these calls have no side effects on the physics beyond continuing that motion.

Generally this is ensured by having the Simulation conform to a physical metaphor of a particle in ballistic flight, where the forces on the particle depend only on its position, velocity, and environment, and not on the current time or any internal state. This means that the time-derivative of Simulation.dx should be possible to write mathematically as a function purely of the values of Simulation.x, Simulation.dx, and the parameters used to construct the Simulation, independent of the time.

Implementation

@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
  assert(
    position is _FixedExtentScrollPosition,
    'FixedExtentScrollPhysics can only be used with Scrollables that uses '
    'the FixedExtentScrollController',
  );

  final _FixedExtentScrollPosition metrics = position as _FixedExtentScrollPosition;

  // 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 int settlingItemIndex = _getItemFromOffset(
    offset: testFrictionSimulation?.x(double.infinity) ?? metrics.pixels,
    itemExtent: metrics.itemExtent,
    minScrollExtent: metrics.minScrollExtent,
    maxScrollExtent: metrics.maxScrollExtent,
  );

  final double settlingPixels = settlingItemIndex * metrics.itemExtent;

  // Scenario 3:
  // If there's no velocity and we're already at where we intend to land,
  // do nothing.
  if (velocity.abs() < toleranceFor(position).velocity
      && (settlingPixels - metrics.pixels).abs() < toleranceFor(position).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 == metrics.itemIndex) {
    return SpringSimulation(
      spring,
      metrics.pixels,
      settlingPixels,
      velocity,
      tolerance: toleranceFor(position),
    );
  }

  // 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,
    toleranceFor(position).velocity * velocity.sign,
  );
}