This article, the 18th in the series, will take you deep into the new world of Flutter sliding through ScrollPhysics and Simulation, opening another window on Flutter sliding operation.

Article summary address:

A complete series of articles on Flutter

A series of articles on the world outside Flutter

One, foreword

The following picture shows the default slippable Widget effect of Flutter. Different slippable speeds and edge drag effects appear on Android and iOS because different ScrollPhysics and Simulation are used by default on different platforms. Later we will introduce the implementation principles of these two main actors step by step, and finally allow you to slide and drag the Flutter world into the realm of “whatever you want”.

High energy dry goods from the bottom, please bring your own tea to eat.

Second, the ScrollPhysics

First, introduce the ScrollPhysics. According to the official introduction of Flutter, ScrollPhysics is used to determine the physical properties of the scrollable controls.

  • BouncingScrollPhysics: Allows scrolling out of bounds, but then the content bounces back.
  • ClampingScrollPhysics: Prevents rolling beyond the boundary, clamping.
  • AlwaysScrollableScrollPhysics: always respond to user scroll.
  • NeverScrollableScrollPhysics: no response to the user’s scrolling.

During the development process, it is usually set by the following code:

 CustomScrollView(physics: const BouncingScrollPhysics())
 ListView.builder(physics: const AlwaysScrollableScrollPhysics())
 GridView.count(physics: NeverScrollableScrollPhysics())
Copy the code

But normally, we do not set the physics properties voluntarily. So by default, why do all the Scrollable controls in a Flutter, such as ListView, CustomScrollView, etc., What are the platform differences between Android and iOS when you scroll and drag borders, as shown below?

The key here is ScrollConfiguration and ScrollBehavior.

2.1. ScrollConfiguration and ScrollBehavior

We know that all sliding controls slide in response to a touch through a Scrollable.

As shown in the following code, within the _updatePosition method of Scrollable, when widget.physics == null, _physics defaults to the getScrollPhysics(context) method from scrollConfiguration. of(context), Scrollconfiguration. of(context) returns a ScrollBehavior object.

  // Only call this from places that will definitely trigger a rebuild.
  void _updatePosition() {
    _configuration = ScrollConfiguration.of(context);
    _physics = _configuration.getScrollPhysics(context);
    if(widget.physics ! = null) _physics = widget.physics.applyTo(_physics); final ScrollController controller = widget.controller; final ScrollPosition oldPosition = position;if(oldPosition ! = null) { controller? .detach(oldPosition); scheduleMicrotask(oldPosition.dispose); } _position = controller? .createScrollPosition(_physics, this, oldPosition) ?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition); assert(position ! = null); controller? .attach(position); }Copy the code

So by default,ScrollPhysicsIs andScrollConfigurationScrollBehaviorHave a relationship.

So ScrollBehavior works like this, right?

By default, the ScrollBehavior returns different ScrollPhysics on different platforms. By default, the ScrollBehavior returns different ScrollPhysics on different platforms.

  ScrollPhysics getScrollPhysics(BuildContext context) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
        return const BouncingScrollPhysics();
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return const ClampingScrollPhysics();
    }
    return null;
  }
Copy the code

ScrollPhysics determines the physics of a scrollable control. So, how does the blue semicircle of an Android drag-and-drop spill come about? When is the ScrollBehavior of the ScrollConfiguration set?

ScrollConfiguration is an InheritedWidget like Theme, Localizations, etc., so it should be shared from the top down.

Therefore, check the source code of the MaterialApp and get the following code. It can be seen that the ScrollConfiguration is nested by default in the MaterialApp. Override’s buildViewportChrome method implements the overflow and drag semicird effect on Android. One of GlowingOverscrollIndicator is semicircle effect drawing control.

Override Widget build(BuildContext context) {return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      child: result,
    );
}
class _MaterialScrollBehavior extends ScrollBehavior {
  @override
  TargetPlatform getPlatform(BuildContext context) {
    return Theme.of(context).platform;
  }
  @override
  Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
        return child;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return GlowingOverscrollIndicator(
          child: child,
          axisDirection: axisDirection,
          color: Theme.of(context).accentColor,
        );
    }
    returnnull; }}Copy the code

Here’s how ScrollPhysics is configured for sliding controls by default:

  • ScrollConfiguration is an InheritedWidget.
  • 2. The MaterialApp internally uses ScrollConfiguration and shares a subclass of ScrollBehavior _MaterialScrollBehavior.
  • 3. ScrollBehavior by default returns the specific BouncingScrollPhysics and ClampingScrollPhysics effects depending on the platform.
  • 4. BuildViewportChrome’s blue hemisphere drag overflow effect is implemented in _MaterialScrollBehavior for Android platform.

Ps: We can implement a custom drag overflow effect by implementing our own ScrollBehavior.

ScrollPhysics

So how does ScrollPhysics implement scrolling and edge dragging? ScrollPhysics has no code logic by default, and the main way it is defined is as follows:

/// [position] current position, /// Convert the dragged distance of the user into the required pixels. Double applyPhysicsToUserOffset(ScrollMetrics position, Double offset) // return overscroll, if 0 is returned, Double applyBoundaryConditions(ScrollMetrics position, Double value) / / / create a rolling simulator Simulation createBallisticSimulation (ScrollMetrics position, Double get minFlingVelocity double get minFlingVelocity Returns the repeated scrolling speed double carriedMomentum (double existingVelocity) / / / minimum distance between the start of the drag double get dragStartDistanceMotionThreshold /// Tolerances for rolling simulation /// Specify the distance, duration and speed differences to be treated as equal differences of the structure. Tolerance get toleranceCopy the code

The above code notes the general role of each method of ScrollPhysics, and in the previous “13, comprehensive in-depth Touch and sliding principle”, we have in-depth analysis of the principle of touch and sliding, the general process from touch down, and finally trigger layout to achieve the phenomenon of sliding:

The working principle of ScrollPhysics is interspersed in the process, as shown in the figure below. The main logic lies in the three methods marked in red:

  • applyPhysicsToUserOffset: Drags the user to a distance using physicsoffsetintosetPixelsThe increment of (scroll).
  • ApplyBoundaryConditions: Calculates the boundary conditions of the current scroll using physics.
  • CreateBallisticSimulation: create automatic sliding of the simulator.

These three methods are triggered by _handleDragUpdate, _handleDragCancel, and _handleDragEnd, which is when the drag process and drag end:

  • applyPhysicsToUserOffsetapplyBoundaryConditionsIs in the_handleDragUpdateWhen triggered.
  • createBallisticSimulationIs in the_handleDragCancel_handleDragEndWhen triggered.

So the biggest difference between default BouncingScrollPhysics and ClampingScrollPhysics is in these three methods.

3.1, applyPhysicsToUserOffset

ClampingScrollPhysics does not override applyPhysicsToUserOffset by default. When parent == null, the user’s sliding offset will return whatever it is:

  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    if (parent == null)
      return offset;
    return parent.applyPhysicsToUserOffset(position, offset);
  }
Copy the code

Override the applyPhysicsToUserOffset method in BouncingScrollPhysics. The default offset is returned before the user reaches the boundary. When the user reaches the boundary, the algorithm is used to simulate overflow damping.

// double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1-overscrollFraction, 2); // Double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1-overscrollFraction, 2); @override double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { assert(offset ! = 0.0). assert(position.minScrollExtent <= position.maxScrollExtent);if(! position.outOfRange)returnoffset; Final double overscrollPastStart = math.max(position.minscrollextent-position.Pixels, 0.0); Final double overscrollPastEnd = math. Max (position. The pixels - position. MaxScrollExtent, 0.0); final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd); Final bool much = (overscrollPastStart > 0.0 && offset < 0.0) | | (overscrollPastEnd > 0.0 && offset > 0.0); final double friction = easing // Apply less resistance when easing the overscroll vs tensioning. ? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension) : frictionFactor(overscrollPast / position.viewportDimension); final double direction = offset.sign;return direction * _applyFriction(overscrollPast, offset.abs(), friction);
  }
Copy the code

3.2, applyBoundaryConditions

In applyBoundaryConditions method of ClampingScrollPhysics, when the boundary condition value is calculated, the sliding value will be subtracted from the boundary value to get the opposite data, making the sliding boundary relatively static and thus achieving the function of “clamping”, namely the dynamic boundary. So by default, if you scroll to the edge, Android will stop responding.

  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
      return value - position.pixels;
    if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
      return value - position.pixels;
    if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
      return value - position.minScrollExtent;
    if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
      return value - position.maxScrollExtent;
    return 0.0;
  }
Copy the code

The blue semicircle is implemented by the default buildViewportChrome method in ScrollBehavior.

In BouncingScrollPhysics, applyBoundaryConditions return 0 directly, so that applyBoundaryConditions reach 0 and drag beyond 0.

  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
Copy the code

3.3, createBallisticSimulation

Because createBallisticSimulation is triggered when _handleDragCancel and _handleDragEnd, is the time to stop touching, When createBallisticSimulation returns null, will enter IdleScrollActivity Scrllable, namely stop rolling state.

As shown in the figure below, list scrolling without Simulation at all will not continue.

ClampingScrollPhysics createBallisticSimulation method, ClampingScrollSimulation(fixed) and ScrollSpringSimulation(elastic) are used, as shown in the code below, Theoretically, only Position. outOfRange can trigger the elastic rebound effect, but ScrollPhysics uses a similar parent agent model, whose parent may trigger Position. outOfRange. That’s why there’s speculation that ScrollSpringSimulation complements that.

As can be seen from the following code, only when velocity is greater than the default acceleration and within the sliding range, will ClampingScrollPhysics simulation slide be returned, otherwise null will be returned to enter the previously mentioned Idle stop sliding, which is also why ordinary slow drag, The reason why automatic scrolling is not triggered.

@override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    final Tolerance tolerance = this.tolerance;
    if (position.outOfRange) {
      double end;
      if (position.pixels > position.maxScrollExtent)
        end = position.maxScrollExtent;
      if(position.pixels < position.minScrollExtent) end = position.minScrollExtent; assert(end ! = null);returnScrollSpringSimulation(Spring, Position.pixels, end, Math.min (0.0, velocity), tolerance: tolerance,); }if (velocity.abs() < tolerance.velocity) return null;
    if(Velocity > 0.0&& position.pixels >= Position.maxscrollextent)return null;
    if(Velocity < 0.0&& position.pixels <= position.minscrollextent)return null;
    return ClampingScrollSimulation(
      position: position.pixels,
      velocity: velocity,
      tolerance: tolerance,
    );
  }
Copy the code

BouncingScrollPhysics createBallisticSimulation is simple, and only at the end of touch, the initial velocity is greater than the default acceleration or area beyond, BouncingScrollSimulation will be returned for simulation sliding calculation, otherwise, the Idle mentioned above will be entered to stop sliding.

  @override
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    final Tolerance tolerance = this.tolerance;
    if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
      returnBouncingScrollSimulation(Spring: spring, position: position.pixels, Velocity: Velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end. leadingExtent: position.minScrollExtent, trailingExtent: position.maxScrollExtent, tolerance: tolerance, ); }return null;
  }
Copy the code

It can be seen that whether the list will continue to simulate sliding when touching stops is related to velocity and tolerance. Velocity, that is, it will continue sliding only when the speed is greater than the specified acceleration. In addition, ClampingScrollSimulation and BouncingScrollSimulation show different effects in the sliding area.

As shown in the figure below, ScrollSpringSimulation on the first page has a deceleration effect before stopping scrolling. The second page ClampingScrollSimulation slides directly and quickly to the boundary.

In fact, by selection or adjustmentSimulation, you can customize the sliding speed, damping and rebound effect of the list flexibly.

Fourth, the Simulation

Simulation can be used to perform sliding, damping, and springback effects on lists. How does Simulation work?

As shown in the above, in the Simulation of creation is in ScrollPositionWithSingleContext goBallistic method is invoked, then through BallisticScrollActivity to trigger execution.

@override void goBallistic(double velocity) { assert(pixels ! = null); final Simulation simulation = physics.createBallisticSimulation(this, velocity);if(simulation ! = null) { beginActivity(BallisticScrollActivity(this, simulation, context.vsync)); }else{ goIdle(); }}Copy the code

In the BallisticScrollActivity state, Simulation is used to drive the value of the AnimationController, Then get the value obtained after Simulation calculation in the callback of the animation to implement setPixels(value) rolling.

When the vsync signal of drawFrame arrives, the _tick method inside the AnimationController will be executed. X (elapsedInSeconds). Clamp (lowerBound, upperBound); Change and notifyListeners (); Notification update.

The internal calculation logic of ClampingScrollSimulation is not expanded here. Generally speaking, it can be seen that the friction factor of ClampingScrollSimulation is fixed, and the friction factor and calculation of BouncingScrollSimulation are related to the position of transmission.

The important thing here is, whyBouncingScrollPhysicsDoes it bounce back?

And BouncingScrollSimulation, because when BouncingScrollSimulation is built, Will pass leadingExtent: position. MinScrollExtent and trailingExtent: Position. maxScrollExtent, in the case of underscroll and overscroll, ScrollSpringSimulation will be used to animate elastic rollbacks to leadingExtent and trailingExtent to achieve the following effect:

The last

The ScrollPhysics and Simulation of Flutter are basically analyzed here. Strictly speaking, Simulation should be part of animation, but ScrollPhysics is also included here.

To sum it upScrollPhysicsControl the user touch transformation and boundary conditions, and when the user stops touching, useSimulationRealize the animation effect of automatic scrolling and overflow rebound.

From this, canto XVIII is finally over! (/ / / del / / /)

Resources to recommend

  • Making: github.com/CarGuo
  • Open Source Flutter complete project:Github.com/CarGuo/GSYG…
  • Open Source Flutter Multi-case learning project:Github.com/CarGuo/GSYF…
  • Open Source Fluttre Combat Ebook Project:Github.com/CarGuo/GSYF…
  • Open Source React Native project: github.com/CarGuo/GSYG…