fzyzcjy / flutter_smooth

Achieve ~60 FPS, no matter how heavy the tree is to build/layout
https://fzyzcjy.github.io/flutter_smooth/
MIT License
1.52k stars 65 forks source link

Review: How is scrolling and inertia implemented? #69

Closed fzyzcjy closed 1 year ago

fzyzcjy commented 1 year ago

UML

https://www.processon.com/diagraming/63363e8ae401fd4f1b759f2e

(outdated) screenshot

image

fzyzcjy commented 1 year ago

ListView

class ListView extends BoxScrollView {
  @override
  Widget buildChildLayout(BuildContext context) {
    return SliverList(delegate: childrenDelegate);

BoxScrollView

/// A [ScrollView] that uses a single child layout model.
abstract class BoxScrollView extends ScrollView {
  @override
  List<Widget> buildSlivers(BuildContext context) {
    Widget sliver = buildChildLayout(context);
    return <Widget>[ sliver ];
  }

  /// Subclasses should override this method to build the layout model.
  @protected
  Widget buildChildLayout(BuildContext context);

ScrollView

/// A widget that scrolls.
///
/// Scrollable widgets consist of three pieces:
///
///  1. A [Scrollable] widget, which listens for various user gestures and
///     implements the interaction design for scrolling.
///  2. A viewport widget, such as [Viewport] or [ShrinkWrappingViewport], which
///     implements the visual design for scrolling by displaying only a portion
///     of the widgets inside the scroll view.
///  3. One or more slivers, which are widgets that can be composed to created
///     various scrolling effects, such as lists, grids, and expanding headers.
///
/// [ScrollView] helps orchestrate these pieces by creating the [Scrollable] and
/// the viewport and deferring to its subclass to create the slivers.
abstract class ScrollView extends StatelessWidget {
  /// Build the list of widgets to place inside the viewport.
  ///
  /// Subclasses should override this method to build the slivers for the inside
  /// of the viewport.
  @protected
  List<Widget> buildSlivers(BuildContext context);

  Widget buildViewport(
    ViewportOffset offset,
    List<Widget> slivers,
  ) {
    return Viewport();
  }

  Widget build(BuildContext context) {
    final List<Widget> slivers = buildSlivers(context);
    final Scrollable scrollable = Scrollable(
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return buildViewport(context, offset, axisDirection, slivers);
      },
    );
    return scrollable;
  }

Scrollable

/// A widget that scrolls.
///
/// [Scrollable] implements the interaction model for a scrollable widget,
/// including gesture recognition, but does not have an opinion about how the
/// viewport, which actually displays the children, is constructed.
///
/// To further customize scrolling behavior with a [Scrollable]:
///
/// 1. You can provide a [viewportBuilder] to customize the child model. For
///    example, [SingleChildScrollView] uses a viewport that displays a single
///    box child whereas [CustomScrollView] uses a [Viewport] or a
///    [ShrinkWrappingViewport], both of which display a list of slivers.
///
/// 2. You can provide a custom [ScrollController] that creates a custom
///    [ScrollPosition] subclass. For example, [PageView] uses a
///    [PageController], which creates a page-oriented scroll position subclass
///    that keeps the same page visible when the [Scrollable] resizes.
class Scrollable extends StatefulWidget {
  final ScrollController? controller;
  final ScrollPhysics? physics;
  final ViewportBuilder viewportBuilder;
}
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin implements ScrollContext {
  /// The manager for this [Scrollable] widget's viewport position.
  ///
  /// To control what kind of [ScrollPosition] is created for a [Scrollable],
  /// provide it with custom [ScrollController] that creates the appropriate
  /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
  ScrollPosition get position => _position!;
  ScrollPosition? _position;

  void setCanDrag(bool value) {
          _gestureRecognizers = ...VerticalDragGestureRecognizer...
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel;
  }

  Drag? _drag;
  void _handleDragStart(DragStartDetails details) => _drag = position.drag(details, _disposeDrag);
  void _handleDragUpdate(DragUpdateDetails details) => _drag?.update(details);
  void _handleDragEnd(DragEndDetails details) => _drag?.end(details);

  Widget build(BuildContext context) {
    return _ScrollableScope(
      scrollable: this,
      position: position,
      child: RawGestureDetector(
        key: _gestureDetectorKey,
        gestures: _gestureRecognizers,
        child: IgnorePointer(
          key: _ignorePointerKey,
          ignoring: _shouldIgnorePointer,
          child: widget.viewportBuilder(context, position),
        ),
      ),
    );

Drag

/// Interface for objects that receive updates about drags.
/// ... Similarly, the scrolling infrastructure in the widgets
/// library uses it to notify the [DragScrollActivity] when the user drags the
/// scrollable.
abstract class Drag {
  void update(DragUpdateDetails details) { }
  void end(DragEndDetails details) { }
  void cancel() { }
}

ScrollPosition

/// Determines which portion of the content is visible in a scroll view.
///
/// The [pixels] value determines the scroll offset that the scroll view uses to
/// select which part of its content to display. As the user scrolls the
/// viewport, this value changes, which changes the content that is displayed.
///
/// The [ScrollPosition] applies [physics] to scrolling, and stores the
/// [minScrollExtent] and [maxScrollExtent].
///
/// Scrolling is controlled by the current [activity], which is set by
/// [beginActivity]. [ScrollPosition] itself does not start any activities.
/// Instead, concrete subclasses, such as [ScrollPositionWithSingleContext],
/// typically start activities in response to user input or instructions from a
/// [ScrollController].
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
  @override
  double get pixels => _pixels!;
  double? _pixels;

  /// Start a drag activity corresponding to the given [DragStartDetails].
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);

  /// Update the scroll position ([pixels]) to a given pixel value.
  ///
  /// This should only be called by the current [ScrollActivity], either during
  /// the transient callback phase or in response to user input.
  double setPixels(double newPixels) {
      final double overscroll = applyBoundaryConditions(newPixels); // ... about overscroll ...
      _pixels = newPixels - overscroll;
  }

ScrollPositionWithSingleContext

most commonly used ScrollPosition

/// A scroll position that manages scroll activities for a single
/// [ScrollContext].
///
/// This class is a concrete subclass of [ScrollPosition] logic that handles a
/// single [ScrollContext], such as a [Scrollable]. An instance of this class
/// manages [ScrollActivity] instances, which change what content is visible in
/// the [Scrollable]'s [Viewport].
class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollActivityDelegate {
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
    final ScrollDragController drag = ScrollDragController(...);
    beginActivity(DragScrollActivity(this, drag));
    _currentDrag = drag;
    return drag;
  }

  void applyUserOffset(double delta) {
    updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
    setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
  }
}

ScrollActivity, DragScrollActivity

/// Base class for scrolling activities like dragging and flinging.
abstract class ScrollActivity {
  ScrollActivityDelegate _delegate;
}

/// The activity a scroll view performs when the user drags their finger
/// across the screen.
class DragScrollActivity extends ScrollActivity {
}

ScrollActivityDelegate

/// A backend for a [ScrollActivity].
///
/// Used by subclasses of [ScrollActivity] to manipulate the scroll view that
/// they are acting upon.
abstract class ScrollActivityDelegate {
  /// Update the scroll position to the given pixel value.
  ///
  /// Returns the overscroll, if any. See [ScrollPosition.setPixels] for more
  /// information.
  double setPixels(double pixels);

  /// Updates the scroll position by the given amount.
  ///
  /// Appropriate for when the user is directly manipulating the scroll
  /// position, for example by dragging the scroll view. Typically applies
  /// [ScrollPhysics.applyPhysicsToUserOffset] and other transformations that
  /// are appropriate for user-driving scrolling.
  void applyUserOffset(double delta);

  /// Terminate the current activity and start an idle activity.
  void goIdle();

  /// Terminate the current activity and start a ballistic activity with the
  /// given velocity.
  void goBallistic(double velocity);
}

ScrollDragController

/// Scrolls a scroll view as the user drags their finger across the screen.
///
///  * [DragScrollActivity], which is the activity the scroll view performs
///    while a drag is underway.
class ScrollDragController implements Drag {
  ScrollActivityDelegate _delegate;

  void update(DragUpdateDetails details) {
    double offset = details.primaryDelta!;
    ...
    delegate.applyUserOffset(offset);
  }
}

ScrollController

/// Controls a scrollable widget.
///
/// A single scroll controller can
/// be used to control multiple scrollable widgets, but some operations, such
/// as reading the scroll [offset], require the controller to be used with a
/// single scrollable widget.
///
/// A scroll controller creates a [ScrollPosition] to manage the state specific
/// to an individual [Scrollable] widget.
class ScrollController extends ChangeNotifier {
  /// The currently attached positions.
  Iterable<ScrollPosition> get positions => _positions;
  final List<ScrollPosition> _positions = <ScrollPosition>[];
  void attach(ScrollPosition position);
  void detach(ScrollPosition position);

  ScrollPosition createScrollPosition() {
    return ScrollPositionWithSingleContext(...);
  }

Simulation, ClampingScrollSimulation

/// A simulation models an object, in a one-dimensional space, on which particular
/// forces are being applied, and exposes:
///
///  * The object's position, [x]
///  * The object's velocity, [dx]
///  * Whether the simulation is "done", [isDone]
///
/// The [x], [dx], and [isDone] functions take a time argument which specifies
/// the time for which they are to be evaluated.
abstract class Simulation {
  double x(double time);
  double dx(double time);
  bool isDone(double time);
}
class ClampingScrollSimulation extends Simulation {
  ClampingScrollSimulation(this.position, this.velocity) {
    _duration = _flingDuration(velocity);
    _distance = (velocity * _duration / _initialVelocityPenetration).abs();
  }

  /// The position of the particle at the beginning of the simulation.
  final double position;

  /// The velocity at which the particle is traveling at the beginning of the
  /// simulation.
  final double velocity;

  final double friction = 0.015;

  @override
  double x(double time) {
    final double t = clampDouble(time / _duration, 0.0, 1.0);
    return position + _distance * _flingDistancePenetration(t) * velocity.sign;
  }

Viewport, RenderViewport

/// A widget that is bigger on the inside.
///
/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a
/// subset of its children according to its own dimensions and the given
/// [offset]. As the offset varies, different children are visible through
/// the viewport.
///
/// [Viewport] hosts a bidirectional list of slivers, anchored on a [center]
/// sliver, which is placed at the zero scroll offset. The center widget is
/// displayed in the viewport according to the [anchor] property.
class Viewport extends MultiChildRenderObjectWidget {
  /// The viewport listens to the [offset], which means you do not need to
  /// rebuild this widget when the [offset] changes.
  Viewport({...});
}

class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> {
  void performLayout() {
    offset.applyViewportDimension(size.height);
    mainAxisExtent = size.height;
    crossAxisExtent = size.width;
    double correction;
    int count = 0;
    do {
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           )) {
          break;
        }
      }
      count += 1;
    } while (count < _maxLayoutCycles);
  }

  double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    return layoutChildSequence(...);
  }

RenderViewportBase

/// A base class for render objects that are bigger on the inside.
///
/// This render object provides the shared code for render objects that host
/// [RenderSliver] render objects inside a [RenderBox]. The viewport establishes
/// an [axisDirection], which orients the sliver's coordinate system, which is
/// based on scroll offsets rather than Cartesian coordinates.
///
/// The viewport also listens to an [offset], which determines the
/// [SliverConstraints.scrollOffset] input to the sliver layout protocol.
///
/// Subclasses typically override [performLayout] and call
/// [layoutChildSequence], perhaps multiple times.
abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
    extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
    implements RenderAbstractViewport {
  /// Determines the size and position of some of the children of the viewport.
  ///
  /// This function is the workhorse of `performLayout` implementations in
  /// subclasses.
  ///
  /// Layout starts with `child`, proceeds according to the `advance` callback,
  /// and stops once `advance` returns null.
  double layoutChildSequence({...}){ ... }

  void paint(PaintingContext context, Offset offset) {
      _paintContents(context, offset);
  }

  void _paintContents(PaintingContext context, Offset offset) {
    for (final RenderSliver child in childrenInPaintOrder) {
      if (child.geometry!.visible) {
        context.paintChild(child, offset + paintOffsetOf(child));
      }
    }
  }

ViewportOffset

/// Which part of the content inside the viewport should be visible.
///
/// The [pixels] value determines the scroll offset that the viewport uses to
/// select which part of its content to display. As the user scrolls the
/// viewport, this value changes, which changes the content that is displayed.
abstract class ViewportOffset extends ChangeNotifier {
  /// The number of pixels to offset the children
  double get pixels;

  /// Animates [pixels] from its current value to the given value.
  Future<void> animateTo();
  Future<void> moveTo();
fzyzcjy commented 1 year ago

https://juejin.cn/post/6895273504225263629

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33d83a7a2d1b465f88929c939a80a031~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp

image

https://guoshuyu.cn/home/wx/Flutter-18.html

image

https://juejin.cn/post/6931920850925191176

image

image

etc

fzyzcjy commented 1 year ago

Question: How is inertia implemented when user is dragging the screen?

ScrollableState gesture detector, call handleDragStart, handleDragUpdate

handleDragStart

handleDragUpdate

Conclusion

Seems no inertia at all, when user is dragging the screen. Only if the user leave the screen (pointer up/cancel), will start doing things like ClampingScrollSimulation.