scrollable_list.dart 21.1 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
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 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/rendering.dart';

Adam Barth's avatar
Adam Barth committed
10
import 'framework.dart';
11
import 'scroll_configuration.dart';
Adam Barth's avatar
Adam Barth committed
12 13 14
import 'scrollable.dart';
import 'virtual_viewport.dart';

15 16 17 18 19 20
/// A scrollable list of children that have equal size.
///
/// [ScrollableList] differs from [ScrollableLazyList] in that [ScrollableList]
/// uses an [Iterable] list of children. That makes [ScrollableList] suitable
/// for a large (but not extremely large or infinite) list of children.
///
21 22 23 24
/// [ScrollableList] differs from [Block] and [LazyBlock] in that
/// [ScrollableList] requires each of its children to be the same size. That
/// makes [ScrollableList] more efficient but less flexible than [Block] and
/// [LazyBlock].
25 26 27 28 29
///
/// Prefer [ScrollableViewport] when there is only one child.
///
/// See also:
///
30 31 32 33
///  * [Block], which allows its children to have arbitrary sizes.
///  * [ScrollableLazyList], a more efficient version of [ScrollableList].
///  * [LazyBlock], a more efficient version of [Block].
///  * [ScrollableViewport], which only has one child.
34
class ScrollableList extends StatelessWidget {
35 36 37 38
  /// Creats a scrollable list of children that have equal size.
  ///
  /// The [scrollDirection], [scrollAnchor], and [itemExtent] arguments must not
  /// be null.
39
  ScrollableList({
Adam Barth's avatar
Adam Barth committed
40
    Key key,
41 42 43 44 45 46 47 48
    this.initialScrollOffset,
    this.scrollDirection: Axis.vertical,
    this.scrollAnchor: ViewportAnchor.start,
    this.onScrollStart,
    this.onScroll,
    this.onScrollEnd,
    this.snapOffsetCallback,
    this.scrollableKey,
49
    @required this.itemExtent,
50 51
    this.itemsWrap: false,
    this.padding,
52
    this.children: const <Widget>[],
53
  }) : super(key: key) {
54 55
    assert(scrollDirection != null);
    assert(scrollAnchor != null);
56 57
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
58

Hans Muller's avatar
Hans Muller committed
59 60 61 62
  // Warning: keep the dartdoc comments that follow in sync with the copies in
  // Scrollable, LazyBlock, ScrollableLazyList, ScrollableViewport, and
  // ScrollableGrid. And see: https://github.com/dart-lang/dartdoc/issues/1161.

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  /// The scroll offset this widget should use when first created.
  final double initialScrollOffset;

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

  /// Whether to place first child at the start of the container or
  /// the last child at the end of the container, when the scrollable
  /// has not been scrolled and has no initial scroll offset.
  ///
  /// For example, if the [scrollDirection] 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 scrollable 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
  /// scrollable, 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.
  final ViewportAnchor scrollAnchor;

  /// Called whenever this widget starts to scroll.
  final ScrollListener onScrollStart;

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

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

  /// 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.
  final SnapOffsetCallback snapOffsetCallback;

  /// The key for the Scrollable created by this widget.
  final Key scrollableKey;

  /// The height of each item if [scrollDirection] is Axis.vertical, otherwise the width of each item.
Adam Barth's avatar
Adam Barth committed
119
  final double itemExtent;
120

121
  /// Whether the first item should be revealed after scrolling past the last item.
122
  final bool itemsWrap;
123 124

  /// The amount of space by which to inset the children inside the viewport.
125
  final EdgeInsets padding;
126

127
  /// The children, some of which might be materialized.
128
  final Iterable<Widget> children;
Adam Barth's avatar
Adam Barth committed
129

130
  Widget _buildViewport(BuildContext context, ScrollableState state) {
131
    return new ListViewport(
132
      onExtentsChanged: (double contentExtent, double containerExtent) {
133
        state.handleExtentsChanged(itemsWrap ? double.INFINITY : contentExtent, containerExtent);
134
      },
135
      scrollOffset: state.scrollOffset,
136 137 138 139 140 141
      mainAxis: scrollDirection,
      anchor: scrollAnchor,
      itemExtent: itemExtent,
      itemsWrap: itemsWrap,
      padding: padding,
      children: children
Adam Barth's avatar
Adam Barth committed
142
    );
143 144
  }

145 146
  @override
  Widget build(BuildContext context) {
147
    final Widget result = new Scrollable(
148 149 150 151 152 153 154 155
      key: scrollableKey,
      initialScrollOffset: initialScrollOffset,
      scrollDirection: scrollDirection,
      scrollAnchor: scrollAnchor,
      onScrollStart: onScrollStart,
      onScroll: onScroll,
      onScrollEnd: onScrollEnd,
      snapOffsetCallback: snapOffsetCallback,
156
      builder: _buildViewport
157
    );
158
    return ScrollConfiguration.wrap(context, result);
159
  }
Adam Barth's avatar
Adam Barth committed
160 161
}

162 163
class _VirtualListViewport extends VirtualViewport {
  _VirtualListViewport(
Adam Barth's avatar
Adam Barth committed
164
    this.onExtentsChanged,
165
    this.scrollOffset,
166
    this.mainAxis,
167
    this.anchor,
168
    this.itemExtent,
169
    this.itemsWrap,
170
    this.padding
171
  ) {
172
    assert(mainAxis != null);
173
    assert(anchor != null);
174 175
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
176

177
  /// Called when the interior or exterior dimensions of the viewport change.
178
  final ExtentsChangedCallback onExtentsChanged;
179 180

  /// The [startOffset] without taking the [padding] into account.
181
  final double scrollOffset;
182 183 184 185 186 187

  /// 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).
188
  final Axis mainAxis;
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208

  /// 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.
209
  final ViewportAnchor anchor;
210 211

  /// The height of each item if [scrollDirection] is Axis.vertical, otherwise the width of each item.
Adam Barth's avatar
Adam Barth committed
212
  final double itemExtent;
213 214

  /// Whether the first item should be revealed after scrolling past the last item.
215
  final bool itemsWrap;
216 217

  /// The amount of space by which to inset the children inside the viewport.
218
  final EdgeInsets padding;
Adam Barth's avatar
Adam Barth committed
219

220
  double get _leadingPadding {
221
    switch (mainAxis) {
222
      case Axis.vertical:
223
        switch (anchor) {
224 225 226 227 228 229 230
          case ViewportAnchor.start:
            return padding.top;
          case ViewportAnchor.end:
            return padding.bottom;
        }
        break;
      case Axis.horizontal:
231
        switch (anchor) {
232 233 234 235 236 237 238
          case ViewportAnchor.start:
            return padding.left;
          case ViewportAnchor.end:
            return padding.right;
        }
        break;
    }
pq's avatar
pq committed
239
    assert(mainAxis != null);
pq's avatar
pq committed
240
    return null;
241 242
  }

243
  @override
244 245 246 247 248 249
  double get startOffset {
    if (padding == null)
      return scrollOffset;
    return scrollOffset - _leadingPadding;
  }

250
  @override
251
  RenderList createRenderObject(BuildContext context) => new RenderList(itemExtent: itemExtent);
Adam Barth's avatar
Adam Barth committed
252

253
  @override
254
  _VirtualListViewportElement createElement() => new _VirtualListViewportElement(this);
Adam Barth's avatar
Adam Barth committed
255 256
}

257
class _VirtualListViewportElement extends VirtualViewportElement {
258
  _VirtualListViewportElement(VirtualViewport widget) : super(widget);
Adam Barth's avatar
Adam Barth committed
259

260
  @override
261 262
  _VirtualListViewport get widget => super.widget;

263
  @override
Adam Barth's avatar
Adam Barth committed
264 265
  RenderList get renderObject => super.renderObject;

266
  @override
Adam Barth's avatar
Adam Barth committed
267 268 269
  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

270
  @override
Adam Barth's avatar
Adam Barth committed
271 272 273
  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

274
  @override
275 276
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
Adam Barth's avatar
Adam Barth committed
277

278
  @override
279 280
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
Adam Barth's avatar
Adam Barth committed
281

282
  @override
283 284
  void updateRenderObject(_VirtualListViewport oldWidget) {
    renderObject
285
      ..mainAxis = widget.mainAxis
286
      ..anchor = widget.anchor
287
      ..itemExtent = widget.itemExtent
288
      ..padding = widget.padding;
289
    super.updateRenderObject(oldWidget);
Adam Barth's avatar
Adam Barth committed
290 291
  }

292 293
  double _lastReportedContentExtent;
  double _lastReportedContainerExtent;
Adam Barth's avatar
Adam Barth committed
294

295
  @override
Adam Barth's avatar
Adam Barth committed
296
  void layout(BoxConstraints constraints) {
297 298
    final int length = renderObject.virtualChildCount;
    final double itemExtent = widget.itemExtent;
299
    final EdgeInsets padding = widget.padding ?? EdgeInsets.zero;
300
    final Size containerSize = renderObject.size;
301

302 303
    double containerExtent;
    double contentExtent;
Adam Barth's avatar
Adam Barth committed
304

305
    switch (widget.mainAxis) {
306 307 308 309 310 311 312 313 314
      case Axis.vertical:
        containerExtent = containerSize.height;
        contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.vertical;
        break;
      case Axis.horizontal:
        containerExtent = renderObject.size.width;
        contentExtent = length == null ? double.INFINITY : widget.itemExtent * length + padding.horizontal;
        break;
    }
Adam Barth's avatar
Adam Barth committed
315

316 317 318 319 320 321
    if (length == 0) {
      _materializedChildBase = 0;
      _materializedChildCount = 0;
      _startOffsetBase = 0.0;
      _startOffsetLimit = double.INFINITY;
    } else {
322 323 324
      final double startOffset = widget.startOffset;
      int startItem = math.max(0, startOffset ~/ itemExtent);
      int limitItem = math.max(0, ((startOffset + containerExtent) / itemExtent).ceil());
325 326 327 328 329 330 331 332 333 334 335

      if (!widget.itemsWrap && length != null) {
        startItem = math.min(length, startItem);
        limitItem = math.min(length, limitItem);
      }

      _materializedChildBase = startItem;
      _materializedChildCount = limitItem - startItem;
      _startOffsetBase = startItem * itemExtent;
      _startOffsetLimit = limitItem * itemExtent - containerExtent;

336
      if (widget.anchor == ViewportAnchor.end)
337
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
338 339
    }

340
    Size materializedContentSize;
341
    switch (widget.mainAxis) {
342 343 344 345 346 347 348 349
      case Axis.vertical:
        materializedContentSize = new Size(containerSize.width, _materializedChildCount * itemExtent);
        break;
      case Axis.horizontal:
        materializedContentSize = new Size(_materializedChildCount * itemExtent, containerSize.height);
        break;
    }
    renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
Adam Barth's avatar
Adam Barth committed
350 351 352

    super.layout(constraints);

353 354 355 356
    if (contentExtent != _lastReportedContentExtent || containerExtent != _lastReportedContainerExtent) {
      _lastReportedContentExtent = contentExtent;
      _lastReportedContainerExtent = containerExtent;
      widget.onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent);
Adam Barth's avatar
Adam Barth committed
357 358 359
    }
  }
}
360

361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
/// A virtual viewport onto a list of equally sized children.
///
/// [ListViewport] differs from [LazyListViewport] in that [ListViewport]
/// uses an [Iterable] list of children. That makes [ListViewport] suitable
/// for a large (but not extremely large or infinite) list of children.
///
/// [ListViewport] differs from [LazyBlockViewport] in that [ListViewport]
/// requires each of its children to be the same size. That makes [ListViewport]
/// more efficient but less flexible than [LazyBlockViewport].
///
/// Prefer [Viewport] when there is only one child.
///
/// Used by [ScrollableList].
///
/// See also:
///
377 378 379
///  * [LazyListViewport].
///  * [LazyBlockViewport].
///  * [GridViewport].
380
class ListViewport extends _VirtualListViewport with VirtualViewportFromIterable {
381 382 383
  /// Creates a virtual viewport onto a list of equally sized children.
  ///
  /// The [mainAxis], [anchor], and [itemExtent] arguments must not be null.
384 385
  ListViewport({
    ExtentsChangedCallback onExtentsChanged,
386
    double scrollOffset: 0.0,
387
    Axis mainAxis: Axis.vertical,
388
    ViewportAnchor anchor: ViewportAnchor.start,
389
    @required double itemExtent,
390
    bool itemsWrap: false,
391
    EdgeInsets padding,
392
    this.children: const <Widget>[],
393 394
  }) : super(
    onExtentsChanged,
395
    scrollOffset,
396
    mainAxis,
397
    anchor,
398 399
    itemExtent,
    itemsWrap,
400
    padding
401 402
  );

403
  @override
404 405 406
  final Iterable<Widget> children;
}

407 408 409 410 411 412 413 414 415 416 417 418 419
/// An infinite scrollable list of children that have equal size.
///
/// [ScrollableLazyList] differs from [ScrollableList] in that
/// [ScrollableLazyList] uses an [ItemListBuilder] to lazily create children.
/// That makes [ScrollableLazyList] suitable for an extremely large or infinite
/// list of children but also makes it more verbose than [ScrollableList].
///
/// [ScrollableLazyList] differs from [LazyBlock] in that [ScrollableLazyList]
/// requires each of its children to be the same size. That makes
/// [ScrollableLazyList] more efficient but less flexible than [LazyBlock].
///
/// See also:
///
420 421
///  * [ScrollableList].
///  * [LazyBlock].
422
class ScrollableLazyList extends StatelessWidget {
423 424 425 426 427
  /// Creates an infinite scrollable list of children that have equal size.
  ///
  /// The [scrollDirection], [scrollAnchor], [itemExtent], and [itemBuilder]
  /// arguments must not be null. The [itemCount] argument must not be null
  /// unless the [scrollAnchor] argument is [ViewportAnchor.start].
428 429
  ScrollableLazyList({
    Key key,
430 431 432 433 434 435 436 437
    this.initialScrollOffset,
    this.scrollDirection: Axis.vertical,
    this.scrollAnchor: ViewportAnchor.start,
    this.onScrollStart,
    this.onScroll,
    this.onScrollEnd,
    this.snapOffsetCallback,
    this.scrollableKey,
438
    @required this.itemExtent,
439
    this.itemCount,
440
    @required this.itemBuilder,
441
    this.padding
442
  }) : super(key: key) {
443 444
    assert(itemExtent != null);
    assert(itemBuilder != null);
445
    assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
446 447
  }

Hans Muller's avatar
Hans Muller committed
448 449 450 451
  // Warning: keep the dartdoc comments that follow in sync with the copies in
  // Scrollable, LazyBlock, ScrollableViewport, ScrollableList, and
  // ScrollableGrid. And see: https://github.com/dart-lang/dartdoc/issues/1161.

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
  /// The scroll offset this widget should use when first created.
  final double initialScrollOffset;

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

  /// Whether to place first child at the start of the container or
  /// the last child at the end of the container, when the scrollable
  /// has not been scrolled and has no initial scroll offset.
  ///
  /// For example, if the [scrollDirection] 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 scrollable 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
  /// scrollable, 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.
  final ViewportAnchor scrollAnchor;

  /// Called whenever this widget starts to scroll.
  final ScrollListener onScrollStart;

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

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

  /// 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.
  final SnapOffsetCallback snapOffsetCallback;

  /// The key for the Scrollable created by this widget.
  final Key scrollableKey;

  /// The height of each item if [scrollDirection] is Axis.vertical, otherwise the width of each item.
507
  final double itemExtent;
508 509

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

512 513 514 515 516 517 518 519 520 521 522 523
  /// Returns a widget representing the item with the given index.
  ///
  /// This function might be called with index parameters in any order. This
  /// function should return null for indices that exceed the number of children
  /// (i.e., [itemCount] if non-null). If this function must not return a null
  /// value for an index if it previously returned a non-null value for that
  /// index or a larger index.
  ///
  /// This function might be called during the build or layout phases of the
  /// pipeline.
  ///
  /// The returned widget might or might not be cached by [ScrollableLazyList].
524
  final ItemListBuilder itemBuilder;
525

526
  /// The amount of space by which to inset the children inside the viewport.
527
  final EdgeInsets padding;
528

529
  Widget _buildViewport(BuildContext context, ScrollableState state) {
530
    return new LazyListViewport(
531
      onExtentsChanged: state.handleExtentsChanged,
532
      scrollOffset: state.scrollOffset,
533 534 535 536 537 538 539 540 541 542 543
      mainAxis: scrollDirection,
      anchor: scrollAnchor,
      itemExtent: itemExtent,
      itemCount: itemCount,
      itemBuilder: itemBuilder,
      padding: padding
    );
  }

  @override
  Widget build(BuildContext context) {
544
    final Widget result = new Scrollable(
545 546 547 548 549 550 551 552
      key: scrollableKey,
      initialScrollOffset: initialScrollOffset,
      scrollDirection: scrollDirection,
      scrollAnchor: scrollAnchor,
      onScrollStart: onScrollStart,
      onScroll: onScroll,
      onScrollEnd: onScrollEnd,
      snapOffsetCallback: snapOffsetCallback,
553
      builder: _buildViewport
554
    );
555
    return ScrollConfiguration.wrap(context, result);
556 557 558
  }
}

559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
/// A virtual viewport onto an extremely large or infinite list of equally sized children.
///
/// [LazyListViewport] differs from [ListViewport] in that [LazyListViewport]
/// uses an [ItemListBuilder] to lazily create children. That makes
/// [LazyListViewport] suitable for an extremely large or infinite list of
/// children but also makes it more verbose than [ListViewport].
///
/// [LazyListViewport] differs from [LazyBlockViewport] in that
/// [LazyListViewport] requires each of its children to be the same size. That
/// makes [LazyListViewport] more efficient but less flexible than
/// [LazyBlockViewport].
///
/// Used by [ScrollableLazyList].
///
/// See also:
///
575 576
///  * [ListViewport].
///  * [LazyBlockViewport].
577
class LazyListViewport extends _VirtualListViewport with VirtualViewportFromBuilder {
578 579 580 581
  /// Creates a virtual viewport onto an extremely large or infinite list of equally sized children.
  ///
  /// The [mainAxis], [anchor], [itemExtent], and [itemBuilder] arguments must
  /// not be null.
582 583
  LazyListViewport({
    ExtentsChangedCallback onExtentsChanged,
584
    double scrollOffset: 0.0,
585
    Axis mainAxis: Axis.vertical,
586
    ViewportAnchor anchor: ViewportAnchor.start,
587
    @required double itemExtent,
588
    EdgeInsets padding,
589
    this.itemCount,
590
    @required this.itemBuilder
591 592
  }) : super(
    onExtentsChanged,
593
    scrollOffset,
594
    mainAxis,
595
    anchor,
596 597
    itemExtent,
    false, // Don't support wrapping yet.
598
    padding
599 600 601
  ) {
    assert(itemBuilder != null);
  }
602

603
  @override
604
  final int itemCount;
605 606

  @override
607 608
  final ItemListBuilder itemBuilder;
}