pageable_list.dart 19.1 KB
Newer Older
Hans Muller's avatar
Hans Muller committed
1 2 3 4 5 6
// 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';

7
import 'package:flutter/rendering.dart' show RenderList, ViewportDimensions;
Hans Muller's avatar
Hans Muller committed
8 9 10

import 'basic.dart';
import 'framework.dart';
11
import 'scroll_behavior.dart';
12
import 'scroll_configuration.dart';
Hans Muller's avatar
Hans Muller committed
13
import 'scrollable.dart';
14
import 'virtual_viewport.dart';
Hans Muller's avatar
Hans Muller committed
15

16 17
/// Controls how a pageable list should behave during a fling.
enum PageableListFlingBehavior {
18
  /// A fling gesture can scroll the list by more than one page.
19
  canFlingAcrossMultiplePages,
20 21

  /// A fling gesture can scroll the list by at most one page.
22
  stopAtNextPage
23
}
Hans Muller's avatar
Hans Muller committed
24

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/// A base class for widgets that display one page at a time.
///
/// [Pageable] widgets are similar to [Scrollable] except that they display a
/// single child at a time. When being scrolled, they can display adjacent
/// pages, but when the user stops scrolling, they settle their scroll offset to
/// a value that shows a single page.
///
/// [Pageable] uses different units for its scroll offset than [Scrollable]. One
/// unit of scroll offset cooresponds to one child widget, which means a scroll
/// offset of 2.75 indicates that the viewport is three quarters of the way
/// between the child with index 2 and the child with index 3.
///
/// Widgets that subclass [Pageable] typically use state objects that subclass
/// [PageableState].
///
/// See also:
///
///  * [PageableList], which pages through an iterable list of children.
///  * [PageableLazyList], which pages through a lazily constructed list of
///    children.
abstract class Pageable extends Scrollable {
  /// Initializes fields for subclasses.
  ///
  /// The [scrollDirection], [scrollAnchor], and [itemsSnapAlignment] arguments
  /// must not be null.
  Pageable({
Hans Muller's avatar
Hans Muller committed
51
    Key key,
52
    double initialScrollOffset,
53
    Axis scrollDirection: Axis.vertical,
54
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
Hans Muller's avatar
Hans Muller committed
55 56 57 58 59
    ScrollListener onScrollStart,
    ScrollListener onScroll,
    ScrollListener onScrollEnd,
    SnapOffsetCallback snapOffsetCallback,
    this.itemsWrap: false,
60
    this.itemsSnapAlignment: PageableListFlingBehavior.stopAtNextPage,
Hans Muller's avatar
Hans Muller committed
61 62
    this.onPageChanged,
    this.duration: const Duration(milliseconds: 200),
63
    this.curve: Curves.ease
Hans Muller's avatar
Hans Muller committed
64 65 66 67
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
68
    scrollAnchor: scrollAnchor,
Hans Muller's avatar
Hans Muller committed
69 70 71
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd,
72
    snapOffsetCallback: snapOffsetCallback
73 74 75
  ) {
    assert(itemsSnapAlignment != null);
  }
Hans Muller's avatar
Hans Muller committed
76

Adam Barth's avatar
Adam Barth committed
77
  /// Whether the first item should be revealed after scrolling past the last item.
Hans Muller's avatar
Hans Muller committed
78
  final bool itemsWrap;
Adam Barth's avatar
Adam Barth committed
79 80

  /// Controls whether a fling always reveals the adjacent item or whether flings can traverse many items.
81
  final PageableListFlingBehavior itemsSnapAlignment;
Adam Barth's avatar
Adam Barth committed
82 83

  /// Called when the currently visible page changes.
84
  final ValueChanged<int> onPageChanged;
Adam Barth's avatar
Adam Barth committed
85 86

  /// The duration used when animating to a given page.
Hans Muller's avatar
Hans Muller committed
87
  final Duration duration;
Adam Barth's avatar
Adam Barth committed
88 89

  /// The animation curve to use when animating to a given page.
Hans Muller's avatar
Hans Muller committed
90
  final Curve curve;
Adam Barth's avatar
Adam Barth committed
91

92 93 94
  int get _itemCount;
}

95
/// A widget that pages through an iterable list of children.
96
///
97 98 99 100 101 102 103 104 105 106 107 108 109
/// A [PageableList] displays a single child at a time. When being scrolled, it
/// can display adjacent pages, but when the user stops scrolling, it settles
/// its scroll offset to a value that shows a single page.
///
/// See also:
///
///  * [PageableLazyList], which pages through a lazily constructed list of
///    children.
class PageableList extends Pageable {
  /// Creates a widget that pages through an iterable list of children.
  ///
  /// The [scrollDirection], [scrollAnchor], and [itemsSnapAlignment] arguments
  /// must not be null.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
  PageableList({
    Key key,
    double initialScrollOffset,
    Axis scrollDirection: Axis.vertical,
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
    ScrollListener onScrollStart,
    ScrollListener onScroll,
    ScrollListener onScrollEnd,
    SnapOffsetCallback snapOffsetCallback,
    bool itemsWrap: false,
    PageableListFlingBehavior itemsSnapAlignment: PageableListFlingBehavior.stopAtNextPage,
    ValueChanged<int> onPageChanged,
    Duration duration: const Duration(milliseconds: 200),
    Curve curve: Curves.ease,
    this.children
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
    scrollAnchor: scrollAnchor,
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd,
    snapOffsetCallback: snapOffsetCallback,
    itemsWrap: itemsWrap,
    itemsSnapAlignment: itemsSnapAlignment,
    onPageChanged: onPageChanged,
    duration: duration,
    curve: curve
  );

Adam Barth's avatar
Adam Barth committed
141
  /// The list of pages themselves.
142
  final Iterable<Widget> children;
Hans Muller's avatar
Hans Muller committed
143

144 145 146
  @override
  int get _itemCount => children?.length ?? 0;

147
  @override
148
  PageableListState<PageableList> createState() => new PageableListState<PageableList>();
Hans Muller's avatar
Hans Muller committed
149 150
}

151 152 153 154 155 156 157 158 159 160 161 162 163 164
/// A widget that pages through a lazily constructed list of children.
///
/// A [PageableList] displays a single child at a time. When being scrolled, it
/// can display adjacent pages, but when the user stops scrolling, it settles
/// its scroll offset to a value that shows a single page.
///
/// See also:
///
///  * [PageableList], which pages through an iterable list of children.
class PageableLazyList extends Pageable {
  /// Creates a widget that pages through a lazily constructed list of children.
  ///
  /// The [scrollDirection], [scrollAnchor], and [itemsSnapAlignment] arguments
  /// must not be null.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  PageableLazyList({
    Key key,
    double initialScrollOffset,
    Axis scrollDirection: Axis.vertical,
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
    ScrollListener onScrollStart,
    ScrollListener onScroll,
    ScrollListener onScrollEnd,
    SnapOffsetCallback snapOffsetCallback,
    PageableListFlingBehavior itemsSnapAlignment: PageableListFlingBehavior.stopAtNextPage,
    ValueChanged<int> onPageChanged,
    Duration duration: const Duration(milliseconds: 200),
    Curve curve: Curves.ease,
    this.itemCount,
    this.itemBuilder
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
    scrollAnchor: scrollAnchor,
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd,
    snapOffsetCallback: snapOffsetCallback,
    itemsWrap: false,
    itemsSnapAlignment: itemsSnapAlignment,
    onPageChanged: onPageChanged,
    duration: duration,
    curve: curve
  );

  /// The total number of list items.
  final int itemCount;

  /// A function that returns the pages themselves.
  final ItemListBuilder itemBuilder;

  @override
  int get _itemCount => itemCount ?? 0;

  @override
  _PageableLazyListState createState() => new _PageableLazyListState();
}

209 210 211 212 213 214
/// State for widgets that subclass [Pageable].
///
/// Specializes [ScrollableState] to support page-based scrolling.
///
/// Subclasses typically override [buildContent] to build viewports.
abstract class PageableState<T extends Pageable> extends ScrollableState<T> {
215
  int get _itemCount => config._itemCount;
Hans Muller's avatar
Hans Muller committed
216 217
  int _previousItemCount;

218
  double get _pixelsPerScrollUnit {
Hans Muller's avatar
Hans Muller committed
219 220 221
    final RenderBox box = context.findRenderObject();
    if (box == null || !box.hasSize)
      return 0.0;
222 223 224 225 226 227
    switch (config.scrollDirection) {
      case Axis.horizontal:
        return box.size.width;
      case Axis.vertical:
        return box.size.height;
    }
pq's avatar
pq committed
228
    assert(config.scrollDirection != null);
pq's avatar
pq committed
229
    return null;
230 231
  }

232
  @override
233 234 235 236 237
  double pixelOffsetToScrollOffset(double pixelOffset) {
    final double pixelsPerScrollUnit = _pixelsPerScrollUnit;
    return super.pixelOffsetToScrollOffset(pixelsPerScrollUnit == 0.0 ? 0.0 : pixelOffset / pixelsPerScrollUnit);
  }

238
  @override
239 240
  double scrollOffsetToPixelOffset(double scrollOffset) {
    return super.scrollOffsetToPixelOffset(scrollOffset * _pixelsPerScrollUnit);
Hans Muller's avatar
Hans Muller committed
241 242
  }

243 244 245 246 247 248 249 250 251 252 253
  int _scrollOffsetToPageIndex(double scrollOffset) {
    int itemCount = _itemCount;
    if (itemCount == 0)
      return 0;
    int scrollIndex = scrollOffset.floor();
    switch (config.scrollAnchor) {
      case ViewportAnchor.start:
        return scrollIndex % itemCount;
      case ViewportAnchor.end:
        return (_itemCount - scrollIndex - 1) % itemCount;
    }
pq's avatar
pq committed
254 255
    assert(config.scrollAnchor != null);
    return null;
256 257
  }

258
  @override
259
  void didUpdateConfig(Pageable oldConfig) {
Hans Muller's avatar
Hans Muller committed
260 261 262 263 264 265 266
    super.didUpdateConfig(oldConfig);

    bool scrollBehaviorUpdateNeeded = config.scrollDirection != oldConfig.scrollDirection;

    if (config.itemsWrap != oldConfig.itemsWrap)
      scrollBehaviorUpdateNeeded = true;

267 268
    if (_itemCount != _previousItemCount) {
      _previousItemCount = _itemCount;
Hans Muller's avatar
Hans Muller committed
269 270 271 272 273 274 275 276
      scrollBehaviorUpdateNeeded = true;
    }

    if (scrollBehaviorUpdateNeeded)
      _updateScrollBehavior();
  }

  void _updateScrollBehavior() {
277
    didUpdateScrollBehavior(scrollBehavior.updateExtents(
278
      contentExtent: _itemCount.toDouble(),
Hans Muller's avatar
Hans Muller committed
279 280 281 282 283
      containerExtent: 1.0,
      scrollOffset: scrollOffset
    ));
  }

Hans Muller's avatar
Hans Muller committed
284 285 286
  UnboundedBehavior _unboundedBehavior;
  OverscrollBehavior _overscrollBehavior;

287
  @override
Hans Muller's avatar
Hans Muller committed
288 289
  ExtentScrollBehavior get scrollBehavior {
    if (config.itemsWrap) {
290
      _unboundedBehavior ??= new UnboundedBehavior(platform: platform);
Hans Muller's avatar
Hans Muller committed
291 292
      return _unboundedBehavior;
    }
293
    _overscrollBehavior ??= new OverscrollBehavior(platform: platform);
Hans Muller's avatar
Hans Muller committed
294
    return _overscrollBehavior;
Hans Muller's avatar
Hans Muller committed
295 296
  }

297 298 299 300 301
  /// Returns the style of scrolling to use.
  ///
  /// By default, defers to the nearest [ScrollConfiguration].
  TargetPlatform get platform => ScrollConfiguration.of(context)?.platform;

302
  @override
303
  ExtentScrollBehavior createScrollBehavior() => scrollBehavior;
Hans Muller's avatar
Hans Muller committed
304

305
  @override
306
  bool get shouldSnapScrollOffset => config.itemsSnapAlignment == PageableListFlingBehavior.canFlingAcrossMultiplePages;
Hans Muller's avatar
Hans Muller committed
307

308
  @override
Hans Muller's avatar
Hans Muller committed
309
  double snapScrollOffset(double newScrollOffset) {
310 311
    final double previousItemOffset = newScrollOffset.floorToDouble();
    final double nextItemOffset = newScrollOffset.ceilToDouble();
Hans Muller's avatar
Hans Muller committed
312 313 314 315
    return (newScrollOffset - previousItemOffset < 0.5 ? previousItemOffset : nextItemOffset)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

316
  Future<Null> _flingToAdjacentItem(double scrollVelocity) {
317
    final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
Hans Muller's avatar
Hans Muller committed
318 319 320 321 322
      .clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
    return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

323
  @override
324
  Future<Null> fling(double scrollVelocity) {
Hans Muller's avatar
Hans Muller committed
325
    switch(config.itemsSnapAlignment) {
326
      case PageableListFlingBehavior.canFlingAcrossMultiplePages:
327
        return super.fling(scrollVelocity).then(_notifyPageChanged);
328 329
      case PageableListFlingBehavior.stopAtNextPage:
        return _flingToAdjacentItem(scrollVelocity);
Hans Muller's avatar
Hans Muller committed
330
    }
pq's avatar
pq committed
331 332
    assert(config.itemsSnapAlignment != null);
    return null;
Hans Muller's avatar
Hans Muller committed
333 334
  }

335
  @override
336
  Future<Null> settleScrollOffset() {
Hans Muller's avatar
Hans Muller committed
337 338 339 340 341 342
    return scrollTo(snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

  void _notifyPageChanged(_) {
    if (config.onPageChanged != null)
343
      config.onPageChanged(_scrollOffsetToPageIndex(scrollOffset));
Hans Muller's avatar
Hans Muller committed
344 345
  }
}
346

347 348 349 350
/// State for a [PageableList] widget.
///
/// Widgets that subclass [PageableList] can subclass this class to have
/// sensible default behaviors for pageable lists.
351
class PageableListState<T extends PageableList> extends PageableState<T> {
352 353 354 355 356 357 358 359 360 361 362 363
  @override
  Widget buildContent(BuildContext context) {
    return new PageViewport(
      itemsWrap: config.itemsWrap,
      mainAxis: config.scrollDirection,
      anchor: config.scrollAnchor,
      startOffset: scrollOffset,
      children: config.children
    );
  }
}

364
class _PageableLazyListState extends PageableState<PageableLazyList> {
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
  @override
  Widget buildContent(BuildContext context) {
    return new LazyPageViewport(
      mainAxis: config.scrollDirection,
      anchor: config.scrollAnchor,
      startOffset: scrollOffset,
      itemCount: config.itemCount,
      itemBuilder: config.itemBuilder
    );
  }
}

class _VirtualPageViewport extends VirtualViewport {
  _VirtualPageViewport(
    this.startOffset,
    this.mainAxis,
    this.anchor,
382
    this.itemsWrap
383
  ) {
384
    assert(mainAxis != null);
385
    assert(anchor != null);
386 387
  }

388
  @override
389
  final double startOffset;
390

391 392 393 394 395
  /// The direction in which the children are permitted to be larger than the viewport.
  ///
  /// The children are given layout constraints that are fully unconstrained
  /// along the main axis (e.g., children can be as tall as they want if the
  /// main axis is vertical).
396
  final Axis mainAxis;
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

  /// Whether to place first child at the start of the container or the last
  /// child at the end of the container, when the viewport has not been offset.
  ///
  /// For example, if the [mainAxis] is [Axis.vertical] and
  /// there are enough items to overflow the container, then
  /// [ViewportAnchor.start] means that the top of the first item
  /// should be aligned with the top of the viewport with the last
  /// item below the bottom, and [ViewportAnchor.end] means the bottom
  /// of the last item should be aligned with the bottom of the
  /// viewport, with the first item above the top.
  ///
  /// This also affects whether, when an item is added or removed, the
  /// displacement will be towards the first item or the last item.
  /// Continuing the earlier example, if a new item is inserted in the
  /// middle of the list, in the [ViewportAnchor.start] case the items
  /// after it (with greater indices, down to the item with the
  /// highest index) will be pushed down, while in the
  /// [ViewportAnchor.end] case the items before it (with lower
  /// indices, up to the item with the index 0) will be pushed up.
417
  final ViewportAnchor anchor;
418 419

  /// Whether the first item should be revealed after scrolling past the last item.
420
  final bool itemsWrap;
421 422

  @override
423
  RenderList createRenderObject(BuildContext context) => new RenderList();
424

425
  @override
426
  _VirtualPageViewportElement createElement() => new _VirtualPageViewportElement(this);
427 428
}

429 430
class _VirtualPageViewportElement extends VirtualViewportElement {
  _VirtualPageViewportElement(_VirtualPageViewport widget) : super(widget);
431

432
  @override
433
  _VirtualPageViewport get widget => super.widget;
434

435
  @override
436 437
  RenderList get renderObject => super.renderObject;

438
  @override
439 440 441
  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

442
  @override
443 444 445
  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

446
  @override
447 448
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
449

450
  @override
451 452
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
453

454
  @override
455
  double scrollOffsetToPixelOffset(double scrollOffset) {
456 457
    if (_containerExtent == null)
      return 0.0;
458
    return super.scrollOffsetToPixelOffset(scrollOffset) * _containerExtent;
459 460
  }

461
  @override
462
  void updateRenderObject(_VirtualPageViewport oldWidget) {
463
    renderObject
464
      ..mainAxis = widget.mainAxis;
465
    super.updateRenderObject(oldWidget);
466 467 468 469
  }

  double _containerExtent;

470 471 472 473
  void _updateViewportDimensions() {
    final Size containerSize = renderObject.size;

    Size materializedContentSize;
474
    switch (widget.mainAxis) {
475
      case Axis.vertical:
476 477
        materializedContentSize = new Size(containerSize.width, _materializedChildCount * containerSize.height);
        break;
478
      case Axis.horizontal:
479 480
        materializedContentSize = new Size(_materializedChildCount * containerSize.width, containerSize.height);
        break;
481
    }
482
    renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
483 484
  }

485
  @override
486
  void layout(BoxConstraints constraints) {
487
    final int length = renderObject.virtualChildCount;
488

489
    switch (widget.mainAxis) {
490 491 492 493 494 495
      case Axis.vertical:
        _containerExtent = renderObject.size.height;
        break;
      case Axis.horizontal:
        _containerExtent =  renderObject.size.width;
        break;
496 497
    }

498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
    if (length == 0) {
      _materializedChildBase = 0;
      _materializedChildCount = 0;
      _startOffsetBase = 0.0;
      _startOffsetLimit = double.INFINITY;
    } else {
      int startItem = widget.startOffset.floor();
      int limitItem = (widget.startOffset + 1.0).ceil();

      if (!widget.itemsWrap) {
        startItem = startItem.clamp(0, length);
        limitItem = limitItem.clamp(0, length);
      }

      _materializedChildBase = startItem;
      _materializedChildCount = limitItem - startItem;
      _startOffsetBase = startItem.toDouble();
      _startOffsetLimit = (limitItem - 1).toDouble();
516
      if (widget.anchor == ViewportAnchor.end)
517 518
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
    }
519

520
    _updateViewportDimensions();
521 522 523
    super.layout(constraints);
  }
}
524

525 526 527 528 529 530 531 532 533 534 535
/// A virtual viewport that displays a single child at a time.
///
/// Useful for [Pageable] widgets.
///
/// One unit of start offset cooresponds to one child widget, which means a
/// start offset of 2.75 indicates that the viewport is three quarters of the
/// way between the child with index 2 and the child with index 3.
///
/// [PageViewport] differs from [LazyPageViewport] in that [PageViewport] uses
/// an [Iterable] list of children. That makes [PageViewport] suitable for a
/// large (but not extremely large or infinite) list of children.
536
class PageViewport extends _VirtualPageViewport with VirtualViewportFromIterable {
537 538 539
  /// Creates a virtual viewport that displays a single child at a time.
  ///
  /// The [mainAxis] and [anchor] arguments must not be null.
540 541 542 543 544 545 546 547 548 549
  PageViewport({
    double startOffset: 0.0,
    Axis mainAxis: Axis.vertical,
    ViewportAnchor anchor: ViewportAnchor.start,
    bool itemsWrap: false,
    this.children
  }) : super(
    startOffset,
    mainAxis,
    anchor,
550
    itemsWrap
551 552 553 554 555 556
  );

  @override
  final Iterable<Widget> children;
}

557 558 559 560 561 562 563 564 565 566 567
/// A virtual viewport that displays a single child at a time.
///
/// Useful for [Pageable] widgets.
///
/// One unit of start offset cooresponds to one child widget, which means a
/// start offset of 2.75 indicates that the viewport is three quarters of the
/// way between the child with index 2 and the child with index 3.
///
/// [LazyPageViewport] differs from [PageViewport] in that [LazyPageViewport]
/// uses an [ItemListBuilder] to lazily create children. That makes
/// [LazyPageViewport] suitable for an extremely large or infinite list of
568
/// children but also makes it more verbose than [PageViewport].
569
class LazyPageViewport extends _VirtualPageViewport with VirtualViewportFromBuilder {
570 571 572
  /// Creates a virtual viewport that displays a single child at a time.
  ///
  /// The [mainAxis] and [anchor] arguments must not be null.
573 574 575 576 577 578 579 580 581 582
  LazyPageViewport({
    double startOffset: 0.0,
    Axis mainAxis: Axis.vertical,
    ViewportAnchor anchor: ViewportAnchor.start,
    this.itemCount,
    this.itemBuilder
  }) : super(
    startOffset,
    mainAxis,
    anchor,
583
    false // Don't support wrapping yet.
584 585 586 587 588 589 590 591
  );

  @override
  final int itemCount;

  @override
  final ItemListBuilder itemBuilder;
}