scrollable_list.dart 11.2 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
class ScrollableList extends Scrollable {
  ScrollableList({
Adam Barth's avatar
Adam Barth committed
16 17
    Key key,
    double initialScrollOffset,
18
    Axis scrollDirection: Axis.vertical,
19
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
Adam Barth's avatar
Adam Barth committed
20 21 22
    ScrollListener onScroll,
    SnapOffsetCallback snapOffsetCallback,
    this.itemExtent,
23 24 25
    this.itemsWrap: false,
    this.padding,
    this.scrollableListPainter,
Adam Barth's avatar
Adam Barth committed
26 27 28 29
    this.children
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
30
    scrollDirection: scrollDirection,
31
    scrollAnchor: scrollAnchor,
Adam Barth's avatar
Adam Barth committed
32
    onScroll: onScroll,
33
    snapOffsetCallback: snapOffsetCallback
34 35 36
  ) {
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
37 38

  final double itemExtent;
39 40 41
  final bool itemsWrap;
  final EdgeDims padding;
  final ScrollableListPainter scrollableListPainter;
42
  final Iterable<Widget> children;
Adam Barth's avatar
Adam Barth committed
43

Adam Barth's avatar
Adam Barth committed
44
  ScrollableState createState() => new _ScrollableListState();
Adam Barth's avatar
Adam Barth committed
45 46
}

Adam Barth's avatar
Adam Barth committed
47
class _ScrollableListState extends ScrollableState<ScrollableList> {
48
  ScrollBehavior<double, double> createScrollBehavior() => new OverscrollBehavior();
Adam Barth's avatar
Adam Barth committed
49 50 51
  ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;

  void _handleExtentsChanged(double contentExtent, double containerExtent) {
52
    config.scrollableListPainter?.contentExtent = contentExtent;
Adam Barth's avatar
Adam Barth committed
53 54
    setState(() {
      scrollTo(scrollBehavior.updateExtents(
55
        contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
Adam Barth's avatar
Adam Barth committed
56 57 58 59 60 61
        containerExtent: containerExtent,
        scrollOffset: scrollOffset
      ));
    });
  }

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  void dispatchOnScrollStart() {
    super.dispatchOnScrollStart();
    config.scrollableListPainter?.scrollStarted();
  }

  void dispatchOnScroll() {
    super.dispatchOnScroll();
    config.scrollableListPainter?.scrollOffset = scrollOffset;
  }

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

Adam Barth's avatar
Adam Barth committed
77 78
  Widget buildContent(BuildContext context) {
    return new ListViewport(
79
      onExtentsChanged: _handleExtentsChanged,
80
      scrollOffset: scrollOffset,
81
      scrollDirection: config.scrollDirection,
82
      scrollAnchor: config.scrollAnchor,
Adam Barth's avatar
Adam Barth committed
83
      itemExtent: config.itemExtent,
84 85 86
      itemsWrap: config.itemsWrap,
      padding: config.padding,
      overlayPainter: config.scrollableListPainter,
Adam Barth's avatar
Adam Barth committed
87 88 89 90 91
      children: config.children
    );
  }
}

92 93
class _VirtualListViewport extends VirtualViewport {
  _VirtualListViewport(
Adam Barth's avatar
Adam Barth committed
94
    this.onExtentsChanged,
95
    this.scrollOffset,
96
    this.scrollDirection,
97
    this.scrollAnchor,
98
    this.itemExtent,
99
    this.itemsWrap,
100
    this.padding,
101 102
    this.overlayPainter
  ) {
103 104 105
    assert(scrollDirection != null);
    assert(itemExtent != null);
  }
Adam Barth's avatar
Adam Barth committed
106

107
  final ExtentsChangedCallback onExtentsChanged;
108
  final double scrollOffset;
109
  final Axis scrollDirection;
110
  final ViewportAnchor scrollAnchor;
Adam Barth's avatar
Adam Barth committed
111
  final double itemExtent;
112 113 114
  final bool itemsWrap;
  final EdgeDims padding;
  final Painter overlayPainter;
Adam Barth's avatar
Adam Barth committed
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
  double get _leadingPadding {
    switch (scrollDirection) {
      case Axis.vertical:
        switch (scrollAnchor) {
          case ViewportAnchor.start:
            return padding.top;
          case ViewportAnchor.end:
            return padding.bottom;
        }
        break;
      case Axis.horizontal:
        switch (scrollAnchor) {
          case ViewportAnchor.start:
            return padding.left;
          case ViewportAnchor.end:
            return padding.right;
        }
        break;
    }
  }

  double get startOffset {
    if (padding == null)
      return scrollOffset;
    return scrollOffset - _leadingPadding;
  }

143
  RenderList createRenderObject(BuildContext context) => new RenderList(itemExtent: itemExtent);
Adam Barth's avatar
Adam Barth committed
144

145
  _VirtualListViewportElement createElement() => new _VirtualListViewportElement(this);
Adam Barth's avatar
Adam Barth committed
146 147
}

148
class _VirtualListViewportElement extends VirtualViewportElement {
149
  _VirtualListViewportElement(VirtualViewport widget) : super(widget);
Adam Barth's avatar
Adam Barth committed
150

151 152
  _VirtualListViewport get widget => super.widget;

Adam Barth's avatar
Adam Barth committed
153 154 155 156 157 158 159 160
  RenderList get renderObject => super.renderObject;

  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

161 162
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
Adam Barth's avatar
Adam Barth committed
163

164 165
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
Adam Barth's avatar
Adam Barth committed
166

167 168 169
  void updateRenderObject(_VirtualListViewport oldWidget) {
    renderObject
      ..scrollDirection = widget.scrollDirection
170
      ..scrollAnchor = widget.scrollAnchor
171 172 173
      ..itemExtent = widget.itemExtent
      ..padding = widget.padding
      ..overlayPainter = widget.overlayPainter;
174
    super.updateRenderObject(oldWidget);
Adam Barth's avatar
Adam Barth committed
175 176 177 178 179 180
  }

  double _contentExtent;
  double _containerExtent;

  void layout(BoxConstraints constraints) {
181 182
    final int length = renderObject.virtualChildCount;
    final double itemExtent = widget.itemExtent;
183
    final EdgeDims padding = widget.padding ?? EdgeDims.zero;
184
    final Size containerSize = renderObject.size;
185

186 187
    double containerExtent;
    double contentExtent;
Adam Barth's avatar
Adam Barth committed
188

189 190 191 192 193 194 195 196 197 198
    switch (widget.scrollDirection) {
      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
199

200 201 202 203 204 205
    if (length == 0) {
      _materializedChildBase = 0;
      _materializedChildCount = 0;
      _startOffsetBase = 0.0;
      _startOffsetLimit = double.INFINITY;
    } else {
206 207 208
      final double startOffset = widget.startOffset;
      int startItem = math.max(0, startOffset ~/ itemExtent);
      int limitItem = math.max(0, ((startOffset + containerExtent) / itemExtent).ceil());
209 210 211 212 213 214 215 216 217 218 219 220 221

      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;

      if (widget.scrollAnchor == ViewportAnchor.end)
        _materializedChildBase = (length - _materializedChildBase - _materializedChildCount) % length;
222 223
    }

224 225 226 227 228 229 230 231 232 233
    Size materializedContentSize;
    switch (widget.scrollDirection) {
      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
234 235 236 237 238 239 240 241 242 243

    super.layout(constraints);

    if (contentExtent != _contentExtent || containerExtent != _containerExtent) {
      _contentExtent = contentExtent;
      _containerExtent = containerExtent;
      widget.onExtentsChanged(_contentExtent, _containerExtent);
    }
  }
}
244

245
class ListViewport extends _VirtualListViewport with VirtualViewportFromIterable {
246 247
  ListViewport({
    ExtentsChangedCallback onExtentsChanged,
248
    double scrollOffset: 0.0,
249
    Axis scrollDirection: Axis.vertical,
250
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
251 252 253 254 255 256 257
    double itemExtent,
    bool itemsWrap: false,
    EdgeDims padding,
    Painter overlayPainter,
    this.children
  }) : super(
    onExtentsChanged,
258
    scrollOffset,
259
    scrollDirection,
260
    scrollAnchor,
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    itemExtent,
    itemsWrap,
    padding,
    overlayPainter
  );

  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
// about offscreen widgets consuming resources.
class ScrollableLazyList extends Scrollable {
  ScrollableLazyList({
    Key key,
    double initialScrollOffset,
    Axis scrollDirection: Axis.vertical,
280
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
281 282 283 284 285 286 287 288 289 290 291
    ScrollListener onScroll,
    SnapOffsetCallback snapOffsetCallback,
    this.itemExtent,
    this.itemCount,
    this.itemBuilder,
    this.padding,
    this.scrollableListPainter
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
292
    scrollAnchor: scrollAnchor,
293
    onScroll: onScroll,
294
    snapOffsetCallback: snapOffsetCallback
295 296 297
  ) {
    assert(itemExtent != null);
    assert(itemBuilder != null);
298
    assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
299 300 301 302 303 304 305 306 307 308 309 310
  }

  final double itemExtent;
  final int itemCount;
  final ItemListBuilder itemBuilder;
  final EdgeDims padding;
  final ScrollableListPainter scrollableListPainter;

  ScrollableState createState() => new _ScrollableLazyListState();
}

class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
311
  ScrollBehavior<double, double> createScrollBehavior() => new OverscrollBehavior();
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
  ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;

  void _handleExtentsChanged(double contentExtent, double containerExtent) {
    config.scrollableListPainter?.contentExtent = contentExtent;
    setState(() {
      scrollTo(scrollBehavior.updateExtents(
        contentExtent: contentExtent,
        containerExtent: containerExtent,
        scrollOffset: scrollOffset
      ));
    });
  }

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

  void dispatchOnScroll() {
    super.dispatchOnScroll();
    config.scrollableListPainter?.scrollOffset = scrollOffset;
  }

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

  Widget buildContent(BuildContext context) {
    return new LazyListViewport(
      onExtentsChanged: _handleExtentsChanged,
343
      scrollOffset: scrollOffset,
344
      scrollDirection: config.scrollDirection,
345
      scrollAnchor: config.scrollAnchor,
346 347 348 349 350 351 352 353 354
      itemExtent: config.itemExtent,
      itemCount: config.itemCount,
      itemBuilder: config.itemBuilder,
      padding: config.padding,
      overlayPainter: config.scrollableListPainter
    );
  }
}

355
class LazyListViewport extends _VirtualListViewport with VirtualViewportFromBuilder {
356 357
  LazyListViewport({
    ExtentsChangedCallback onExtentsChanged,
358
    double scrollOffset: 0.0,
359
    Axis scrollDirection: Axis.vertical,
360
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
361 362 363 364 365 366 367
    double itemExtent,
    EdgeDims padding,
    Painter overlayPainter,
    this.itemCount,
    this.itemBuilder
  }) : super(
    onExtentsChanged,
368
    scrollOffset,
369
    scrollDirection,
370
    scrollAnchor,
371 372 373 374 375 376 377 378 379
    itemExtent,
    false, // Don't support wrapping yet.
    padding,
    overlayPainter
  );

  final int itemCount;
  final ItemListBuilder itemBuilder;
}