| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/physics.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| import 'scroll_activity.dart'; |
| import 'scroll_context.dart'; |
| import 'scroll_notification.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_position.dart'; |
| |
| /// 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]. |
| /// |
| /// See also: |
| /// |
| /// * [ScrollPosition], which defines the underlying model for a position |
| /// within a [Scrollable] but is agnostic as to how that position is |
| /// changed. |
| /// * [ScrollView] and its subclasses such as [ListView], which use |
| /// [ScrollPositionWithSingleContext] to manage their scroll position. |
| /// * [ScrollController], which can manipulate one or more [ScrollPosition]s, |
| /// and which uses [ScrollPositionWithSingleContext] as its default class for |
| /// scroll positions. |
| class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollActivityDelegate { |
| /// Create a [ScrollPosition] object that manages its behavior using |
| /// [ScrollActivity] objects. |
| /// |
| /// The `initialPixels` argument can be null, but in that case it is |
| /// imperative that the value be set, using [correctPixels], as soon as |
| /// [applyNewDimensions] is invoked, before calling the inherited |
| /// implementation of that method. |
| /// |
| /// If [keepScrollOffset] is true (the default), the current scroll offset is |
| /// saved with [PageStorage] and restored it if this scroll position's scrollable |
| /// is recreated. |
| ScrollPositionWithSingleContext({ |
| required super.physics, |
| required super.context, |
| double? initialPixels = 0.0, |
| super.keepScrollOffset, |
| super.oldPosition, |
| super.debugLabel, |
| }) { |
| // If oldPosition is not null, the superclass will first call absorb(), |
| // which may set _pixels and _activity. |
| if (!hasPixels && initialPixels != null) { |
| correctPixels(initialPixels); |
| } |
| if (activity == null) { |
| goIdle(); |
| } |
| assert(activity != null); |
| } |
| |
| /// Velocity from a previous activity temporarily held by [hold] to potentially |
| /// transfer to a next activity. |
| double _heldPreviousVelocity = 0.0; |
| |
| @override |
| AxisDirection get axisDirection => context.axisDirection; |
| |
| @override |
| double setPixels(double newPixels) { |
| assert(activity!.isScrolling); |
| return super.setPixels(newPixels); |
| } |
| |
| @override |
| void absorb(ScrollPosition other) { |
| super.absorb(other); |
| if (other is! ScrollPositionWithSingleContext) { |
| goIdle(); |
| return; |
| } |
| activity!.updateDelegate(this); |
| _userScrollDirection = other._userScrollDirection; |
| assert(_currentDrag == null); |
| if (other._currentDrag != null) { |
| _currentDrag = other._currentDrag; |
| _currentDrag!.updateDelegate(this); |
| other._currentDrag = null; |
| } |
| } |
| |
| @override |
| void applyNewDimensions() { |
| super.applyNewDimensions(); |
| context.setCanDrag(physics.shouldAcceptUserOffset(this)); |
| } |
| |
| @override |
| void beginActivity(ScrollActivity? newActivity) { |
| _heldPreviousVelocity = 0.0; |
| if (newActivity == null) { |
| return; |
| } |
| assert(newActivity.delegate == this); |
| super.beginActivity(newActivity); |
| _currentDrag?.dispose(); |
| _currentDrag = null; |
| if (!activity!.isScrolling) { |
| updateUserScrollDirection(ScrollDirection.idle); |
| } |
| } |
| |
| @override |
| void applyUserOffset(double delta) { |
| updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse); |
| setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); |
| } |
| |
| @override |
| void goIdle() { |
| beginActivity(IdleScrollActivity(this)); |
| } |
| |
| /// Start a physics-driven simulation that settles the [pixels] position, |
| /// starting at a particular velocity. |
| /// |
| /// This method defers to [ScrollPhysics.createBallisticSimulation], which |
| /// typically provides a bounce simulation when the current position is out of |
| /// bounds and a friction simulation when the position is in bounds but has a |
| /// non-zero velocity. |
| /// |
| /// The velocity should be in logical pixels per second. |
| @override |
| void goBallistic(double velocity) { |
| assert(hasPixels); |
| final Simulation? simulation = physics.createBallisticSimulation(this, velocity); |
| if (simulation != null) { |
| beginActivity(BallisticScrollActivity( |
| this, |
| simulation, |
| context.vsync, |
| activity?.shouldIgnorePointer ?? true, |
| )); |
| } else { |
| goIdle(); |
| } |
| } |
| |
| @override |
| ScrollDirection get userScrollDirection => _userScrollDirection; |
| ScrollDirection _userScrollDirection = ScrollDirection.idle; |
| |
| /// Set [userScrollDirection] to the given value. |
| /// |
| /// If this changes the value, then a [UserScrollNotification] is dispatched. |
| @protected |
| @visibleForTesting |
| void updateUserScrollDirection(ScrollDirection value) { |
| assert(value != null); |
| if (userScrollDirection == value) { |
| return; |
| } |
| _userScrollDirection = value; |
| didUpdateScrollDirection(value); |
| } |
| |
| @override |
| Future<void> animateTo( |
| double to, { |
| required Duration duration, |
| required Curve curve, |
| }) { |
| if (nearEqual(to, pixels, physics.tolerance.distance)) { |
| // Skip the animation, go straight to the position as we are already close. |
| jumpTo(to); |
| return Future<void>.value(); |
| } |
| |
| final DrivenScrollActivity activity = DrivenScrollActivity( |
| this, |
| from: pixels, |
| to: to, |
| duration: duration, |
| curve: curve, |
| vsync: context.vsync, |
| ); |
| beginActivity(activity); |
| return activity.done; |
| } |
| |
| @override |
| void jumpTo(double value) { |
| goIdle(); |
| if (pixels != value) { |
| final double oldPixels = pixels; |
| forcePixels(value); |
| didStartScroll(); |
| didUpdateScrollPositionBy(pixels - oldPixels); |
| didEndScroll(); |
| } |
| goBallistic(0.0); |
| } |
| |
| @override |
| void pointerScroll(double delta) { |
| // If an update is made to pointer scrolling here, consider if the same |
| // (or similar) change should be made in |
| // _NestedScrollCoordinator.pointerScroll. |
| assert(delta != 0.0); |
| |
| final double targetPixels = |
| math.min(math.max(pixels + delta, minScrollExtent), maxScrollExtent); |
| if (targetPixels != pixels) { |
| goIdle(); |
| updateUserScrollDirection( |
| -delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse, |
| ); |
| final double oldPixels = pixels; |
| forcePixels(targetPixels); |
| isScrollingNotifier.value = true; |
| didStartScroll(); |
| didUpdateScrollPositionBy(pixels - oldPixels); |
| didEndScroll(); |
| goBallistic(0.0); |
| } |
| } |
| |
| |
| @Deprecated('This will lead to bugs.') // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/44609 |
| @override |
| void jumpToWithoutSettling(double value) { |
| goIdle(); |
| if (pixels != value) { |
| final double oldPixels = pixels; |
| forcePixels(value); |
| didStartScroll(); |
| didUpdateScrollPositionBy(pixels - oldPixels); |
| didEndScroll(); |
| } |
| } |
| |
| @override |
| ScrollHoldController hold(VoidCallback holdCancelCallback) { |
| final double previousVelocity = activity!.velocity; |
| final HoldScrollActivity holdActivity = HoldScrollActivity( |
| delegate: this, |
| onHoldCanceled: holdCancelCallback, |
| ); |
| beginActivity(holdActivity); |
| _heldPreviousVelocity = previousVelocity; |
| return holdActivity; |
| } |
| |
| ScrollDragController? _currentDrag; |
| |
| @override |
| Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { |
| final ScrollDragController drag = ScrollDragController( |
| delegate: this, |
| details: details, |
| onDragCanceled: dragCancelCallback, |
| carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity), |
| motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold, |
| ); |
| beginActivity(DragScrollActivity(this, drag)); |
| assert(_currentDrag == null); |
| _currentDrag = drag; |
| return drag; |
| } |
| |
| @override |
| void dispose() { |
| _currentDrag?.dispose(); |
| _currentDrag = null; |
| super.dispose(); |
| } |
| |
| @override |
| void debugFillDescription(List<String> description) { |
| super.debugFillDescription(description); |
| description.add('${context.runtimeType}'); |
| description.add('$physics'); |
| description.add('$activity'); |
| description.add('$userScrollDirection'); |
| } |
| } |