virtual_viewport.dart 10.3 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 'basic.dart';
8
import 'debug.dart';
Adam Barth's avatar
Adam Barth committed
9 10 11 12
import 'framework.dart';

import 'package:flutter/rendering.dart';

13 14 15 16 17 18 19
/// Signature for reporting the interior and exterior dimensions of a viewport.
///
///  * The [contentExtent] is the interior dimension of the viewport (i.e., the
///    size of the thing that's being viewed through the viewport).
///  * The [containerExtent] is the exterior dimension of the viewport (i.e.,
///    the amount of the thing inside the viewport that is visible from outside
///    the viewport).
Adam Barth's avatar
Adam Barth committed
20 21
typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);

22
/// An abstract widget whose children are not all materialized.
Adam Barth's avatar
Adam Barth committed
23
abstract class VirtualViewport extends RenderObjectWidget {
24
  /// The offset from the [ViewportAnchor] at which the viewport should start painting children.
Adam Barth's avatar
Adam Barth committed
25
  double get startOffset;
26 27 28 29 30 31 32 33 34

  _WidgetProvider _createWidgetProvider();
}

abstract class _WidgetProvider {
  void didUpdateWidget(VirtualViewport oldWidget, VirtualViewport newWidget);
  int get virtualChildCount;
  void prepareChildren(VirtualViewportElement context, int base, int count);
  Widget getChild(int i);
Adam Barth's avatar
Adam Barth committed
35 36
}

37 38 39 40 41
/// Materializes a contiguous subset of its children.
///
/// This class is a building block for building a widget that has more children
/// than it wishes to display at any given time. For example, [ScrollableList]
/// uses this element to materialize only those children that are visible.
42 43 44
abstract class VirtualViewportElement extends RenderObjectElement {
  VirtualViewportElement(VirtualViewport widget) : super(widget);

45
  @override
46
  VirtualViewport get widget => super.widget;
Adam Barth's avatar
Adam Barth committed
47

48
  /// The index of the first child to materialize.
Adam Barth's avatar
Adam Barth committed
49
  int get materializedChildBase;
50 51

  /// The number of children to materializes.
Adam Barth's avatar
Adam Barth committed
52
  int get materializedChildCount;
53 54

  /// The least offset for which [materializedChildBase] and [materializedChildCount] are valid.
55
  double get startOffsetBase;
56 57

  /// The greatest offset for which [materializedChildBase] and [materializedChildCount] are valid.
58 59
  double get startOffsetLimit;

60 61 62
  /// Returns the pixel offset for a scroll offset, accounting for the scroll
  /// anchor.
  double scrollOffsetToPixelOffset(double scrollOffset) {
63
    switch (renderObject.anchor) {
64 65 66 67 68 69 70 71 72 73
      case ViewportAnchor.start:
        return -scrollOffset;
      case ViewportAnchor.end:
        return scrollOffset;
    }
  }

  /// Returns a two-dimensional representation of the scroll offset, accounting
  /// for the scroll direction and scroll anchor.
  Offset scrollOffsetToPixelDelta(double scrollOffset) {
74
    switch (renderObject.mainAxis) {
75 76 77 78 79 80
      case Axis.horizontal:
        return new Offset(scrollOffsetToPixelOffset(scrollOffset), 0.0);
      case Axis.vertical:
        return new Offset(0.0, scrollOffsetToPixelOffset(scrollOffset));
    }
  }
Adam Barth's avatar
Adam Barth committed
81 82 83

  List<Element> _materializedChildren = const <Element>[];

84
  @override
Hixie's avatar
Hixie committed
85
  RenderVirtualViewport<ContainerBoxParentDataMixin<RenderBox>> get renderObject => super.renderObject;
Adam Barth's avatar
Adam Barth committed
86

87
  @override
Adam Barth's avatar
Adam Barth committed
88 89 90 91 92 93 94
  void visitChildren(ElementVisitor visitor) {
    if (_materializedChildren == null)
      return;
    for (Element child in _materializedChildren)
      visitor(child);
  }

95 96
  _WidgetProvider _widgetProvider;

97
  @override
Adam Barth's avatar
Adam Barth committed
98
  void mount(Element parent, dynamic newSlot) {
99 100
    _widgetProvider = widget._createWidgetProvider();
    _widgetProvider.didUpdateWidget(null, widget);
Adam Barth's avatar
Adam Barth committed
101 102
    super.mount(parent, newSlot);
    renderObject.callback = layout;
103
    updateRenderObject(null);
Adam Barth's avatar
Adam Barth committed
104 105
  }

106
  @override
Adam Barth's avatar
Adam Barth committed
107 108 109 110 111
  void unmount() {
    renderObject.callback = null;
    super.unmount();
  }

112
  @override
113 114
  void update(VirtualViewport newWidget) {
    VirtualViewport oldWidget = widget;
115
    _widgetProvider.didUpdateWidget(oldWidget, newWidget);
Adam Barth's avatar
Adam Barth committed
116
    super.update(newWidget);
117
    updateRenderObject(oldWidget);
Adam Barth's avatar
Adam Barth committed
118 119 120 121 122
    if (!renderObject.needsLayout)
      _materializeChildren();
  }

  void _updatePaintOffset() {
123
    renderObject.paintOffset = scrollOffsetToPixelDelta(widget.startOffset - startOffsetBase);
124 125
  }

126
  void updateRenderObject(VirtualViewport oldWidget) {
127
    renderObject.virtualChildCount = _widgetProvider.virtualChildCount;
Adam Barth's avatar
Adam Barth committed
128

129
    if (startOffsetBase != null) {
Adam Barth's avatar
Adam Barth committed
130 131 132 133 134
      _updatePaintOffset();

      // If we don't already need layout, we need to request a layout if the
      // viewport has shifted to expose new children.
      if (!renderObject.needsLayout) {
135
        final double startOffset = widget.startOffset;
136 137
        bool shouldLayout = false;
        if (startOffsetBase != null) {
138
          if (startOffset < startOffsetBase)
139
            shouldLayout = true;
140
          else if (startOffset == startOffsetBase && oldWidget?.startOffset != startOffsetBase)
141 142 143 144
            shouldLayout = true;
        }

        if (startOffsetLimit != null) {
145
          if (startOffset > startOffsetLimit)
146
            shouldLayout = true;
147
          else if (startOffset == startOffsetLimit && oldWidget?.startOffset != startOffsetLimit)
148 149 150 151
            shouldLayout = true;
        }

        if (shouldLayout)
Adam Barth's avatar
Adam Barth committed
152 153 154 155 156
          renderObject.markNeedsLayout();
      }
    }
  }

157 158 159 160 161 162
  /// Called by [RenderVirtualViewport] during layout.
  ///
  /// Subclasses should override this function to compute [materializedChildBase]
  /// and [materializedChildCount]. Overrides should call this function to
  /// update the [RenderVirtualViewport]'s paint offset and to materialize the
  /// children.
Adam Barth's avatar
Adam Barth committed
163
  void layout(BoxConstraints constraints) {
164 165
    assert(startOffsetBase != null);
    assert(startOffsetLimit != null);
Adam Barth's avatar
Adam Barth committed
166
    _updatePaintOffset();
167
    owner.lockState(_materializeChildren, building: true);
Adam Barth's avatar
Adam Barth committed
168 169 170 171 172 173 174
  }

  void _materializeChildren() {
    int base = materializedChildBase;
    int count = materializedChildCount;
    assert(base != null);
    assert(count != null);
175
    _widgetProvider.prepareChildren(this, base, count);
Adam Barth's avatar
Adam Barth committed
176 177 178
    List<Widget> newWidgets = new List<Widget>(count);
    for (int i = 0; i < count; ++i) {
      int childIndex = base + i;
179
      Widget child = _widgetProvider.getChild(childIndex);
180
      newWidgets[i] = new RepaintBoundary.wrap(child, childIndex);
Adam Barth's avatar
Adam Barth committed
181
    }
182 183 184

    assert(!debugChildrenHaveDuplicateKeys(widget, newWidgets));
    _materializedChildren = updateChildren(_materializedChildren, newWidgets.toList());
Adam Barth's avatar
Adam Barth committed
185 186
  }

187
  @override
Adam Barth's avatar
Adam Barth committed
188
  void insertChildRenderObject(RenderObject child, Element slot) {
189
    renderObject.insert(child, after: slot?.renderObject);
Adam Barth's avatar
Adam Barth committed
190 191
  }

192
  @override
Adam Barth's avatar
Adam Barth committed
193 194
  void moveChildRenderObject(RenderObject child, Element slot) {
    assert(child.parent == renderObject);
195
    renderObject.move(child, after: slot?.renderObject);
Adam Barth's avatar
Adam Barth committed
196 197
  }

198
  @override
Adam Barth's avatar
Adam Barth committed
199 200 201 202 203
  void removeChildRenderObject(RenderObject child) {
    assert(child.parent == renderObject);
    renderObject.remove(child);
  }
}
204

205 206 207 208 209 210
/// A VirtualViewport that represents its children using [Iterable<Widget>].
///
/// The iterator is advanced just far enough to obtain widgets for the children
/// that need to be materialized.
abstract class VirtualViewportFromIterable extends VirtualViewport {
  /// The children, some of which might be materialized.
211 212
  Iterable<Widget> get children;

213
  @override
214 215 216 217 218 219 220 221
  _IterableWidgetProvider _createWidgetProvider() => new _IterableWidgetProvider();
}

class _IterableWidgetProvider extends _WidgetProvider {
  int _length;
  Iterator<Widget> _iterator;
  List<Widget> _widgets;

222
  @override
223
  void didUpdateWidget(VirtualViewportFromIterable oldWidget, VirtualViewportFromIterable newWidget) {
224 225 226 227 228 229 230
    if (oldWidget == null || newWidget.children != oldWidget.children) {
      _iterator = null;
      _widgets = <Widget>[];
      _length = newWidget.children.length;
    }
  }

231
  @override
232 233
  int get virtualChildCount => _length;

234
  @override
235 236 237 238
  void prepareChildren(VirtualViewportElement context, int base, int count) {
    int limit = base < 0 ? _length : math.min(_length, base + count);
    if (limit <= _widgets.length)
      return;
239
    VirtualViewportFromIterable widget = context.widget;
240 241 242 243 244 245 246 247 248 249 250 251 252 253
    if (widget.children is List<Widget>) {
      _widgets = widget.children;
      return;
    }
    _iterator ??= widget.children.iterator;
    while (_widgets.length < limit) {
      bool moved = _iterator.moveNext();
      assert(moved);
      Widget current = _iterator.current;
      assert(current != null);
      _widgets.add(current);
    }
  }

254
  @override
255 256 257
  Widget getChild(int i) => _widgets[(i % _length).abs()];
}

258
/// Signature of a callback that returns the sublist of widgets in the given range.
259 260
typedef List<Widget> ItemListBuilder(BuildContext context, int start, int count);

261 262 263 264 265 266
/// A VirtualViewport that represents its children using [ItemListBuilder].
///
/// This widget is less ergonomic than [VirtualViewportFromIterable] but scales to
/// unlimited numbers of children.
abstract class VirtualViewportFromBuilder extends VirtualViewport {
  /// The total number of children that can be built.
267
  int get itemCount;
268 269 270 271 272

  /// A callback to build the subset of widgets that are needed to populate the
  /// viewport. Not all of the returned widgets will actually be included in the
  /// viewport (e.g., if we need to measure the size of non-visible children to
  /// determine which children are visible).
273 274
  ItemListBuilder get itemBuilder;

275
  @override
276 277 278 279 280 281 282 283
  _LazyWidgetProvider _createWidgetProvider() => new _LazyWidgetProvider();
}

class _LazyWidgetProvider extends _WidgetProvider {
  int _length;
  int _base;
  List<Widget> _widgets;

284
  @override
285
  void didUpdateWidget(VirtualViewportFromBuilder oldWidget, VirtualViewportFromBuilder newWidget) {
286 287
    // TODO(abarth): We shouldn't check the itemBuilder closure for equality with.
    // instead, we should use the widget's identity to decide whether to rebuild.
288 289 290 291 292 293 294
    if (_length != newWidget.itemCount || oldWidget?.itemBuilder != newWidget.itemBuilder) {
      _length = newWidget.itemCount;
      _base = null;
      _widgets = null;
    }
  }

295
  @override
296 297
  int get virtualChildCount => _length;

298
  @override
299 300 301
  void prepareChildren(VirtualViewportElement context, int base, int count) {
    if (_widgets != null && _widgets.length == count && _base == base)
      return;
302
    VirtualViewportFromBuilder widget = context.widget;
303 304 305 306
    _base = base;
    _widgets = widget.itemBuilder(context, base, count);
  }

307
  @override
308
  Widget getChild(int i) {
309 310 311
    final int childCount = virtualChildCount;
    final int index = childCount != null ? (i % childCount).abs() : i;
    return _widgets[index - _base];
312 313
  }
}