pageable_list.dart 13.6 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';
Hans Muller's avatar
Hans Muller committed
12
import 'scrollable.dart';
13
import 'virtual_viewport.dart';
Hans Muller's avatar
Hans Muller committed
14

15 16 17 18
/// Controls how a pageable list should behave during a fling.
enum PageableListFlingBehavior {
  canFlingAcrossMultiplePages,
  stopAtNextPage
19
}
Hans Muller's avatar
Hans Muller committed
20

21 22
abstract class PageableListBase extends Scrollable {
  PageableListBase({
Hans Muller's avatar
Hans Muller committed
23
    Key key,
24
    double initialScrollOffset,
25
    Axis scrollDirection: Axis.vertical,
26
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
Hans Muller's avatar
Hans Muller committed
27 28 29 30 31
    ScrollListener onScrollStart,
    ScrollListener onScroll,
    ScrollListener onScrollEnd,
    SnapOffsetCallback snapOffsetCallback,
    this.itemsWrap: false,
32
    this.itemsSnapAlignment: PageableListFlingBehavior.stopAtNextPage,
Hans Muller's avatar
Hans Muller committed
33 34
    this.onPageChanged,
    this.duration: const Duration(milliseconds: 200),
35
    this.curve: Curves.ease
Hans Muller's avatar
Hans Muller committed
36 37 38 39
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
40
    scrollAnchor: scrollAnchor,
Hans Muller's avatar
Hans Muller committed
41 42 43
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd,
44
    snapOffsetCallback: snapOffsetCallback
45 46 47
  ) {
    assert(itemsSnapAlignment != null);
  }
Hans Muller's avatar
Hans Muller committed
48

Adam Barth's avatar
Adam Barth committed
49
  /// Whether the first item should be revealed after scrolling past the last item.
Hans Muller's avatar
Hans Muller committed
50
  final bool itemsWrap;
Adam Barth's avatar
Adam Barth committed
51 52

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

  /// Called when the currently visible page changes.
56
  final ValueChanged<int> onPageChanged;
Adam Barth's avatar
Adam Barth committed
57 58

  /// The duration used when animating to a given page.
Hans Muller's avatar
Hans Muller committed
59
  final Duration duration;
Adam Barth's avatar
Adam Barth committed
60 61

  /// The animation curve to use when animating to a given page.
Hans Muller's avatar
Hans Muller committed
62
  final Curve curve;
Adam Barth's avatar
Adam Barth committed
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
  int get _itemCount;
}

/// Scrollable widget that scrolls one "page" at a time.
///
/// In a pageable list, one child is visible at a time. Scrolling the list
/// reveals either the next or previous child.
class PageableList extends PageableListBase {
  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
103
  /// The list of pages themselves.
104
  final Iterable<Widget> children;
Hans Muller's avatar
Hans Muller committed
105

106 107 108
  @override
  int get _itemCount => children?.length ?? 0;

109
  @override
110
  PageableListState<PageableList> createState() => new PageableListState<PageableList>();
Hans Muller's avatar
Hans Muller committed
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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
class PageableLazyList extends PageableListBase {
  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();
}

abstract class _PageableListStateBase<T extends PageableListBase> extends ScrollableState<T> {
  int get _itemCount => config._itemCount;
Hans Muller's avatar
Hans Muller committed
160 161
  int _previousItemCount;

162
  double get _pixelsPerScrollUnit {
Hans Muller's avatar
Hans Muller committed
163 164 165
    final RenderBox box = context.findRenderObject();
    if (box == null || !box.hasSize)
      return 0.0;
166 167 168 169 170 171 172 173
    switch (config.scrollDirection) {
      case Axis.horizontal:
        return box.size.width;
      case Axis.vertical:
        return box.size.height;
    }
  }

174
  @override
175 176 177 178 179
  double pixelOffsetToScrollOffset(double pixelOffset) {
    final double pixelsPerScrollUnit = _pixelsPerScrollUnit;
    return super.pixelOffsetToScrollOffset(pixelsPerScrollUnit == 0.0 ? 0.0 : pixelOffset / pixelsPerScrollUnit);
  }

180
  @override
181 182
  double scrollOffsetToPixelOffset(double scrollOffset) {
    return super.scrollOffsetToPixelOffset(scrollOffset * _pixelsPerScrollUnit);
Hans Muller's avatar
Hans Muller committed
183 184
  }

185 186 187 188 189 190 191 192 193 194 195 196 197
  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;
    }
  }

198
  @override
199 200 201 202 203
  void initState() {
    super.initState();
    _updateScrollBehavior();
  }

204
  @override
205
  void didUpdateConfig(PageableListBase oldConfig) {
Hans Muller's avatar
Hans Muller committed
206 207 208 209 210 211 212
    super.didUpdateConfig(oldConfig);

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

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

213 214
    if (_itemCount != _previousItemCount) {
      _previousItemCount = _itemCount;
Hans Muller's avatar
Hans Muller committed
215 216 217 218 219 220 221 222
      scrollBehaviorUpdateNeeded = true;
    }

    if (scrollBehaviorUpdateNeeded)
      _updateScrollBehavior();
  }

  void _updateScrollBehavior() {
223
    didUpdateScrollBehavior(scrollBehavior.updateExtents(
224
      contentExtent: _itemCount.toDouble(),
Hans Muller's avatar
Hans Muller committed
225 226 227 228 229
      containerExtent: 1.0,
      scrollOffset: scrollOffset
    ));
  }

Hans Muller's avatar
Hans Muller committed
230 231 232
  UnboundedBehavior _unboundedBehavior;
  OverscrollBehavior _overscrollBehavior;

233
  @override
Hans Muller's avatar
Hans Muller committed
234 235 236 237 238 239 240
  ExtentScrollBehavior get scrollBehavior {
    if (config.itemsWrap) {
      _unboundedBehavior ??= new UnboundedBehavior();
      return _unboundedBehavior;
    }
    _overscrollBehavior ??= new OverscrollBehavior();
    return _overscrollBehavior;
Hans Muller's avatar
Hans Muller committed
241 242
  }

243
  @override
244
  ExtentScrollBehavior createScrollBehavior() => scrollBehavior;
Hans Muller's avatar
Hans Muller committed
245

246
  @override
247
  bool get shouldSnapScrollOffset => config.itemsSnapAlignment == PageableListFlingBehavior.canFlingAcrossMultiplePages;
Hans Muller's avatar
Hans Muller committed
248

249
  @override
Hans Muller's avatar
Hans Muller committed
250
  double snapScrollOffset(double newScrollOffset) {
251 252
    final double previousItemOffset = newScrollOffset.floorToDouble();
    final double nextItemOffset = newScrollOffset.ceilToDouble();
Hans Muller's avatar
Hans Muller committed
253 254 255 256
    return (newScrollOffset - previousItemOffset < 0.5 ? previousItemOffset : nextItemOffset)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

257
  Future<Null> _flingToAdjacentItem(double scrollVelocity) {
258
    final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
Hans Muller's avatar
Hans Muller committed
259 260 261 262 263
      .clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
    return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

264
  @override
265
  Future<Null> fling(double scrollVelocity) {
Hans Muller's avatar
Hans Muller committed
266
    switch(config.itemsSnapAlignment) {
267
      case PageableListFlingBehavior.canFlingAcrossMultiplePages:
268
        return super.fling(scrollVelocity).then(_notifyPageChanged);
269 270
      case PageableListFlingBehavior.stopAtNextPage:
        return _flingToAdjacentItem(scrollVelocity);
Hans Muller's avatar
Hans Muller committed
271 272 273
    }
  }

274
  @override
275
  Future<Null> settleScrollOffset() {
Hans Muller's avatar
Hans Muller committed
276 277 278 279 280 281
    return scrollTo(snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

  void _notifyPageChanged(_) {
    if (config.onPageChanged != null)
282
      config.onPageChanged(_scrollOffsetToPageIndex(scrollOffset));
Hans Muller's avatar
Hans Muller committed
283 284
  }
}
285

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
/// State for a [PageableList] widget.
///
/// Widgets that subclass [PageableList] can subclass this class to have
/// sensible default behaviors for pageable lists.
class PageableListState<T extends PageableList> extends _PageableListStateBase<T> {
  @override
  Widget buildContent(BuildContext context) {
    return new PageViewport(
      itemsWrap: config.itemsWrap,
      mainAxis: config.scrollDirection,
      anchor: config.scrollAnchor,
      startOffset: scrollOffset,
      children: config.children
    );
  }
}

class _PageableLazyListState extends _PageableListStateBase<PageableLazyList> {
  @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,
321
    this.itemsWrap
322
  ) {
323
    assert(mainAxis != null);
324 325
  }

326
  @override
327
  final double startOffset;
328

329
  final Axis mainAxis;
330
  final ViewportAnchor anchor;
331
  final bool itemsWrap;
332 333

  @override
334
  RenderList createRenderObject(BuildContext context) => new RenderList();
335

336
  @override
337
  _VirtualPageViewportElement createElement() => new _VirtualPageViewportElement(this);
338 339
}

340 341
class _VirtualPageViewportElement extends VirtualViewportElement {
  _VirtualPageViewportElement(_VirtualPageViewport widget) : super(widget);
342

343
  @override
344
  _VirtualPageViewport get widget => super.widget;
345

346
  @override
347 348
  RenderList get renderObject => super.renderObject;

349
  @override
350 351 352
  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

353
  @override
354 355 356
  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

357
  @override
358 359
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
360

361
  @override
362 363
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
364

365
  @override
366
  double scrollOffsetToPixelOffset(double scrollOffset) {
367 368
    if (_containerExtent == null)
      return 0.0;
369
    return super.scrollOffsetToPixelOffset(scrollOffset) * _containerExtent;
370 371
  }

372
  @override
373
  void updateRenderObject(_VirtualPageViewport oldWidget) {
374
    renderObject
375
      ..mainAxis = widget.mainAxis;
376
    super.updateRenderObject(oldWidget);
377 378 379 380
  }

  double _containerExtent;

381 382 383 384
  void _updateViewportDimensions() {
    final Size containerSize = renderObject.size;

    Size materializedContentSize;
385
    switch (widget.mainAxis) {
386
      case Axis.vertical:
387 388
        materializedContentSize = new Size(containerSize.width, _materializedChildCount * containerSize.height);
        break;
389
      case Axis.horizontal:
390 391
        materializedContentSize = new Size(_materializedChildCount * containerSize.width, containerSize.height);
        break;
392
    }
393
    renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
394 395
  }

396
  @override
397
  void layout(BoxConstraints constraints) {
398
    final int length = renderObject.virtualChildCount;
399

400
    switch (widget.mainAxis) {
401 402 403 404 405 406
      case Axis.vertical:
        _containerExtent = renderObject.size.height;
        break;
      case Axis.horizontal:
        _containerExtent =  renderObject.size.width;
        break;
407 408
    }

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
    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();
427
      if (widget.anchor == ViewportAnchor.end)
428 429
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
    }
430

431
    _updateViewportDimensions();
432 433 434
    super.layout(constraints);
  }
}
435 436 437 438 439 440 441 442 443 444 445 446

class PageViewport extends _VirtualPageViewport with VirtualViewportFromIterable {
  PageViewport({
    double startOffset: 0.0,
    Axis mainAxis: Axis.vertical,
    ViewportAnchor anchor: ViewportAnchor.start,
    bool itemsWrap: false,
    this.children
  }) : super(
    startOffset,
    mainAxis,
    anchor,
447
    itemsWrap
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
  );

  @override
  final Iterable<Widget> children;
}

class LazyPageViewport extends _VirtualPageViewport with VirtualViewportFromBuilder {
  LazyPageViewport({
    double startOffset: 0.0,
    Axis mainAxis: Axis.vertical,
    ViewportAnchor anchor: ViewportAnchor.start,
    this.itemCount,
    this.itemBuilder
  }) : super(
    startOffset,
    mainAxis,
    anchor,
465
    false // Don't support wrapping yet.
466 467 468 469 470 471 472 473
  );

  @override
  final int itemCount;

  @override
  final ItemListBuilder itemBuilder;
}