scrollable_list.dart 11.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
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> {
Adam Barth's avatar
Adam Barth committed
48 49 50 51
  ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
  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;
  }

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

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

148 149
class _VirtualListViewportElement extends VirtualViewportElement<_VirtualListViewport> {
  _VirtualListViewportElement(VirtualViewport widget) : super(widget);
Adam Barth's avatar
Adam Barth committed
150 151 152 153 154 155 156 157 158

  RenderList get renderObject => super.renderObject;

  int get materializedChildBase => _materializedChildBase;
  int _materializedChildBase;

  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;

159 160
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
Adam Barth's avatar
Adam Barth committed
161

162 163
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
Adam Barth's avatar
Adam Barth committed
164

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

  double _contentExtent;
  double _containerExtent;

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

184 185
    double containerExtent;
    double contentExtent;
Adam Barth's avatar
Adam Barth committed
186

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

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

      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;
220 221
    }

222 223 224 225 226 227 228 229 230 231
    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
232 233 234 235 236 237 238 239 240 241

    super.layout(constraints);

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

class ListViewport extends _VirtualListViewport with VirtualViewportIterableMixin {
  ListViewport({
    ExtentsChangedCallback onExtentsChanged,
246
    double scrollOffset: 0.0,
247
    Axis scrollDirection: Axis.vertical,
248
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
249 250 251 252 253 254 255
    double itemExtent,
    bool itemsWrap: false,
    EdgeDims padding,
    Painter overlayPainter,
    this.children
  }) : super(
    onExtentsChanged,
256
    scrollOffset,
257
    scrollDirection,
258
    scrollAnchor,
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
    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,
278
    ViewportAnchor scrollAnchor: ViewportAnchor.start,
279 280 281 282 283 284 285 286 287 288 289
    ScrollListener onScroll,
    SnapOffsetCallback snapOffsetCallback,
    this.itemExtent,
    this.itemCount,
    this.itemBuilder,
    this.padding,
    this.scrollableListPainter
  }) : super(
    key: key,
    initialScrollOffset: initialScrollOffset,
    scrollDirection: scrollDirection,
290
    scrollAnchor: scrollAnchor,
291
    onScroll: onScroll,
292
    snapOffsetCallback: snapOffsetCallback
293 294 295
  ) {
    assert(itemExtent != null);
    assert(itemBuilder != null);
296
    assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 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
  }

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

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

class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
  ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
  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,
341
      scrollOffset: scrollOffset,
342
      scrollDirection: config.scrollDirection,
343
      scrollAnchor: config.scrollAnchor,
344 345 346 347 348 349 350 351 352 353 354 355
      itemExtent: config.itemExtent,
      itemCount: config.itemCount,
      itemBuilder: config.itemBuilder,
      padding: config.padding,
      overlayPainter: config.scrollableListPainter
    );
  }
}

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

  final int itemCount;
  final ItemListBuilder itemBuilder;
}