scrollable.dart 24.5 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
import 'dart:math' as math;
7
import 'dart:ui' as ui show window;
8 9

import 'package:newton/newton.dart';
10
import 'package:flutter/gestures.dart';
11
import 'package:flutter/rendering.dart' show HasScrollDirection;
12 13 14 15 16

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'mixed_viewport.dart';
17
import 'notification_listener.dart';
Adam Barth's avatar
Adam Barth committed
18
import 'page_storage.dart';
19
import 'scroll_behavior.dart';
20

21
/// The accuracy to which scrolling is computed.
Hans Muller's avatar
Hans Muller committed
22 23 24 25 26
final Tolerance kPixelScrollTolerance = new Tolerance(
  velocity: 1.0 / (0.050 * ui.window.devicePixelRatio),  // logical pixels per second
  distance: 1.0 / ui.window.devicePixelRatio  // logical pixels
);

27
typedef void ScrollListener(double scrollOffset);
28
typedef double SnapOffsetCallback(double scrollOffset, Size containerSize);
29

30 31 32 33 34 35 36
/// A base class for scrollable widgets.
///
/// Commonly used subclasses include [ScrollableList], [ScrollableGrid], and
/// [ScrollableViewport].
///
/// Widgets that subclass [Scrollable] typically use state objects that subclass
/// [ScrollableState].
37 38
abstract class Scrollable extends StatefulComponent {
  Scrollable({
39
    Key key,
40
    this.initialScrollOffset,
41
    this.scrollDirection: Axis.vertical,
42
    this.scrollAnchor: ViewportAnchor.start,
43
    this.onScrollStart,
44
    this.onScroll,
45
    this.onScrollEnd,
46
    this.snapOffsetCallback
47
  }) : super(key: key) {
48 49
    assert(scrollDirection == Axis.vertical || scrollDirection == Axis.horizontal);
    assert(scrollAnchor == ViewportAnchor.start || scrollAnchor == ViewportAnchor.end);
50
  }
51

52
  /// The scroll offset this widget should use when first created.
53
  final double initialScrollOffset;
54 55

  /// The axis along which this widget should scroll.
56
  final Axis scrollDirection;
57

58 59
  final ViewportAnchor scrollAnchor;

60
  /// Called whenever this widget starts to scroll.
61
  final ScrollListener onScrollStart;
62 63

  /// Called whenever this widget's scroll offset changes.
64
  final ScrollListener onScroll;
65 66

  /// Called whenever this widget stops scrolling.
67
  final ScrollListener onScrollEnd;
68

69 70 71 72 73 74 75 76 77 78 79 80 81
  /// Called to determine the offset to which scrolling should snap,
  /// when handling a fling.
  ///
  /// This callback, if set, will be called with the offset that the
  /// Scrollable would have scrolled to in the absence of this
  /// callback, and a Size describing the size of the Scrollable
  /// itself.
  ///
  /// The callback's return value is used as the new scroll offset to
  /// aim for.
  ///
  /// If the callback simply returns its first argument (the offset),
  /// then it is as if the callback was null.
82
  final SnapOffsetCallback snapOffsetCallback;
83

84
  /// The state from the closest instance of this class that encloses the given context.
85
  static ScrollableState of(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
86
    return context.ancestorStateOfType(const TypeMatcher<ScrollableState>());
87 88 89
  }

  /// Scrolls the closest enclosing scrollable to make the given context visible.
90
  static Future ensureVisible(BuildContext context, { Duration duration, Curve curve: Curves.ease }) {
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    assert(context.findRenderObject() is RenderBox);
    // TODO(abarth): This function doesn't handle nested scrollable widgets.

    ScrollableState scrollable = Scrollable.of(context);
    if (scrollable == null)
      return new Future.value();

    RenderBox targetBox = context.findRenderObject();
    assert(targetBox.attached);
    Size targetSize = targetBox.size;

    RenderBox scrollableBox = scrollable.context.findRenderObject();
    assert(scrollableBox.attached);
    Size scrollableSize = scrollableBox.size;

106 107 108 109 110
    double targetMin;
    double targetMax;
    double scrollableMin;
    double scrollableMax;

111
    switch (scrollable.config.scrollDirection) {
112
      case Axis.vertical:
113 114 115 116
        targetMin = targetBox.localToGlobal(Point.origin).y;
        targetMax = targetBox.localToGlobal(new Point(0.0, targetSize.height)).y;
        scrollableMin = scrollableBox.localToGlobal(Point.origin).y;
        scrollableMax = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height)).y;
117
        break;
118
      case Axis.horizontal:
119 120 121 122
        targetMin = targetBox.localToGlobal(Point.origin).x;
        targetMax = targetBox.localToGlobal(new Point(targetSize.width, 0.0)).x;
        scrollableMin = scrollableBox.localToGlobal(Point.origin).x;
        scrollableMax = scrollableBox.localToGlobal(new Point(scrollableSize.width, 0.0)).x;
123 124 125
        break;
    }

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
    double scrollOffsetDelta;
    if (targetMin < scrollableMin) {
      if (targetMax > scrollableMax) {
        // The target is to big to fit inside the scrollable. The best we can do
        // is to center the target.
        double targetCenter = (targetMin + targetMax) / 2.0;
        double scrollableCenter = (scrollableMin + scrollableMax) / 2.0;
        scrollOffsetDelta = targetCenter - scrollableCenter;
      } else {
        scrollOffsetDelta = targetMin - scrollableMin;
      }
    } else if (targetMax > scrollableMax) {
      scrollOffsetDelta = targetMax - scrollableMax;
    } else {
      return new Future.value();
    }

143 144 145 146 147 148 149 150 151 152
    ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
    double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);

    if (scrollOffset != scrollable.scrollOffset)
      return scrollable.scrollTo(scrollOffset, duration: duration, curve: curve);

    return new Future.value();
  }

153
  ScrollableState createState();
154
}
155

156 157 158 159
/// Contains the state for common scrolling behaviors.
///
/// Widgets that subclass [Scrollable] typically use state objects that subclass
/// [ScrollableState].
160
abstract class ScrollableState<T extends Scrollable> extends State<T> {
161
  void initState() {
162
    super.initState();
163
    _controller = new AnimationController.unbounded()..addListener(_handleAnimationChanged);
Adam Barth's avatar
Adam Barth committed
164
    _scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
165 166
  }

167
  AnimationController _controller;
168

169 170 171 172 173
  /// The current scroll offset.
  ///
  /// The scroll offset is applied to the child widget along the scroll
  /// direction before painting. A positive scroll offset indicates that
  /// more content in the preferred reading direction is visible.
174
  double get scrollOffset => _scrollOffset;
Ian Hickson's avatar
Ian Hickson committed
175
  double _scrollOffset;
176

Hans Muller's avatar
Hans Muller committed
177 178 179 180
  /// Convert a position or velocity measured in terms of pixels to a scrollOffset.
  /// Scrollable gesture handlers convert their incoming values with this method.
  /// Subclasses that define scrollOffset in units other than pixels must
  /// override this method.
181 182 183 184 185 186 187 188 189 190
  double pixelOffsetToScrollOffset(double pixelOffset) {
    switch (config.scrollAnchor) {
      case ViewportAnchor.start:
        // We negate the delta here because a positive scroll offset moves the
        // the content up (or to the left) rather than down (or the right).
        return -pixelOffset;
      case ViewportAnchor.end:
        return pixelOffset;
    }
  }
Hans Muller's avatar
Hans Muller committed
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
  double scrollOffsetToPixelOffset(double scrollOffset) {
    switch (config.scrollAnchor) {
      case ViewportAnchor.start:
        return -scrollOffset;
      case ViewportAnchor.end:
        return scrollOffset;
    }
  }

  /// Returns the scroll offset component of the given pixel delta, accounting
  /// for the scroll direction and scroll anchor.
  double pixelDeltaToScrollOffset(Offset pixelDelta) {
    switch (config.scrollDirection) {
      case Axis.horizontal:
        return pixelOffsetToScrollOffset(pixelDelta.dx);
      case Axis.vertical:
        return pixelOffsetToScrollOffset(pixelDelta.dy);
    }
  }

  /// Returns a two-dimensional representation of the scroll offset, accounting
  /// for the scroll direction and scroll anchor.
  Offset scrollOffsetToPixelDelta(double scrollOffset) {
    switch (config.scrollDirection) {
      case Axis.horizontal:
        return new Offset(scrollOffsetToPixelOffset(scrollOffset), 0.0);
      case Axis.vertical:
        return new Offset(0.0, scrollOffsetToPixelOffset(scrollOffset));
    }
Hans Muller's avatar
Hans Muller committed
221 222
  }

223
  ScrollBehavior _scrollBehavior;
224 225 226

  /// Subclasses should override this function to create the [ScrollBehavior]
  /// they desire.
227
  ScrollBehavior createScrollBehavior();
228 229 230 231 232 233

  /// The current scroll behavior of this widget.
  ///
  /// Scroll behaviors control where the boundaries of the scrollable are placed
  /// and how the scrolling physics should behave near those boundaries and
  /// after the user stops directly manipulating the scrollable.
234 235 236 237 238 239
  ScrollBehavior get scrollBehavior {
    if (_scrollBehavior == null)
      _scrollBehavior = createScrollBehavior();
    return _scrollBehavior;
  }

240
  GestureDragStartCallback _getDragStartHandler(Axis direction) {
241 242 243 244 245
    if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
      return null;
    return _handleDragStart;
  }

246
  GestureDragUpdateCallback _getDragUpdateHandler(Axis direction) {
247
    if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
248
      return null;
249
    return _handleDragUpdate;
250 251
  }

252
  GestureDragEndCallback _getDragEndHandler(Axis direction) {
253
    if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
254
      return null;
255
    return _handleDragEnd;
256 257
  }

258
  Widget build(BuildContext context) {
259
    return new GestureDetector(
260 261 262 263 264 265
      onVerticalDragStart: _getDragStartHandler(Axis.vertical),
      onVerticalDragUpdate: _getDragUpdateHandler(Axis.vertical),
      onVerticalDragEnd: _getDragEndHandler(Axis.vertical),
      onHorizontalDragStart: _getDragStartHandler(Axis.horizontal),
      onHorizontalDragUpdate: _getDragUpdateHandler(Axis.horizontal),
      onHorizontalDragEnd: _getDragEndHandler(Axis.horizontal),
Hixie's avatar
Hixie committed
266
      behavior: HitTestBehavior.opaque,
267
      child: new Listener(
268
        child: buildContent(context),
269
        onPointerDown: _handlePointerDown
270
      )
271 272 273
    );
  }

274 275 276 277
  /// Subclasses should override this function to build the interior of their
  /// scrollable widget. Scrollable wraps the returned widget in a
  /// [GestureDetector] to observe the user's interaction with this widget and
  /// to adjust the scroll offset accordingly.
278 279
  Widget buildContent(BuildContext context);

280
  Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
281 282 283
    _controller.stop();
    _controller.value = scrollOffset;
    return _controller.animateTo(newScrollOffset, duration: duration, curve: curve);
284 285
  }

286
  bool _scrollOffsetIsInBounds(double scrollOffset) {
287 288 289
    if (scrollBehavior is! ExtentScrollBehavior)
      return false;
    ExtentScrollBehavior behavior = scrollBehavior;
290
    return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
291 292
  }

293 294
  Simulation _createFlingSimulation(double scrollVelocity) {
    final Simulation simulation =  scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
295
    if (simulation != null) {
296 297 298
      final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity) * scrollVelocity.sign;
      final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
      simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
299 300
    }
    return simulation;
301 302
  }

303 304
  /// Returns the snapped offset closest to the given scroll offset.
  double snapScrollOffset(double scrollOffset) {
305 306
    RenderBox box = context.findRenderObject();
    return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, box.size);
Hans Muller's avatar
Hans Muller committed
307 308
  }

309
  /// Whether this scrollable should attempt to snap scroll offsets.
310
  bool get shouldSnapScrollOffset => config.snapOffsetCallback != null;
Hans Muller's avatar
Hans Muller committed
311

312 313
  Simulation _createSnapSimulation(double scrollVelocity) {
    if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
314 315
      return null;

316
    Simulation simulation = _createFlingSimulation(scrollVelocity);
317 318 319
    if (simulation == null)
        return null;

320
    final double endScrollOffset = simulation.x(double.INFINITY);
321 322 323
    if (endScrollOffset.isNaN)
      return null;

324 325
    final double snappedScrollOffset = snapScrollOffset(endScrollOffset);
    if (!_scrollOffsetIsInBounds(snappedScrollOffset))
326 327
      return null;

328
    final double snapVelocity = scrollVelocity.abs() * (snappedScrollOffset - scrollOffset).sign;
329
    final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * scrollVelocity.sign;
330 331 332
    Simulation toSnapSimulation = scrollBehavior.createSnapScrollSimulation(
      scrollOffset, snappedScrollOffset, snapVelocity, endVelocity
    );
333 334 335
    if (toSnapSimulation == null)
      return null;

336 337
    final double scrollOffsetMin = math.min(scrollOffset, snappedScrollOffset);
    final double scrollOffsetMax = math.max(scrollOffset, snappedScrollOffset);
338
    return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
339 340
  }

341
  Future _startToEndAnimation(double scrollVelocity) {
342
    _controller.stop();
343
    Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
344 345
    if (simulation == null)
      return new Future.value();
346
    return _controller.animateWith(simulation);
347 348
  }

349
  void dispose() {
350
    _controller.stop();
351
    super.dispose();
352 353
  }

354 355 356 357
  void _handleAnimationChanged() {
    _setScrollOffset(_controller.value);
  }

358 359 360 361 362 363
  void _setScrollOffset(double newScrollOffset) {
    if (_scrollOffset == newScrollOffset)
      return;
    setState(() {
      _scrollOffset = newScrollOffset;
    });
Adam Barth's avatar
Adam Barth committed
364
    PageStorage.of(context)?.writeState(context, _scrollOffset);
365
    new ScrollNotification(this, _scrollOffset).dispatch(context);
366
    dispatchOnScroll();
367 368
  }

369 370 371 372
  /// Scroll this widget to the given scroll offset.
  ///
  /// If a non-null [duration] is provided, the widget will animate to the new
  /// scroll offset over the given duration with the given curve.
373
  Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: Curves.ease }) {
374
    if (newScrollOffset == _scrollOffset)
375
      return new Future.value();
376

377
    if (duration == null) {
378
      _controller.stop();
379 380
      _setScrollOffset(newScrollOffset);
      return new Future.value();
381 382
    }

383
    return _animateTo(newScrollOffset, duration, curve);
384 385
  }

386 387 388 389
  /// Scroll this widget by the given scroll delta.
  ///
  /// If a non-null [duration] is provided, the widget will animate to the new
  /// scroll offset over the given duration with the given curve.
390
  Future scrollBy(double scrollDelta, { Duration duration, Curve curve: Curves.ease }) {
391
    double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
392
    return scrollTo(newScrollOffset, duration: duration, curve: curve);
393 394
  }

395 396 397 398 399
  /// Fling the scroll offset with the given velocity.
  ///
  /// Calling this function starts a physics-based animation of the scroll
  /// offset with the given value as the initial velocity. The physics
  /// simulation used is determined by the scroll behavior.
400 401
  Future fling(double scrollVelocity) {
    if (scrollVelocity != 0.0)
Hans Muller's avatar
Hans Muller committed
402
      return _startToEndAnimation(scrollVelocity);
403
    if (!_controller.isAnimating)
404 405
      return settleScrollOffset();
    return new Future.value();
406 407
  }

408 409 410 411 412
  /// Animate the scroll offset to a value with a local minima of energy.
  ///
  /// Calling this function starts a physics-based animation of the scroll
  /// offset either to a snap point or to within the scrolling bounds. The
  /// physics simulation used is determined by the scroll behavior.
413
  Future settleScrollOffset() {
414
    return _startToEndAnimation(0.0);
415 416
  }

417 418 419
  /// Calls the onScrollStart callback.
  ///
  /// Subclasses can override this function to hook the scroll start callback.
420 421 422 423 424
  void dispatchOnScrollStart() {
    if (config.onScrollStart != null)
      config.onScrollStart(_scrollOffset);
  }

425 426 427
  /// Calls the onScroll callback.
  ///
  /// Subclasses can override this function to hook the scroll callback.
428 429 430 431 432
  void dispatchOnScroll() {
    if (config.onScroll != null)
      config.onScroll(_scrollOffset);
  }

433 434 435
  /// Calls the dispatchOnScrollEnd callback.
  ///
  /// Subclasses can override this function to hook the scroll end callback.
436 437 438 439 440
  void dispatchOnScrollEnd() {
    if (config.onScrollEnd != null)
      config.onScrollEnd(_scrollOffset);
  }

Adam Barth's avatar
Adam Barth committed
441
  void _handlePointerDown(_) {
442
    _controller.stop();
443 444
  }

445
  void _handleDragStart(_) {
446 447 448
    scheduleMicrotask(dispatchOnScrollStart);
  }

449
  void _handleDragUpdate(double delta) {
450
    scrollBy(pixelOffsetToScrollOffset(delta));
Hans Muller's avatar
Hans Muller committed
451 452
  }

453 454
  Future _handleDragEnd(Velocity velocity) {
    double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
455 456 457
    // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
    return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) {
      dispatchOnScrollEnd();
458
    });
459
  }
460 461
}

462
/// Indicates that a descendant scrollable has scrolled.
463
class ScrollNotification extends Notification {
464 465 466
  ScrollNotification(this.scrollable, this.scrollOffset);

  /// The scrollable that scrolled.
467
  final ScrollableState scrollable;
468 469 470

  /// The new scroll offset that the scrollable obtained.
  final double scrollOffset;
471 472
}

473 474 475
/// A simple scrollable widget that has a single child. Use this component if
/// you are not worried about offscreen widgets consuming resources.
class ScrollableViewport extends Scrollable {
476 477
  ScrollableViewport({
    Key key,
478
    double initialScrollOffset,
479
    Axis scrollDirection: Axis.vertical,
480
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
481 482
    ScrollListener onScrollStart,
    ScrollListener onScroll,
483 484
    ScrollListener onScrollEnd,
    this.child
485 486 487
  }) : super(
    key: key,
    scrollDirection: scrollDirection,
488
    scrollAnchor: scrollAnchor,
489
    initialScrollOffset: initialScrollOffset,
490 491 492
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd
493
  );
494

495
  final Widget child;
496

497
  ScrollableState createState() => new _ScrollableViewportState();
498
}
499

500
class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
501 502
  ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
  OverscrollWhenScrollableBehavior get scrollBehavior => super.scrollBehavior;
503

504 505
  double _viewportSize = 0.0;
  double _childSize = 0.0;
506
  void _handleViewportSizeChanged(Size newSize) {
507
    _viewportSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
Hixie's avatar
Hixie committed
508
    setState(() {
509
      _updateScrollBehavior();
Hixie's avatar
Hixie committed
510
    });
511 512
  }
  void _handleChildSizeChanged(Size newSize) {
513
    _childSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
Hixie's avatar
Hixie committed
514
    setState(() {
515
      _updateScrollBehavior();
Hixie's avatar
Hixie committed
516
    });
517
  }
518
  void _updateScrollBehavior() {
519
    // if you don't call this from build(), you must call it from setState().
520
    scrollTo(scrollBehavior.updateExtents(
521 522
      contentExtent: _childSize,
      containerExtent: _viewportSize,
Hixie's avatar
Hixie committed
523 524
      scrollOffset: scrollOffset
    ));
525 526
  }

527
  Widget buildContent(BuildContext context) {
528
    return new SizeObserver(
529
      onSizeChanged: _handleViewportSizeChanged,
530
      child: new Viewport(
531
        paintOffset: scrollOffsetToPixelDelta(scrollOffset),
532 533
        scrollDirection: config.scrollDirection,
        scrollAnchor: config.scrollAnchor,
534
        child: new SizeObserver(
535
          onSizeChanged: _handleChildSizeChanged,
536
          child: config.child
537 538 539 540 541 542
        )
      )
    );
  }
}

543
/// A mashup of [ScrollableViewport] and [BlockBody]. Useful when you have a small,
544 545
/// fixed number of children that you wish to arrange in a block layout and that
/// might exceed the height of its container (and therefore need to scroll).
546
class Block extends StatelessComponent {
547
  Block({
548
    Key key,
549
    this.children: const <Widget>[],
Hixie's avatar
Hixie committed
550
    this.padding,
551
    this.initialScrollOffset,
552
    this.scrollDirection: Axis.vertical,
553
    this.scrollAnchor: ViewportAnchor.start,
554 555
    this.onScroll,
    this.scrollableKey
556
  }) : super(key: key) {
557
    assert(children != null);
558 559
    assert(!children.any((Widget child) => child == null));
  }
560 561

  final List<Widget> children;
Hixie's avatar
Hixie committed
562
  final EdgeDims padding;
563
  final double initialScrollOffset;
564
  final Axis scrollDirection;
565
  final ViewportAnchor scrollAnchor;
566
  final ScrollListener onScroll;
567
  final Key scrollableKey;
568

569
  Widget build(BuildContext context) {
570
    Widget contents = new BlockBody(children: children, direction: scrollDirection);
Hixie's avatar
Hixie committed
571 572
    if (padding != null)
      contents = new Padding(padding: padding, child: contents);
573
    return new ScrollableViewport(
574
      key: scrollableKey,
575
      initialScrollOffset: initialScrollOffset,
576
      scrollDirection: scrollDirection,
577
      scrollAnchor: scrollAnchor,
578
      onScroll: onScroll,
Hixie's avatar
Hixie committed
579
      child: contents
580 581 582 583
    );
  }
}

584 585
abstract class ScrollableListPainter extends Painter {
  void attach(RenderObject renderObject) {
586
    assert(renderObject is RenderBox);
587
    assert(renderObject is HasScrollDirection);
588 589 590
    super.attach(renderObject);
  }

591
  RenderBox get renderObject => super.renderObject;
592

593
  Axis get scrollDirection {
594 595
    HasScrollDirection scrollable = renderObject as dynamic;
    return scrollable?.scrollDirection;
596
  }
597

598
  Size get viewportSize => renderObject.size;
599 600 601 602 603 604 605 606 607

  double get contentExtent => _contentExtent;
  double _contentExtent = 0.0;
  void set contentExtent (double value) {
    assert(value != null);
    assert(value >= 0.0);
    if (_contentExtent == value)
      return;
    _contentExtent = value;
608
    renderObject?.markNeedsPaint();
609 610 611 612 613 614 615 616 617
  }

  double get scrollOffset => _scrollOffset;
  double _scrollOffset = 0.0;
  void set scrollOffset (double value) {
    assert(value != null);
    if (_scrollOffset == value)
      return;
    _scrollOffset = value;
618
    renderObject?.markNeedsPaint();
619 620 621 622 623 624 625 626 627 628 629 630 631 632
  }

  /// Called when a scroll starts. Subclasses may override this method to
  /// initialize some state or to play an animation. The returned Future should
  /// complete when the computation triggered by this method has finished.
  Future scrollStarted() => new Future.value();


  /// Similar to scrollStarted(). Called when a scroll ends. For fling scrolls
  /// "ended" means that the scroll animation either stopped of its own accord
  /// or was canceled  by the user.
  Future scrollEnded() => new Future.value();
}

633
/// A general scrollable list for a large number of children that might not all
634
/// have the same height. Prefer [ScrollableWidgetList] when all the children
635 636
/// have the same height because it can use that property to be more efficient.
/// Prefer [ScrollableViewport] with a single child.
637 638
class ScrollableMixedWidgetList extends Scrollable {
  ScrollableMixedWidgetList({
639
    Key key,
640
    double initialScrollOffset,
641 642
    ScrollListener onScroll,
    SnapOffsetCallback snapOffsetCallback,
643 644
    this.builder,
    this.token,
645 646 647 648 649
    this.onInvalidatorAvailable
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    onScroll: onScroll,
650
    snapOffsetCallback: snapOffsetCallback
651
  );
652

653 654 655
  final IndexedBuilder builder;
  final Object token;
  final InvalidatorAvailableCallback onInvalidatorAvailable;
656

657 658
  ScrollableMixedWidgetListState createState() => new ScrollableMixedWidgetListState();
}
659

660
class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidgetList> {
661 662
  void initState() {
    super.initState();
663 664 665
    scrollBehavior.updateExtents(
      contentExtent: double.INFINITY
    );
666 667 668 669 670 671
  }

  ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
  OverscrollBehavior get scrollBehavior => super.scrollBehavior;

  void _handleSizeChanged(Size newSize) {
Hixie's avatar
Hixie committed
672 673 674 675 676 677
    setState(() {
      scrollBy(scrollBehavior.updateExtents(
        containerExtent: newSize.height,
        scrollOffset: scrollOffset
      ));
    });
678 679
  }

680 681 682 683 684 685 686 687 688 689 690 691 692 693
  bool _contentChanged = false;

  void didUpdateConfig(ScrollableMixedWidgetList oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (config.token != oldConfig.token) {
      // When the token changes the scrollable's contents may have changed.
      // Remember as much so that after the new contents have been laid out we
      // can adjust the scrollOffset so that the last page of content is still
      // visible.
      _contentChanged = true;
    }
  }

  void _handleExtentsUpdate(double newExtents) {
Hixie's avatar
Hixie committed
694 695 696
    double newScrollOffset;
    setState(() {
      newScrollOffset = scrollBehavior.updateExtents(
697
        contentExtent: newExtents ?? double.INFINITY,
Hixie's avatar
Hixie committed
698 699 700
        scrollOffset: scrollOffset
      );
    });
701 702
    if (_contentChanged) {
      _contentChanged = false;
703
      scrollTo(newScrollOffset);
704 705 706
    }
  }

707
  Widget buildContent(BuildContext context) {
708
    return new SizeObserver(
709
      onSizeChanged: _handleSizeChanged,
710
      child: new MixedViewport(
711
        startOffset: scrollOffset,
712 713 714 715
        builder: config.builder,
        token: config.token,
        onInvalidatorAvailable: config.onInvalidatorAvailable,
        onExtentsUpdate: _handleExtentsUpdate
716 717 718 719
      )
    );
  }
}