pageable_list.dart 9.4 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 19
/// Controls what alignment items use when settling.
enum ItemsSnapAlignment {
  item,
  adjacentItem
}
Hans Muller's avatar
Hans Muller committed
20

21
class PageableList extends Scrollable {
Hans Muller's avatar
Hans Muller committed
22 23
  PageableList({
    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 32 33 34 35
    ScrollListener onScrollStart,
    ScrollListener onScroll,
    ScrollListener onScrollEnd,
    SnapOffsetCallback snapOffsetCallback,
    this.itemsWrap: false,
    this.itemsSnapAlignment: ItemsSnapAlignment.adjacentItem,
    this.onPageChanged,
    this.scrollableListPainter,
    this.duration: const Duration(milliseconds: 200),
36 37
    this.curve: Curves.ease,
    this.children
Hans Muller's avatar
Hans Muller committed
38 39 40 41
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
42
    scrollAnchor: scrollAnchor,
Hans Muller's avatar
Hans Muller committed
43 44 45
    onScrollStart: onScrollStart,
    onScroll: onScroll,
    onScrollEnd: onScrollEnd,
46
    snapOffsetCallback: snapOffsetCallback
Hans Muller's avatar
Hans Muller committed
47 48 49
  );

  final bool itemsWrap;
50
  final ItemsSnapAlignment itemsSnapAlignment;
51
  final ValueChanged<int> onPageChanged;
Hans Muller's avatar
Hans Muller committed
52 53 54
  final ScrollableListPainter scrollableListPainter;
  final Duration duration;
  final Curve curve;
55
  final Iterable<Widget> children;
Hans Muller's avatar
Hans Muller committed
56

57
  PageableListState createState() => new PageableListState();
Hans Muller's avatar
Hans Muller committed
58 59
}

60
class PageableListState<T extends PageableList> extends ScrollableState<T> {
61
  int get _itemCount => config.children?.length ?? 0;
Hans Muller's avatar
Hans Muller committed
62 63
  int _previousItemCount;

64
  double get _pixelsPerScrollUnit {
Hans Muller's avatar
Hans Muller committed
65 66 67
    final RenderBox box = context.findRenderObject();
    if (box == null || !box.hasSize)
      return 0.0;
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
    switch (config.scrollDirection) {
      case Axis.horizontal:
        return box.size.width;
      case Axis.vertical:
        return box.size.height;
    }
  }

  double pixelOffsetToScrollOffset(double pixelOffset) {
    final double pixelsPerScrollUnit = _pixelsPerScrollUnit;
    return super.pixelOffsetToScrollOffset(pixelsPerScrollUnit == 0.0 ? 0.0 : pixelOffset / pixelsPerScrollUnit);
  }

  double scrollOffsetToPixelOffset(double scrollOffset) {
    return super.scrollOffsetToPixelOffset(scrollOffset * _pixelsPerScrollUnit);
Hans Muller's avatar
Hans Muller committed
83 84
  }

85 86 87 88 89 90 91 92 93 94 95 96 97
  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;
    }
  }

98 99 100 101 102 103
  void initState() {
    super.initState();
    _updateScrollBehavior();
  }

  void didUpdateConfig(PageableList oldConfig) {
Hans Muller's avatar
Hans Muller committed
104 105 106 107 108 109 110
    super.didUpdateConfig(oldConfig);

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

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

111 112
    if (_itemCount != _previousItemCount) {
      _previousItemCount = _itemCount;
Hans Muller's avatar
Hans Muller committed
113 114 115 116 117 118 119 120
      scrollBehaviorUpdateNeeded = true;
    }

    if (scrollBehaviorUpdateNeeded)
      _updateScrollBehavior();
  }

  void _updateScrollBehavior() {
121
    config.scrollableListPainter?.contentExtent = _itemCount.toDouble();
Hans Muller's avatar
Hans Muller committed
122
    scrollTo(scrollBehavior.updateExtents(
123
      contentExtent: _itemCount.toDouble(),
Hans Muller's avatar
Hans Muller committed
124 125 126 127 128 129 130 131 132 133 134 135
      containerExtent: 1.0,
      scrollOffset: scrollOffset
    ));
  }

  void dispatchOnScrollStart() {
    super.dispatchOnScrollStart();
    config.scrollableListPainter?.scrollStarted();
  }

  void dispatchOnScroll() {
    super.dispatchOnScroll();
136
    config.scrollableListPainter?.scrollOffset = scrollOffset;
Hans Muller's avatar
Hans Muller committed
137 138 139 140 141 142 143 144
  }

  void dispatchOnScrollEnd() {
    super.dispatchOnScrollEnd();
    config.scrollableListPainter?.scrollEnded();
  }

  Widget buildContent(BuildContext context) {
145
    return new PageViewport(
Hans Muller's avatar
Hans Muller committed
146
      itemsWrap: config.itemsWrap,
147
      scrollDirection: config.scrollDirection,
148
      scrollAnchor: config.scrollAnchor,
Hans Muller's avatar
Hans Muller committed
149
      startOffset: scrollOffset,
150 151
      overlayPainter: config.scrollableListPainter,
      children: config.children
Hans Muller's avatar
Hans Muller committed
152 153 154
    );
  }

Hans Muller's avatar
Hans Muller committed
155 156 157 158 159 160 161 162 163 164
  UnboundedBehavior _unboundedBehavior;
  OverscrollBehavior _overscrollBehavior;

  ExtentScrollBehavior get scrollBehavior {
    if (config.itemsWrap) {
      _unboundedBehavior ??= new UnboundedBehavior();
      return _unboundedBehavior;
    }
    _overscrollBehavior ??= new OverscrollBehavior();
    return _overscrollBehavior;
Hans Muller's avatar
Hans Muller committed
165 166
  }

Hans Muller's avatar
Hans Muller committed
167
  ScrollBehavior createScrollBehavior() => scrollBehavior;
Hans Muller's avatar
Hans Muller committed
168

169
  bool get shouldSnapScrollOffset => config.itemsSnapAlignment == ItemsSnapAlignment.item;
Hans Muller's avatar
Hans Muller committed
170 171

  double snapScrollOffset(double newScrollOffset) {
172 173
    final double previousItemOffset = newScrollOffset.floorToDouble();
    final double nextItemOffset = newScrollOffset.ceilToDouble();
Hans Muller's avatar
Hans Muller committed
174 175 176 177
    return (newScrollOffset - previousItemOffset < 0.5 ? previousItemOffset : nextItemOffset)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

178
  Future _flingToAdjacentItem(double scrollVelocity) {
179
    final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
Hans Muller's avatar
Hans Muller committed
180 181 182 183 184
      .clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
    return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

185
  Future fling(double scrollVelocity) {
Hans Muller's avatar
Hans Muller committed
186 187
    switch(config.itemsSnapAlignment) {
      case ItemsSnapAlignment.adjacentItem:
188
        return _flingToAdjacentItem(scrollVelocity);
Hans Muller's avatar
Hans Muller committed
189
      default:
190
        return super.fling(scrollVelocity).then(_notifyPageChanged);
Hans Muller's avatar
Hans Muller committed
191 192 193 194 195 196 197 198 199 200
    }
  }

  Future settleScrollOffset() {
    return scrollTo(snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve)
      .then(_notifyPageChanged);
  }

  void _notifyPageChanged(_) {
    if (config.onPageChanged != null)
201
      config.onPageChanged(_scrollOffsetToPageIndex(scrollOffset));
Hans Muller's avatar
Hans Muller committed
202 203
  }
}
204

205
class PageViewport extends VirtualViewport with VirtualViewportIterableMixin {
206 207
  PageViewport({
    this.startOffset: 0.0,
208
    this.scrollDirection: Axis.vertical,
209
    this.scrollAnchor: ViewportAnchor.start,
210 211 212 213 214 215 216 217
    this.itemsWrap: false,
    this.overlayPainter,
    this.children
  }) {
    assert(scrollDirection != null);
  }

  final double startOffset;
218
  final Axis scrollDirection;
219
  final ViewportAnchor scrollAnchor;
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
  final bool itemsWrap;
  final Painter overlayPainter;
  final Iterable<Widget> children;

  RenderList createRenderObject() => new RenderList();

  _PageViewportElement createElement() => new _PageViewportElement(this);
}

class _PageViewportElement extends VirtualViewportElement<PageViewport> {
  _PageViewportElement(PageViewport widget) : super(widget);

  RenderList get renderObject => super.renderObject;

  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

240 241
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
242

243 244
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
245

246
  double scrollOffsetToPixelOffset(double scrollOffset) {
247 248
    if (_containerExtent == null)
      return 0.0;
249
    return super.scrollOffsetToPixelOffset(scrollOffset) * _containerExtent;
250 251
  }

252
  void updateRenderObject(PageViewport oldWidget) {
253 254 255
    renderObject
      ..scrollDirection = widget.scrollDirection
      ..overlayPainter = widget.overlayPainter;
256
    super.updateRenderObject(oldWidget);
257 258 259 260
  }

  double _containerExtent;

261 262 263 264
  void _updateViewportDimensions() {
    final Size containerSize = renderObject.size;

    Size materializedContentSize;
265
    switch (widget.scrollDirection) {
266
      case Axis.vertical:
267 268
        materializedContentSize = new Size(containerSize.width, _materializedChildCount * containerSize.height);
        break;
269
      case Axis.horizontal:
270 271
        materializedContentSize = new Size(_materializedChildCount * containerSize.width, containerSize.height);
        break;
272
    }
273
    renderObject.dimensions = new ViewportDimensions(containerSize: containerSize, contentSize: materializedContentSize);
274 275 276
  }

  void layout(BoxConstraints constraints) {
277
    final int length = renderObject.virtualChildCount;
278

279 280 281 282 283 284 285
    switch (widget.scrollDirection) {
      case Axis.vertical:
        _containerExtent = renderObject.size.height;
        break;
      case Axis.horizontal:
        _containerExtent =  renderObject.size.width;
        break;
286 287
    }

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
    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();
      if (widget.scrollAnchor == ViewportAnchor.end)
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
    }
309

310
    _updateViewportDimensions();
311 312 313
    super.layout(constraints);
  }
}