scrollable.dart 25 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 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
  Map<Type, GestureRecognizerFactory> buildGestureDetectors() {
    if (scrollBehavior.isScrollable) {
      switch (config.scrollDirection) {
        case Axis.vertical:
          return <Type, GestureRecognizerFactory>{
            VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) {
              return (recognizer ??= new VerticalDragGestureRecognizer())
                ..onStart = _handleDragStart
                ..onUpdate = _handleDragUpdate
                ..onEnd = _handleDragEnd;
            }
          };
        case Axis.horizontal:
          return <Type, GestureRecognizerFactory>{
            HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) {
              return (recognizer ??= new HorizontalDragGestureRecognizer())
                ..onStart = _handleDragStart
                ..onUpdate = _handleDragUpdate
                ..onEnd = _handleDragEnd;
            }
          };
      }
    }
    return const <Type, GestureRecognizerFactory>{};
264 265
  }

266
  final GlobalKey _gestureDetectorKey = new GlobalKey();
267

268 269
  void updateGestureDetector() {
    _gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
270 271
  }

272
  Widget build(BuildContext context) {
273 274 275
    return new RawGestureDetector(
      key: _gestureDetectorKey,
      gestures: buildGestureDetectors(),
Hixie's avatar
Hixie committed
276
      behavior: HitTestBehavior.opaque,
277
      child: new Listener(
278
        child: buildContent(context),
279
        onPointerDown: _handlePointerDown
280
      )
281 282 283
    );
  }

284 285 286 287
  /// 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.
288 289
  Widget buildContent(BuildContext context);

290
  Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
291 292 293
    _controller.stop();
    _controller.value = scrollOffset;
    return _controller.animateTo(newScrollOffset, duration: duration, curve: curve);
294 295
  }

296
  bool _scrollOffsetIsInBounds(double scrollOffset) {
297 298 299
    if (scrollBehavior is! ExtentScrollBehavior)
      return false;
    ExtentScrollBehavior behavior = scrollBehavior;
300
    return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
301 302
  }

303 304
  Simulation _createFlingSimulation(double scrollVelocity) {
    final Simulation simulation =  scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
305
    if (simulation != null) {
306 307 308
      final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity) * scrollVelocity.sign;
      final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
      simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
309 310
    }
    return simulation;
311 312
  }

313 314
  /// Returns the snapped offset closest to the given scroll offset.
  double snapScrollOffset(double scrollOffset) {
315 316
    RenderBox box = context.findRenderObject();
    return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, box.size);
Hans Muller's avatar
Hans Muller committed
317 318
  }

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

322 323
  Simulation _createSnapSimulation(double scrollVelocity) {
    if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
324 325
      return null;

326
    Simulation simulation = _createFlingSimulation(scrollVelocity);
327 328 329
    if (simulation == null)
        return null;

330
    final double endScrollOffset = simulation.x(double.INFINITY);
331 332 333
    if (endScrollOffset.isNaN)
      return null;

334
    final double snappedScrollOffset = snapScrollOffset(endScrollOffset); // invokes the config.snapOffsetCallback callback
335
    if (!_scrollOffsetIsInBounds(snappedScrollOffset))
336 337
      return null;

338
    final double snapVelocity = scrollVelocity.abs() * (snappedScrollOffset - scrollOffset).sign;
339
    final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * scrollVelocity.sign;
340 341 342
    Simulation toSnapSimulation = scrollBehavior.createSnapScrollSimulation(
      scrollOffset, snappedScrollOffset, snapVelocity, endVelocity
    );
343 344 345
    if (toSnapSimulation == null)
      return null;

346 347
    final double scrollOffsetMin = math.min(scrollOffset, snappedScrollOffset);
    final double scrollOffsetMax = math.max(scrollOffset, snappedScrollOffset);
348
    return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
349 350
  }

351
  Future _startToEndAnimation(double scrollVelocity) {
352
    _controller.stop();
353
    Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
354 355
    if (simulation == null)
      return new Future.value();
356
    return _controller.animateWith(simulation);
357 358
  }

359
  void dispose() {
360
    _controller.stop();
361
    super.dispose();
362 363
  }

364 365 366 367
  void _handleAnimationChanged() {
    _setScrollOffset(_controller.value);
  }

368 369 370 371 372 373
  void _setScrollOffset(double newScrollOffset) {
    if (_scrollOffset == newScrollOffset)
      return;
    setState(() {
      _scrollOffset = newScrollOffset;
    });
Adam Barth's avatar
Adam Barth committed
374
    PageStorage.of(context)?.writeState(context, _scrollOffset);
375
    new ScrollNotification(this, _scrollOffset).dispatch(context);
376
    dispatchOnScroll();
377 378
  }

379 380 381 382
  /// 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.
383
  Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: Curves.ease }) {
384
    if (newScrollOffset == _scrollOffset)
385
      return new Future.value();
386

387
    if (duration == null) {
388
      _controller.stop();
389 390
      _setScrollOffset(newScrollOffset);
      return new Future.value();
391 392
    }

393
    return _animateTo(newScrollOffset, duration, curve);
394 395
  }

396 397 398 399
  /// 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.
400
  Future scrollBy(double scrollDelta, { Duration duration, Curve curve: Curves.ease }) {
401
    double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
402
    return scrollTo(newScrollOffset, duration: duration, curve: curve);
403 404
  }

405 406 407 408 409
  /// 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.
410 411
  Future fling(double scrollVelocity) {
    if (scrollVelocity != 0.0)
Hans Muller's avatar
Hans Muller committed
412
      return _startToEndAnimation(scrollVelocity);
413
    if (!_controller.isAnimating)
414 415
      return settleScrollOffset();
    return new Future.value();
416 417
  }

418 419 420 421 422
  /// 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.
423
  Future settleScrollOffset() {
424
    return _startToEndAnimation(0.0);
425 426
  }

427 428 429
  /// Calls the onScrollStart callback.
  ///
  /// Subclasses can override this function to hook the scroll start callback.
430 431 432 433 434
  void dispatchOnScrollStart() {
    if (config.onScrollStart != null)
      config.onScrollStart(_scrollOffset);
  }

435 436 437
  /// Calls the onScroll callback.
  ///
  /// Subclasses can override this function to hook the scroll callback.
438 439 440 441 442
  void dispatchOnScroll() {
    if (config.onScroll != null)
      config.onScroll(_scrollOffset);
  }

443 444 445
  /// Calls the dispatchOnScrollEnd callback.
  ///
  /// Subclasses can override this function to hook the scroll end callback.
446 447 448 449 450
  void dispatchOnScrollEnd() {
    if (config.onScrollEnd != null)
      config.onScrollEnd(_scrollOffset);
  }

Adam Barth's avatar
Adam Barth committed
451
  void _handlePointerDown(_) {
452
    _controller.stop();
453 454
  }

455
  void _handleDragStart(_) {
456
    dispatchOnScrollStart();
457 458
  }

459
  void _handleDragUpdate(double delta) {
460
    scrollBy(pixelOffsetToScrollOffset(delta));
Hans Muller's avatar
Hans Muller committed
461 462
  }

463 464
  Future _handleDragEnd(Velocity velocity) {
    double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
465 466 467
    // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
    return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) {
      dispatchOnScrollEnd();
468
    });
469
  }
470 471
}

472
/// Indicates that a descendant scrollable has scrolled.
473
class ScrollNotification extends Notification {
474 475 476
  ScrollNotification(this.scrollable, this.scrollOffset);

  /// The scrollable that scrolled.
477
  final ScrollableState scrollable;
478 479 480

  /// The new scroll offset that the scrollable obtained.
  final double scrollOffset;
481 482
}

483 484 485
/// 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 {
486 487
  ScrollableViewport({
    Key key,
488
    double initialScrollOffset,
489
    Axis scrollDirection: Axis.vertical,
490
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
491 492
    ScrollListener onScrollStart,
    ScrollListener onScroll,
493 494
    ScrollListener onScrollEnd,
    this.child
495 496 497
  }) : super(
    key: key,
    scrollDirection: scrollDirection,
498
    scrollAnchor: scrollAnchor,
499
    initialScrollOffset: initialScrollOffset,
500 501 502
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd
503
  );
504

505
  final Widget child;
506

507
  ScrollableState createState() => new _ScrollableViewportState();
508
}
509

510
class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
511 512
  ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
  OverscrollWhenScrollableBehavior get scrollBehavior => super.scrollBehavior;
513

514 515
  double _viewportSize = 0.0;
  double _childSize = 0.0;
516 517 518 519 520 521 522 523 524 525 526

  Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
    // We make various state changes here but don't have to do so in a
    // setState() callback because we are called during layout and all
    // we're updating is the new offset, which we are providing to the
    // render object via our return value.
    _viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
    _childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
    _updateScrollBehavior();
    updateGestureDetector();
    return scrollOffsetToPixelDelta(scrollOffset);
527
  }
528

529
  void _updateScrollBehavior() {
530
    // if you don't call this from build(), you must call it from setState().
531
    scrollTo(scrollBehavior.updateExtents(
532 533
      contentExtent: _childSize,
      containerExtent: _viewportSize,
Hixie's avatar
Hixie committed
534 535
      scrollOffset: scrollOffset
    ));
536 537
  }

538
  Widget buildContent(BuildContext context) {
539 540 541 542 543 544
    return new Viewport(
      paintOffset: scrollOffsetToPixelDelta(scrollOffset),
      scrollDirection: config.scrollDirection,
      scrollAnchor: config.scrollAnchor,
      onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
      child: config.child
545 546 547 548
    );
  }
}

549
/// A mashup of [ScrollableViewport] and [BlockBody]. Useful when you have a small,
550 551
/// 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).
552
class Block extends StatelessComponent {
553
  Block({
554
    Key key,
555
    this.children: const <Widget>[],
Hixie's avatar
Hixie committed
556
    this.padding,
557
    this.initialScrollOffset,
558
    this.scrollDirection: Axis.vertical,
559
    this.scrollAnchor: ViewportAnchor.start,
560 561
    this.onScroll,
    this.scrollableKey
562
  }) : super(key: key) {
563
    assert(children != null);
564 565
    assert(!children.any((Widget child) => child == null));
  }
566 567

  final List<Widget> children;
Hixie's avatar
Hixie committed
568
  final EdgeDims padding;
569
  final double initialScrollOffset;
570
  final Axis scrollDirection;
571
  final ViewportAnchor scrollAnchor;
572
  final ScrollListener onScroll;
573
  final Key scrollableKey;
574

575
  Widget build(BuildContext context) {
576
    Widget contents = new BlockBody(children: children, direction: scrollDirection);
Hixie's avatar
Hixie committed
577 578
    if (padding != null)
      contents = new Padding(padding: padding, child: contents);
579
    return new ScrollableViewport(
580
      key: scrollableKey,
581
      initialScrollOffset: initialScrollOffset,
582
      scrollDirection: scrollDirection,
583
      scrollAnchor: scrollAnchor,
584
      onScroll: onScroll,
Hixie's avatar
Hixie committed
585
      child: contents
586 587 588 589
    );
  }
}

590 591
abstract class ScrollableListPainter extends Painter {
  void attach(RenderObject renderObject) {
592
    assert(renderObject is RenderBox);
593
    assert(renderObject is HasScrollDirection);
594 595 596
    super.attach(renderObject);
  }

597
  RenderBox get renderObject => super.renderObject;
598

599
  Axis get scrollDirection {
600 601
    HasScrollDirection scrollable = renderObject as dynamic;
    return scrollable?.scrollDirection;
602
  }
603

604
  Size get viewportSize => renderObject.size;
605 606 607 608 609 610 611 612 613

  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;
614
    renderObject?.markNeedsPaint();
615 616 617 618 619 620 621 622 623
  }

  double get scrollOffset => _scrollOffset;
  double _scrollOffset = 0.0;
  void set scrollOffset (double value) {
    assert(value != null);
    if (_scrollOffset == value)
      return;
    _scrollOffset = value;
624
    renderObject?.markNeedsPaint();
625 626 627 628 629 630 631 632 633 634 635 636 637 638
  }

  /// 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();
}

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

659 660 661
  final IndexedBuilder builder;
  final Object token;
  final InvalidatorAvailableCallback onInvalidatorAvailable;
662

663 664
  ScrollableMixedWidgetListState createState() => new ScrollableMixedWidgetListState();
}
665

666
class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidgetList> {
667 668
  void initState() {
    super.initState();
669 670 671
    scrollBehavior.updateExtents(
      contentExtent: double.INFINITY
    );
672 673 674 675 676 677
  }

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

  void _handleSizeChanged(Size newSize) {
Hixie's avatar
Hixie committed
678 679 680 681 682 683
    setState(() {
      scrollBy(scrollBehavior.updateExtents(
        containerExtent: newSize.height,
        scrollOffset: scrollOffset
      ));
    });
684 685
  }

686 687 688 689 690 691 692 693 694 695 696 697 698
  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;
    }
  }

699
  void _handleExtentChanged(double newExtent) {
Hixie's avatar
Hixie committed
700 701 702
    double newScrollOffset;
    setState(() {
      newScrollOffset = scrollBehavior.updateExtents(
703
        contentExtent: newExtent ?? double.INFINITY,
Hixie's avatar
Hixie committed
704 705 706
        scrollOffset: scrollOffset
      );
    });
707 708
    if (_contentChanged) {
      _contentChanged = false;
709
      scrollTo(newScrollOffset);
710 711 712
    }
  }

713
  Widget buildContent(BuildContext context) {
714
    return new SizeObserver(
715
      onSizeChanged: _handleSizeChanged,
716
      child: new MixedViewport(
717
        startOffset: scrollOffset,
718 719 720
        builder: config.builder,
        token: config.token,
        onInvalidatorAvailable: config.onInvalidatorAvailable,
721
        onExtentChanged: _handleExtentChanged
722 723 724 725
      )
    );
  }
}