scrollable.dart 17.7 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:async';
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/scheduler.dart';
11 12 13 14

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
15
import 'notification_listener.dart';
16
import 'scroll_configuration.dart';
17
import 'scroll_context.dart';
18
import 'scroll_controller.dart';
19
import 'scroll_physics.dart';
20
import 'scroll_position.dart';
21
import 'scroll_position_with_single_context.dart';
22
import 'ticker_provider.dart';
23 24 25 26
import 'viewport.dart';

export 'package:flutter/physics.dart' show Tolerance;

27 28
/// Signature used by [Scrollable] to build the viewport through which the
/// scrollable content is displayed.
29 30
typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
/// 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.
///
/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
/// or [GridView], which combine scrolling, viewporting, and a layout model. To
/// combine layout models (or to use a custom layout mode), consider using
/// [CustomScrollView].
///
/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
/// often used to interact with the [Scrollable] widget inside a [ListView] or
/// a [GridView].
///
/// 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.
///
/// See also:
///
///  * [ListView], which is a commonly used [ScrollView] that displays a
///    scrolling, linear list of child widgets.
///  * [PageView], which is a scrolling list of child widgets that are each the
///    size of the viewport.
///  * [GridView], which is a [ScrollView] that displays a scrolling, 2D array
///    of child widgets.
///  * [CustomScrollView], which is a [ScrollView] that creates custom scroll
///    effects using slivers.
///  * [SingleChildScrollView], which is a scrollable widget that has a single
///    child.
70
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
71
///    the scroll position without using a [ScrollController].
Adam Barth's avatar
Adam Barth committed
72
class Scrollable extends StatefulWidget {
73 74 75
  /// Creates a widget that scrolls.
  ///
  /// The [axisDirection] and [viewportBuilder] arguments must not be null.
76
  const Scrollable({
77 78
    Key key,
    this.axisDirection: AxisDirection.down,
79
    this.controller,
Adam Barth's avatar
Adam Barth committed
80
    this.physics,
81
    @required this.viewportBuilder,
82 83 84
  }) : assert(axisDirection != null),
       assert(viewportBuilder != null),
       super (key: key);
85

86 87 88 89 90 91 92 93 94 95
  /// The direction in which this widget scrolls.
  ///
  /// For example, if the [axisDirection] is [AxisDirection.down], increasing
  /// the scroll position will cause content below the bottom of the viewport to
  /// become visible through the viewport. Similarly, if [axisDirection] is
  /// [AxisDirection.right], increasing the scroll position will cause content
  /// beyond the right edge of the viewport to become visible through the
  /// viewport.
  ///
  /// Defaults to [AxisDirection.down].
96 97
  final AxisDirection axisDirection;

98 99 100
  /// An object that can be used to control the position to which this widget is
  /// scrolled.
  ///
101 102 103 104 105 106 107 108
  /// A [ScrollController] serves several purposes. It can be used to control
  /// the initial scroll position (see [ScrollController.initialScrollOffset]).
  /// It can be used to control whether the scroll view should automatically
  /// save and restore its scroll position in the [PageStorage] (see
  /// [ScrollController.keepScrollOffset]). It can be used to read the current
  /// scroll position (see [ScrollController.offset]), or change it (see
  /// [ScrollController.animateTo]).
  ///
109 110 111 112
  /// See also:
  ///
  ///  * [ensureVisible], which animates the scroll position to reveal a given
  ///    [BuildContext].
113 114
  final ScrollController controller;

115 116 117 118 119
  /// How the widgets should respond to user input.
  ///
  /// For example, determines how the widget continues to animate after the
  /// user stops dragging the scroll view.
  ///
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  /// Defaults to matching platform conventions via the physics provided from
  /// the ambient [ScrollConfiguration].
  ///
  /// The physics can be changed dynamically, but new physics will only take
  /// effect if the _class_ of the provided object changes. Merely constructing
  /// a new instance with a different configuration is insufficient to cause the
  /// physics to be reapplied. (This is because the final object used is
  /// generated dynamically, which can be relatively expensive, and it would be
  /// inefficient to speculatively create this object each frame to see if the
  /// physics should be updated.)
  ///
  /// See also:
  ///
  ///  * [AlwaysScrollableScrollPhysics], which can be used to indicate that the
  ///    scrollable should react to scroll requests (and possible overscroll)
  ///    even if the scrollable's contents fit without scrolling being necessary.
Adam Barth's avatar
Adam Barth committed
136 137
  final ScrollPhysics physics;

138 139 140 141 142 143 144 145 146 147
  /// Builds the viewport through which the scrollable content is displayed.
  ///
  /// A typical viewport uses the given [ViewportOffset] to determine which part
  /// of its content is actually visible through the viewport.
  ///
  /// See also:
  ///
  ///  * [Viewport], which is a viewport that displays a list of slivers.
  ///  * [ShrinkWrappingViewport], which is a viewport that displays a list of
  ///    slivers and sizes itself based on the size of the slivers.
148
  final ViewportBuilder viewportBuilder;
149

150 151 152
  /// The axis along which the scroll view scrolls.
  ///
  /// Determined by the [axisDirection].
153 154 155
  Axis get axis => axisDirectionToAxis(axisDirection);

  @override
Adam Barth's avatar
Adam Barth committed
156
  ScrollableState createState() => new ScrollableState();
157 158

  @override
159
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
160 161 162
    super.debugFillProperties(description);
    description.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection));
    description.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics));
163
  }
164 165 166 167 168 169

  /// The state from the closest instance of this class that encloses the given context.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
Adam Barth's avatar
Adam Barth committed
170
  /// ScrollableState scrollable = Scrollable.of(context);
171
  /// ```
Adam Barth's avatar
Adam Barth committed
172
  static ScrollableState of(BuildContext context) {
173 174
    final _ScrollableScope widget = context.inheritFromWidgetOfExactType(_ScrollableScope);
    return widget?.scrollable;
175 176
  }

177 178
  /// Scrolls the scrollables that enclose the given context so as to make the
  /// given context visible.
179 180 181 182 183 184 185
  static Future<Null> ensureVisible(BuildContext context, {
    double alignment: 0.0,
    Duration duration: Duration.ZERO,
    Curve curve: Curves.ease,
  }) {
    final List<Future<Null>> futures = <Future<Null>>[];

Adam Barth's avatar
Adam Barth committed
186
    ScrollableState scrollable = Scrollable.of(context);
187
    while (scrollable != null) {
188 189 190 191 192 193
      futures.add(scrollable.position.ensureVisible(
        context.findRenderObject(),
        alignment: alignment,
        duration: duration,
        curve: curve,
      ));
194
      context = scrollable.context;
Adam Barth's avatar
Adam Barth committed
195
      scrollable = Scrollable.of(context);
196 197 198 199 200
    }

    if (futures.isEmpty || duration == Duration.ZERO)
      return new Future<Null>.value();
    if (futures.length == 1)
201
      return futures.single;
202 203
    return Future.wait<Null>(futures);
  }
204 205
}

206 207 208
// Enable Scrollable.of() to work as if ScrollableState was an inherited widget.
// ScrollableState.build() always rebuilds its _ScrollableScope.
class _ScrollableScope extends InheritedWidget {
209
  const _ScrollableScope({
210 211 212 213
    Key key,
    @required this.scrollable,
    @required this.position,
    @required Widget child
214 215 216
  }) : assert(scrollable != null),
       assert(child != null),
       super(key: key, child: child);
217 218 219 220 221 222 223 224 225 226

  final ScrollableState scrollable;
  final ScrollPosition position;

  @override
  bool updateShouldNotify(_ScrollableScope old) {
    return position != old.position;
  }
}

Adam Barth's avatar
Adam Barth committed
227
/// State object for a [Scrollable] widget.
228
///
Adam Barth's avatar
Adam Barth committed
229
/// To manipulate a [Scrollable] widget's scroll position, use the object
230 231
/// obtained from the [position] property.
///
Adam Barth's avatar
Adam Barth committed
232 233
/// To be informed of when a [Scrollable] widget is scrolling, use a
/// [NotificationListener] to listen for [ScrollNotification] notifications.
234 235
///
/// This class is not intended to be subclassed. To specialize the behavior of a
Adam Barth's avatar
Adam Barth committed
236 237
/// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
238 239
    implements ScrollContext {
  /// The manager for this [Scrollable] widget's viewport position.
240
  ///
Adam Barth's avatar
Adam Barth committed
241
  /// To control what kind of [ScrollPosition] is created for a [Scrollable],
242 243
  /// provide it with custom [ScrollController] that creates the appropriate
  /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
244 245 246
  ScrollPosition get position => _position;
  ScrollPosition _position;

247 248 249
  @override
  AxisDirection get axisDirection => widget.axisDirection;

Adam Barth's avatar
Adam Barth committed
250
  ScrollBehavior _configuration;
251
  ScrollPhysics _physics;
252

253
  // Only call this from places that will definitely trigger a rebuild.
254
  void _updatePosition() {
Adam Barth's avatar
Adam Barth committed
255
    _configuration = ScrollConfiguration.of(context);
256
    _physics = _configuration.getScrollPhysics(context);
257 258 259
    if (widget.physics != null)
      _physics = widget.physics.applyTo(_physics);
    final ScrollController controller = widget.controller;
260 261
    final ScrollPosition oldPosition = position;
    if (oldPosition != null) {
262
      controller?.detach(oldPosition);
263
      oldPosition.removeListener(_sendSemanticsScrollEvent);
264 265 266
      // It's important that we not dispose the old position until after the
      // viewport has had a chance to unregister its listeners from the old
      // position. So, schedule a microtask to do it.
267 268
      scheduleMicrotask(oldPosition.dispose);
    }
269

270
    _position = controller?.createScrollPosition(_physics, this, oldPosition)
271
      ?? new ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
272
    _position.addListener(_sendSemanticsScrollEvent);
273

274
    assert(position != null);
275
    controller?.attach(position);
276 277
  }

278 279 280 281 282 283 284 285 286 287 288 289
  bool _semanticsScrollEventScheduled = false;

  void _sendSemanticsScrollEvent() {
    if (_semanticsScrollEventScheduled)
      return;
    SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
      _gestureDetectorKey.currentState?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent());
      _semanticsScrollEventScheduled = false;
    });
    _semanticsScrollEventScheduled = true;
  }

290
  @override
291 292
  void didChangeDependencies() {
    super.didChangeDependencies();
293 294 295
    _updatePosition();
  }

296
  bool _shouldUpdatePosition(Scrollable oldWidget) {
297 298 299 300 301 302 303 304 305 306
    ScrollPhysics newPhysics = widget.physics;
    ScrollPhysics oldPhysics = oldWidget.physics;
    do {
      if (newPhysics?.runtimeType != oldPhysics?.runtimeType)
        return true;
      newPhysics = newPhysics?.parent;
      oldPhysics = oldPhysics?.parent;
    } while (newPhysics != null || oldPhysics != null);

    return widget.controller?.runtimeType != oldWidget.controller?.runtimeType;
307 308
  }

309
  @override
310 311
  void didUpdateWidget(Scrollable oldWidget) {
    super.didUpdateWidget(oldWidget);
312

313 314 315
    if (widget.controller != oldWidget.controller) {
      oldWidget.controller?.detach(position);
      widget.controller?.attach(position);
316 317
    }

318
    if (_shouldUpdatePosition(oldWidget))
319 320 321 322 323
      _updatePosition();
  }

  @override
  void dispose() {
324
    widget.controller?.detach(position);
325 326 327 328 329
    position.dispose();
    super.dispose();
  }


330 331 332 333 334 335 336 337 338 339
  // SEMANTICS ACTIONS

  @override
  @protected
  void setSemanticsActions(Set<SemanticsAction> actions) {
    if (_gestureDetectorKey.currentState != null)
      _gestureDetectorKey.currentState.replaceSemanticsActions(actions);
  }


340 341 342 343 344 345 346 347 348 349 350 351
  // GESTURE RECOGNITION AND POINTER IGNORING

  final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
  final GlobalKey _ignorePointerKey = new GlobalKey();

  // This field is set during layout, and then reused until the next time it is set.
  Map<Type, GestureRecognizerFactory> _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
  bool _shouldIgnorePointer = false;

  bool _lastCanDrag;
  Axis _lastAxisDirection;

352 353 354
  @override
  @protected
  void setCanDrag(bool canDrag) {
355
    if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection))
356 357 358 359
      return;
    if (!canDrag) {
      _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
    } else {
360
      switch (widget.axis) {
361 362
        case Axis.vertical:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
363 364 365 366 367 368 369 370 371 372 373 374 375 376
            VerticalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
              () => new VerticalDragGestureRecognizer(),
              (VerticalDragGestureRecognizer instance) {
                instance
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics?.minFlingDistance
                  ..minFlingVelocity = _physics?.minFlingVelocity
                  ..maxFlingVelocity = _physics?.maxFlingVelocity;
              },
            ),
377 378 379 380
          };
          break;
        case Axis.horizontal:
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
381 382 383 384 385 386 387 388 389 390 391 392 393 394
            HorizontalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
              () => new HorizontalDragGestureRecognizer(),
              (HorizontalDragGestureRecognizer instance) {
                instance
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics?.minFlingDistance
                  ..minFlingVelocity = _physics?.minFlingVelocity
                  ..maxFlingVelocity = _physics?.maxFlingVelocity;
              },
            ),
395 396 397 398 399
          };
          break;
      }
    }
    _lastCanDrag = canDrag;
400
    _lastAxisDirection = widget.axis;
401 402 403 404
    if (_gestureDetectorKey.currentState != null)
      _gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers);
  }

405 406 407 408 409 410
  @override
  TickerProvider get vsync => this;

  @override
  @protected
  void setIgnorePointer(bool value) {
411 412 413 414
    if (_shouldIgnorePointer == value)
      return;
    _shouldIgnorePointer = value;
    if (_ignorePointerKey.currentContext != null) {
415
      final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject();
416 417 418 419
      renderBox.ignoring = _shouldIgnorePointer;
    }
  }

420
  @override
421
  BuildContext get notificationContext => _gestureDetectorKey.currentContext;
422

423 424 425
  @override
  BuildContext get storageContext => context;

426 427
  // TOUCH HANDLERS

428
  Drag _drag;
429
  ScrollHoldController _hold;
430 431 432

  void _handleDragDown(DragDownDetails details) {
    assert(_drag == null);
433 434
    assert(_hold == null);
    _hold = position.hold(_disposeHold);
435 436 437 438
  }

  void _handleDragStart(DragStartDetails details) {
    assert(_drag == null);
439
    assert(_hold != null);
440
    _drag = position.drag(details, _disposeDrag);
441
    assert(_drag != null);
442
    assert(_hold == null);
443 444 445
  }

  void _handleDragUpdate(DragUpdateDetails details) {
446
    // _drag might be null if the drag activity ended and called _disposeDrag.
447
    assert(_hold == null || _drag == null);
448
    _drag?.update(details);
449 450 451
  }

  void _handleDragEnd(DragEndDetails details) {
452
    // _drag might be null if the drag activity ended and called _disposeDrag.
453
    assert(_hold == null || _drag == null);
454
    _drag?.end(details);
455 456 457
    assert(_drag == null);
  }

458
  void _handleDragCancel() {
459
    // _hold might be null if the drag started.
460
    // _drag might be null if the drag activity ended and called _disposeDrag.
461 462
    assert(_hold == null || _drag == null);
    _hold?.cancel();
463
    _drag?.cancel();
464
    assert(_hold == null);
465 466 467
    assert(_drag == null);
  }

468 469 470 471
  void _disposeHold() {
    _hold = null;
  }

472
  void _disposeDrag() {
473 474 475
    _drag = null;
  }

476 477 478 479 480 481 482

  // DESCRIPTION

  @override
  Widget build(BuildContext context) {
    assert(position != null);
    // TODO(ianh): Having all these global keys is sad.
483
    final Widget result = new RawGestureDetector(
484 485 486 487 488 489
      key: _gestureDetectorKey,
      gestures: _gestureRecognizers,
      behavior: HitTestBehavior.opaque,
      child: new IgnorePointer(
        key: _ignorePointerKey,
        ignoring: _shouldIgnorePointer,
490
        ignoringSemantics: false,
491 492 493 494 495
        child: new _ScrollableScope(
          scrollable: this,
          position: position,
          child: widget.viewportBuilder(context, position),
        ),
496 497
      ),
    );
498
    return _configuration.buildViewportChrome(context, result, widget.axisDirection);
499 500 501
  }

  @override
502
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
503 504
    super.debugFillProperties(description);
    description.add(new DiagnosticsProperty<ScrollPosition>('position', position));
505 506
  }
}