scrollable_list.dart 12.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;

Adam Barth's avatar
Adam Barth committed
7
import 'framework.dart';
8
import 'scroll_behavior.dart';
Adam Barth's avatar
Adam Barth committed
9 10 11 12 13
import 'scrollable.dart';
import 'virtual_viewport.dart';

import 'package:flutter/rendering.dart';

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/// If true, the ClampOverscroll's [Scrollable] descendant will clamp its
/// viewport's scrollOffsets to the [ScrollBehavior]'s min and max values.
/// In this case the Scrollable's scrollOffset will still over and undershoot
/// the ScrollBehavior's limits, but the viewport itself will not.
class ClampOverscrolls extends InheritedWidget {
  ClampOverscrolls({
    Key key,
    this.value,
    Widget child
  }) : super(key: key, child: child) {
    assert(value != null);
    assert(child != null);
  }

  /// True if the [Scrollable] descendant should clamp its viewport's scrollOffset
  /// values when they are less than the [ScrollBehavior]'s minimum or greater than
  /// its maximum.
  final bool value;

  static bool of(BuildContext context) {
    final ClampOverscrolls result = context.inheritFromWidgetOfExactType(ClampOverscrolls);
    return result?.value ?? false;
  }

  @override
  bool updateShouldNotify(ClampOverscrolls old) => value != old.value;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('value: $value');
  }
}

48 49
class ScrollableList extends Scrollable {
  ScrollableList({
Adam Barth's avatar
Adam Barth committed
50 51
    Key key,
    double initialScrollOffset,
52
    Axis scrollDirection: Axis.vertical,
53
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
54
    ScrollListener onScrollStart,
Adam Barth's avatar
Adam Barth committed
55
    ScrollListener onScroll,
56
    ScrollListener onScrollEnd,
Adam Barth's avatar
Adam Barth committed
57 58
    SnapOffsetCallback snapOffsetCallback,
    this.itemExtent,
59 60
    this.itemsWrap: false,
    this.padding,
Adam Barth's avatar
Adam Barth committed
61 62 63 64
    this.children
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
65
    scrollDirection: scrollDirection,
66
    scrollAnchor: scrollAnchor,
67
    onScrollStart: onScrollStart,
Adam Barth's avatar
Adam Barth committed
68
    onScroll: onScroll,
69
    onScrollEnd: onScrollEnd,
70
    snapOffsetCallback: snapOffsetCallback
71 72 73
  ) {
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
74 75

  final double itemExtent;
76
  final bool itemsWrap;
77 78

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

81
  final Iterable<Widget> children;
Adam Barth's avatar
Adam Barth committed
82

83
  @override
Adam Barth's avatar
Adam Barth committed
84
  ScrollableState createState() => new _ScrollableListState();
Adam Barth's avatar
Adam Barth committed
85 86
}

Adam Barth's avatar
Adam Barth committed
87
class _ScrollableListState extends ScrollableState<ScrollableList> {
88
  @override
89
  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
90 91

  @override
Adam Barth's avatar
Adam Barth committed
92 93 94 95
  ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;

  void _handleExtentsChanged(double contentExtent, double containerExtent) {
    setState(() {
96
      didUpdateScrollBehavior(scrollBehavior.updateExtents(
97
        contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
Adam Barth's avatar
Adam Barth committed
98 99 100 101 102 103
        containerExtent: containerExtent,
        scrollOffset: scrollOffset
      ));
    });
  }

104
  @override
Adam Barth's avatar
Adam Barth committed
105
  Widget buildContent(BuildContext context) {
106 107
    final bool clampOverscrolls = ClampOverscrolls.of(context);
    final double listScrollOffset = clampOverscrolls
108 109
      ? scrollOffset.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset)
      : scrollOffset;
110
    Widget viewport = new ListViewport(
111
      onExtentsChanged: _handleExtentsChanged,
112
      scrollOffset: listScrollOffset,
113
      mainAxis: config.scrollDirection,
114
      anchor: config.scrollAnchor,
Adam Barth's avatar
Adam Barth committed
115
      itemExtent: config.itemExtent,
116 117
      itemsWrap: config.itemsWrap,
      padding: config.padding,
Adam Barth's avatar
Adam Barth committed
118 119
      children: config.children
    );
120 121 122
    if (clampOverscrolls)
      viewport = new ClampOverscrolls(value: false, child: viewport);
    return viewport;
Adam Barth's avatar
Adam Barth committed
123 124 125
  }
}

126 127
class _VirtualListViewport extends VirtualViewport {
  _VirtualListViewport(
Adam Barth's avatar
Adam Barth committed
128
    this.onExtentsChanged,
129
    this.scrollOffset,
130
    this.mainAxis,
131
    this.anchor,
132
    this.itemExtent,
133
    this.itemsWrap,
134
    this.padding,
135 136
    this.overlayPainter
  ) {
137
    assert(mainAxis != null);
138 139
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
140

141
  final ExtentsChangedCallback onExtentsChanged;
142
  final double scrollOffset;
143
  final Axis mainAxis;
144
  final ViewportAnchor anchor;
Adam Barth's avatar
Adam Barth committed
145
  final double itemExtent;
146
  final bool itemsWrap;
147
  final EdgeInsets padding;
148
  final RenderObjectPainter overlayPainter;
Adam Barth's avatar
Adam Barth committed
149

150
  double get _leadingPadding {
151
    switch (mainAxis) {
152
      case Axis.vertical:
153
        switch (anchor) {
154 155 156 157 158 159 160
          case ViewportAnchor.start:
            return padding.top;
          case ViewportAnchor.end:
            return padding.bottom;
        }
        break;
      case Axis.horizontal:
161
        switch (anchor) {
162 163 164 165 166 167 168 169 170
          case ViewportAnchor.start:
            return padding.left;
          case ViewportAnchor.end:
            return padding.right;
        }
        break;
    }
  }

171
  @override
172 173 174 175 176 177
  double get startOffset {
    if (padding == null)
      return scrollOffset;
    return scrollOffset - _leadingPadding;
  }

178
  @override
179
  RenderList createRenderObject(BuildContext context) => new RenderList(itemExtent: itemExtent);
Adam Barth's avatar
Adam Barth committed
180

181
  @override
182
  _VirtualListViewportElement createElement() => new _VirtualListViewportElement(this);
Adam Barth's avatar
Adam Barth committed
183 184
}

185
class _VirtualListViewportElement extends VirtualViewportElement {
186
  _VirtualListViewportElement(VirtualViewport widget) : super(widget);
Adam Barth's avatar
Adam Barth committed
187

188
  @override
189 190
  _VirtualListViewport get widget => super.widget;

191
  @override
Adam Barth's avatar
Adam Barth committed
192 193
  RenderList get renderObject => super.renderObject;

194
  @override
Adam Barth's avatar
Adam Barth committed
195 196 197
  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

198
  @override
Adam Barth's avatar
Adam Barth committed
199 200 201
  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

202
  @override
203 204
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
Adam Barth's avatar
Adam Barth committed
205

206
  @override
207 208
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
Adam Barth's avatar
Adam Barth committed
209

210
  @override
211 212
  void updateRenderObject(_VirtualListViewport oldWidget) {
    renderObject
213
      ..mainAxis = widget.mainAxis
214
      ..anchor = widget.anchor
215 216 217
      ..itemExtent = widget.itemExtent
      ..padding = widget.padding
      ..overlayPainter = widget.overlayPainter;
218
    super.updateRenderObject(oldWidget);
Adam Barth's avatar
Adam Barth committed
219 220
  }

221 222
  double _lastReportedContentExtent;
  double _lastReportedContainerExtent;
Adam Barth's avatar
Adam Barth committed
223

224
  @override
Adam Barth's avatar
Adam Barth committed
225
  void layout(BoxConstraints constraints) {
226 227
    final int length = renderObject.virtualChildCount;
    final double itemExtent = widget.itemExtent;
228
    final EdgeInsets padding = widget.padding ?? EdgeInsets.zero;
229
    final Size containerSize = renderObject.size;
230

231 232
    double containerExtent;
    double contentExtent;
Adam Barth's avatar
Adam Barth committed
233

234
    switch (widget.mainAxis) {
235 236 237 238 239 240 241 242 243
      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
244

245 246 247 248 249 250
    if (length == 0) {
      _materializedChildBase = 0;
      _materializedChildCount = 0;
      _startOffsetBase = 0.0;
      _startOffsetLimit = double.INFINITY;
    } else {
251 252 253
      final double startOffset = widget.startOffset;
      int startItem = math.max(0, startOffset ~/ itemExtent);
      int limitItem = math.max(0, ((startOffset + containerExtent) / itemExtent).ceil());
254 255 256 257 258 259 260 261 262 263 264

      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;

265
      if (widget.anchor == ViewportAnchor.end)
266
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
267 268
    }

269
    Size materializedContentSize;
270
    switch (widget.mainAxis) {
271 272 273 274 275 276 277 278
      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
279 280 281

    super.layout(constraints);

282 283 284 285
    if (contentExtent != _lastReportedContentExtent || containerExtent != _lastReportedContainerExtent) {
      _lastReportedContentExtent = contentExtent;
      _lastReportedContainerExtent = containerExtent;
      widget.onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent);
Adam Barth's avatar
Adam Barth committed
286 287 288
    }
  }
}
289

290
class ListViewport extends _VirtualListViewport with VirtualViewportFromIterable {
291 292
  ListViewport({
    ExtentsChangedCallback onExtentsChanged,
293
    double scrollOffset: 0.0,
294
    Axis mainAxis: Axis.vertical,
295
    ViewportAnchor anchor: ViewportAnchor.start,
296 297
    double itemExtent,
    bool itemsWrap: false,
298
    EdgeInsets padding,
299
    RenderObjectPainter overlayPainter,
300 301 302
    this.children
  }) : super(
    onExtentsChanged,
303
    scrollOffset,
304
    mainAxis,
305
    anchor,
306 307 308 309 310 311
    itemExtent,
    itemsWrap,
    padding,
    overlayPainter
  );

312
  @override
313 314 315 316 317 318 319
  final Iterable<Widget> children;
}

/// An optimized scrollable widget for a large number of children that are all
/// the same size (extent) in the scrollDirection. For example for
/// ScrollDirection.vertical itemExtent is the height of each item. Use this
/// widget when you have a large number of children or when you are concerned
320
/// about offscreen widgets consuming resources.
321 322 323 324 325
class ScrollableLazyList extends Scrollable {
  ScrollableLazyList({
    Key key,
    double initialScrollOffset,
    Axis scrollDirection: Axis.vertical,
326
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
327 328 329 330 331
    ScrollListener onScroll,
    SnapOffsetCallback snapOffsetCallback,
    this.itemExtent,
    this.itemCount,
    this.itemBuilder,
332
    this.padding
333 334 335 336
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
337
    scrollAnchor: scrollAnchor,
338
    onScroll: onScroll,
339
    snapOffsetCallback: snapOffsetCallback
340 341 342
  ) {
    assert(itemExtent != null);
    assert(itemBuilder != null);
343
    assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
344 345 346 347 348
  }

  final double itemExtent;
  final int itemCount;
  final ItemListBuilder itemBuilder;
349 350

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

353
  @override
354 355 356 357
  ScrollableState createState() => new _ScrollableLazyListState();
}

class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
358
  @override
359
  ExtentScrollBehavior createScrollBehavior() => new OverscrollBehavior();
360 361

  @override
362 363 364 365
  ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;

  void _handleExtentsChanged(double contentExtent, double containerExtent) {
    setState(() {
366
      didUpdateScrollBehavior(scrollBehavior.updateExtents(
367 368 369 370 371 372 373
        contentExtent: contentExtent,
        containerExtent: containerExtent,
        scrollOffset: scrollOffset
      ));
    });
  }

374
  @override
375 376 377
  Widget buildContent(BuildContext context) {
    return new LazyListViewport(
      onExtentsChanged: _handleExtentsChanged,
378
      scrollOffset: scrollOffset,
379
      mainAxis: config.scrollDirection,
380
      anchor: config.scrollAnchor,
381 382 383
      itemExtent: config.itemExtent,
      itemCount: config.itemCount,
      itemBuilder: config.itemBuilder,
384
      padding: config.padding
385 386 387 388
    );
  }
}

389
class LazyListViewport extends _VirtualListViewport with VirtualViewportFromBuilder {
390 391
  LazyListViewport({
    ExtentsChangedCallback onExtentsChanged,
392
    double scrollOffset: 0.0,
393
    Axis mainAxis: Axis.vertical,
394
    ViewportAnchor anchor: ViewportAnchor.start,
395
    double itemExtent,
396
    EdgeInsets padding,
397
    RenderObjectPainter overlayPainter,
398 399 400 401
    this.itemCount,
    this.itemBuilder
  }) : super(
    onExtentsChanged,
402
    scrollOffset,
403
    mainAxis,
404
    anchor,
405 406 407 408 409 410
    itemExtent,
    false, // Don't support wrapping yet.
    padding,
    overlayPainter
  );

411
  @override
412
  final int itemCount;
413 414

  @override
415 416
  final ItemListBuilder itemBuilder;
}