// Copyright 2015 The Chromium 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:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'basic.dart'; import 'framework.dart'; import 'gesture_detector.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 ScrollPhysics physics, @required ScrollContext context, double initialPixels = 0.0, bool keepScrollOffset = true, ScrollPosition oldPosition, String debugLabel, }) : super( physics: physics, context: context, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ) { // If oldPosition is not null, the superclass will first call absorb(), // which may set _pixels and _activity. if (pixels == null && 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); final ScrollPositionWithSingleContext typedOther = other; _userScrollDirection = typedOther._userScrollDirection; assert(_currentDrag == null); if (typedOther._currentDrag != null) { _currentDrag = typedOther._currentDrag; _currentDrag.updateDelegate(this); typedOther._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(pixels != null); final Simulation simulation = physics.createBallisticSimulation(this, velocity); if (simulation != null) { beginActivity(BallisticScrollActivity(this, simulation, context.vsync)); } 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); notifyListeners(); didStartScroll(); didUpdateScrollPositionBy(pixels - oldPixels); didEndScroll(); } goBallistic(0.0); } @Deprecated('This will lead to bugs.') @override void jumpToWithoutSettling(double value) { goIdle(); if (pixels != value) { final double oldPixels = pixels; forcePixels(value); notifyListeners(); 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'); } }