scroll_position_with_single_context.dart 8.15 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// 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
32
///    within a [Scrollable] but is agnostic as to how that position is
33 34 35 36 37 38 39 40 41 42 43 44 45 46
///    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.
47 48 49 50
  ///
  /// 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.
51 52
  ScrollPositionWithSingleContext({
    @required ScrollPhysics physics,
53
    @required ScrollContext context,
54 55
    double initialPixels = 0.0,
    bool keepScrollOffset = true,
56
    ScrollPosition oldPosition,
57
    String debugLabel,
58 59 60 61 62 63 64
  }) : super(
         physics: physics,
         context: context,
         keepScrollOffset: keepScrollOffset,
         oldPosition: oldPosition,
         debugLabel: debugLabel,
       ) {
65 66 67 68 69 70 71 72 73
    // 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);
  }

74 75 76 77
  /// Velocity from a previous activity temporarily held by [hold] to potentially
  /// transfer to a next activity.
  double _heldPreviousVelocity = 0.0;

78 79 80 81 82 83 84 85 86 87
  @override
  AxisDirection get axisDirection => context.axisDirection;

  @override
  double setPixels(double newPixels) {
    assert(activity.isScrolling);
    return super.setPixels(newPixels);
  }

  @override
88 89 90
  void absorb(ScrollPosition other) {
    super.absorb(other);
    if (other is! ScrollPositionWithSingleContext) {
91 92 93
      goIdle();
      return;
    }
94 95 96
    activity.updateDelegate(this);
    final ScrollPositionWithSingleContext typedOther = other;
    _userScrollDirection = typedOther._userScrollDirection;
97
    assert(_currentDrag == null);
98 99 100 101
    if (typedOther._currentDrag != null) {
      _currentDrag = typedOther._currentDrag;
      _currentDrag.updateDelegate(this);
      typedOther._currentDrag = null;
102
    }
103 104 105 106
  }

  @override
  void applyNewDimensions() {
107
    super.applyNewDimensions();
108 109 110
    context.setCanDrag(physics.shouldAcceptUserOffset(this));
  }

111
  @override
112
  void beginActivity(ScrollActivity newActivity) {
113
    _heldPreviousVelocity = 0.0;
114 115 116
    if (newActivity == null)
      return;
    assert(newActivity.delegate == this);
117
    super.beginActivity(newActivity);
118 119
    _currentDrag?.dispose();
    _currentDrag = null;
120 121 122 123 124
    if (!activity.isScrolling)
      updateUserScrollDirection(ScrollDirection.idle);
  }

  @override
125
  void applyUserOffset(double delta) {
126
    updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
127
    setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
  }

  @override
  void goIdle() {
    beginActivity(new 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(new 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.
162
  @protected
163 164 165 166 167
  void updateUserScrollDirection(ScrollDirection value) {
    assert(value != null);
    if (userScrollDirection == value)
      return;
    _userScrollDirection = value;
168
    didUpdateScrollDirection(value);
169 170 171 172 173 174 175
  }

  @override
  Future<Null> animateTo(double to, {
    @required Duration duration,
    @required Curve curve,
  }) {
176 177 178 179 180 181
    if (nearEqual(to, pixels, physics.tolerance.distance)) {
      // Skip the animation, go straight to the position as we are already close.
      jumpTo(to);
      return new Future<Null>.value();
    }

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
    final DrivenScrollActivity activity = new 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();
201
      didStartScroll();
202
      didUpdateScrollPositionBy(pixels - oldPixels);
203
      didEndScroll();
204 205 206 207 208 209 210 211 212 213 214 215
    }
    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();
216
      didStartScroll();
217
      didUpdateScrollPositionBy(pixels - oldPixels);
218
      didEndScroll();
219 220 221 222
    }
  }

  @override
223
  ScrollHoldController hold(VoidCallback holdCancelCallback) {
224 225
    final double previousVelocity = activity.velocity;
    final HoldScrollActivity holdActivity = new HoldScrollActivity(
226 227 228
      delegate: this,
      onHoldCanceled: holdCancelCallback,
    );
229 230 231
    beginActivity(holdActivity);
    _heldPreviousVelocity = previousVelocity;
    return holdActivity;
232 233
  }

234 235
  ScrollDragController _currentDrag;

236
  @override
237
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
238 239 240
    final ScrollDragController drag = new ScrollDragController(
      delegate: this,
      details: details,
241
      onDragCanceled: dragCancelCallback,
242
      carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
243
      motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
244
    );
245 246 247 248
    beginActivity(new DragScrollActivity(this, drag));
    assert(_currentDrag == null);
    _currentDrag = drag;
    return drag;
249 250 251 252
  }

  @override
  void dispose() {
253 254
    _currentDrag?.dispose();
    _currentDrag = null;
255 256 257 258 259 260 261 262 263 264 265 266
    super.dispose();
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('${context.runtimeType}');
    description.add('$physics');
    description.add('$activity');
    description.add('$userScrollDirection');
  }
}