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) {
  final _MoonCarouselScrollPosition metrics = position as _MoonCarouselScrollPosition;

  // Scenario 1:
  // If the carousel is out of range and not returning to range, we defer to the parent ballistics to bring
  // the carousel back into range at the scrollable boundary.
  if ((velocity <= 0.0 && metrics.pixels <= metrics.minScrollExtent) ||
      (velocity >= 0.0 && metrics.pixels >= metrics.maxScrollExtent)) {
    return super.createBallisticSimulation(metrics, velocity);
  }

  // Perform a test simulation to determine the carousel's ballistic trajectory without
  // interference from carousel items, simulating a natural fall.
  final Simulation? testFrictionSimulation = super.createBallisticSimulation(
    metrics,
    velocity * math.min(metrics.velocityFactor + 0.15, 1.0),
  );

  // Scenario 2:
  // If the simulated trajectory would have taken the carousel beyond the scroll extent,
  // defer back to the parent physics' ballistics to prevent it from overshooting the scrollable boundary.
  if (testFrictionSimulation != null &&
      (testFrictionSimulation.x(double.infinity) == metrics.minScrollExtent ||
          testFrictionSimulation.x(double.infinity) == metrics.maxScrollExtent)) {
    return super.createBallisticSimulation(metrics, velocity);
  }

  // From the simulated final position, identify the nearest item the carousel should have settled
  // onto based on its natural trajectory.
  final int settlingItemIndex = _getItemFromOffset(
    itemExtent: metrics.itemExtent,
    minScrollExtent: metrics.minScrollExtent,
    maxScrollExtent: metrics.maxScrollExtent,
    offset: testFrictionSimulation?.x(double.infinity) ?? metrics.pixels,
  );

  final double settlingPixels = settlingItemIndex * metrics.itemExtent;

  // Scenario 3:
  // If the carousel has zero velocity and is already at the position it should settle onto,
  // there's no need for further action.
  final Tolerance tolerance = toleranceFor(metrics);

  if (velocity.abs() < tolerance.velocity && (settlingPixels - metrics.pixels).abs() < tolerance.distance) {
    return null;
  }

  // Scenario 4:
  // If the simulated trajectory indicates the carousel is likely to return to its initial position due to
  // a lack of sufficient initial velocity, use a spring simulation.
  if (settlingItemIndex == metrics.itemIndex) {
    return SpringSimulation(
      spring,
      metrics.pixels,
      settlingPixels,
      velocity * metrics.velocityFactor,
      tolerance: tolerance,
    );
  }

  // Scenario 5:
  // Create a new friction simulation, adjusting the drag force to ensure the carousel transitions
  // to the nearest item, aligning with its natural stopping point.
  return FrictionSimulation.through(
    metrics.pixels,
    settlingPixels,
    velocity * metrics.velocityFactor,
    tolerance.velocity * metrics.velocityFactor * velocity.sign,
  );
}