scroll_position_with_single_context.dart 8.28 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 32
// 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/foundation.dart';
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
33
///    within a [Scrollable] but is agnostic as to how that position is
34 35 36 37 38 39 40 41 42 43 44 45 46 47
///    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.
48 49 50 51
  ///
  /// 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.
52 53
  ScrollPositionWithSingleContext({
    @required ScrollPhysics physics,
54
    @required ScrollContext context,
55
    double initialPixels: 0.0,
56
    bool keepScrollOffset: true,
57
    ScrollPosition oldPosition,
58
    String debugLabel,
59 60 61 62 63 64 65
  }) : super(
         physics: physics,
         context: context,
         keepScrollOffset: keepScrollOffset,
         oldPosition: oldPosition,
         debugLabel: debugLabel,
       ) {
66 67 68 69 70 71 72 73 74
    // 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);
  }

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

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  @override
  AxisDirection get axisDirection => context.axisDirection;

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

  @override
  void correctBy(double correction) {
    correctPixels(pixels + correction);
  }

  @override
94 95 96
  void absorb(ScrollPosition other) {
    super.absorb(other);
    if (other is! ScrollPositionWithSingleContext) {
97 98 99
      goIdle();
      return;
    }
100 101 102
    activity.updateDelegate(this);
    final ScrollPositionWithSingleContext typedOther = other;
    _userScrollDirection = typedOther._userScrollDirection;
103
    assert(_currentDrag == null);
104 105 106 107
    if (typedOther._currentDrag != null) {
      _currentDrag = typedOther._currentDrag;
      _currentDrag.updateDelegate(this);
      typedOther._currentDrag = null;
108
    }
109 110 111 112
  }

  @override
  void applyNewDimensions() {
113
    super.applyNewDimensions();
114 115 116
    context.setCanDrag(physics.shouldAcceptUserOffset(this));
  }

117
  @override
118
  void beginActivity(ScrollActivity newActivity) {
119
    _heldPreviousVelocity = 0.0;
120 121 122
    if (newActivity == null)
      return;
    assert(newActivity.delegate == this);
123
    super.beginActivity(newActivity);
124 125
    _currentDrag?.dispose();
    _currentDrag = null;
126 127 128 129 130
    if (!activity.isScrolling)
      updateUserScrollDirection(ScrollDirection.idle);
  }

  @override
131
  void applyUserOffset(double delta) {
132
    updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
133
    setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
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 162 163 164 165 166 167
  }

  @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.
168
  @protected
169 170 171 172 173
  void updateUserScrollDirection(ScrollDirection value) {
    assert(value != null);
    if (userScrollDirection == value)
      return;
    _userScrollDirection = value;
174
    didUpdateScrollDirection(value);
175 176 177 178 179 180 181
  }

  @override
  Future<Null> animateTo(double to, {
    @required Duration duration,
    @required Curve curve,
  }) {
182 183 184 185 186 187
    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();
    }

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    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();
207
      didStartScroll();
208
      didUpdateScrollPositionBy(pixels - oldPixels);
209
      didEndScroll();
210 211 212 213 214 215 216 217 218 219 220 221
    }
    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();
222
      didStartScroll();
223
      didUpdateScrollPositionBy(pixels - oldPixels);
224
      didEndScroll();
225 226 227 228
    }
  }

  @override
229
  ScrollHoldController hold(VoidCallback holdCancelCallback) {
230 231
    final double previousVelocity = activity.velocity;
    final HoldScrollActivity holdActivity = new HoldScrollActivity(
232 233 234
      delegate: this,
      onHoldCanceled: holdCancelCallback,
    );
235 236 237
    beginActivity(holdActivity);
    _heldPreviousVelocity = previousVelocity;
    return holdActivity;
238 239
  }

240 241
  ScrollDragController _currentDrag;

242
  @override
243
  Drag drag(DragStartDetails details, VoidCallback onDragCanceled) {
244 245 246 247
    final ScrollDragController drag = new ScrollDragController(
      delegate: this,
      details: details,
      onDragCanceled: onDragCanceled,
248
      carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
249
      motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
250
    );
251 252 253 254
    beginActivity(new DragScrollActivity(this, drag));
    assert(_currentDrag == null);
    _currentDrag = drag;
    return drag;
255 256 257 258
  }

  @override
  void dispose() {
259 260
    _currentDrag?.dispose();
    _currentDrag = null;
261 262 263 264 265 266 267 268 269 270 271 272
    super.dispose();
  }

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