virtual_viewport.dart 7.41 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 8 9 10 11 12 13 14 15
import 'basic.dart';
import 'framework.dart';

import 'package:flutter/rendering.dart';

typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);

abstract class VirtualViewport extends RenderObjectWidget {
  double get startOffset;
16 17 18 19 20 21 22 23 24

  _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
25 26 27 28 29 30 31
}

abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
  VirtualViewportElement(T widget) : super(widget);

  int get materializedChildBase;
  int get materializedChildCount;
32 33 34
  double get startOffsetBase;
  double get startOffsetLimit;

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  /// Returns the pixel offset for a scroll offset, accounting for the scroll
  /// anchor.
  double scrollOffsetToPixelOffset(double scrollOffset) {
    switch (renderObject.scrollAnchor) {
      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) {
    switch (renderObject.scrollDirection) {
      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
56 57 58 59 60 61 62 63 64 65 66 67

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

  RenderVirtualViewport get renderObject => super.renderObject;

  void visitChildren(ElementVisitor visitor) {
    if (_materializedChildren == null)
      return;
    for (Element child in _materializedChildren)
      visitor(child);
  }

68 69
  _WidgetProvider _widgetProvider;

Adam Barth's avatar
Adam Barth committed
70
  void mount(Element parent, dynamic newSlot) {
71 72
    _widgetProvider = widget._createWidgetProvider();
    _widgetProvider.didUpdateWidget(null, widget);
Adam Barth's avatar
Adam Barth committed
73 74
    super.mount(parent, newSlot);
    renderObject.callback = layout;
75
    updateRenderObject(null);
Adam Barth's avatar
Adam Barth committed
76 77 78 79 80 81 82 83
  }

  void unmount() {
    renderObject.callback = null;
    super.unmount();
  }

  void update(T newWidget) {
84
    T oldWidget = widget;
85
    _widgetProvider.didUpdateWidget(oldWidget, newWidget);
Adam Barth's avatar
Adam Barth committed
86
    super.update(newWidget);
87
    updateRenderObject(oldWidget);
Adam Barth's avatar
Adam Barth committed
88 89 90 91 92
    if (!renderObject.needsLayout)
      _materializeChildren();
  }

  void _updatePaintOffset() {
93
    renderObject.paintOffset = scrollOffsetToPixelDelta(widget.startOffset - startOffsetBase);
94 95
  }

96
  void updateRenderObject(T oldWidget) {
97
    renderObject.virtualChildCount = _widgetProvider.virtualChildCount;
Adam Barth's avatar
Adam Barth committed
98

99
    if (startOffsetBase != null) {
Adam Barth's avatar
Adam Barth committed
100 101 102 103 104
      _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) {
105
        final double startOffset = widget.startOffset;
106 107
        bool shouldLayout = false;
        if (startOffsetBase != null) {
108
          if (startOffset < startOffsetBase)
109
            shouldLayout = true;
110
          else if (startOffset == startOffsetBase && oldWidget?.startOffset != startOffsetBase)
111 112 113 114
            shouldLayout = true;
        }

        if (startOffsetLimit != null) {
115
          if (startOffset > startOffsetLimit)
116
            shouldLayout = true;
117
          else if (startOffset == startOffsetLimit && oldWidget?.startOffset != startOffsetLimit)
118 119 120 121
            shouldLayout = true;
        }

        if (shouldLayout)
Adam Barth's avatar
Adam Barth committed
122 123 124 125 126 127
          renderObject.markNeedsLayout();
      }
    }
  }

  void layout(BoxConstraints constraints) {
128 129
    assert(startOffsetBase != null);
    assert(startOffsetLimit != null);
Adam Barth's avatar
Adam Barth committed
130
    _updatePaintOffset();
131
    BuildableElement.lockState(_materializeChildren, building: true);
Adam Barth's avatar
Adam Barth committed
132 133 134 135 136 137 138
  }

  void _materializeChildren() {
    int base = materializedChildBase;
    int count = materializedChildCount;
    assert(base != null);
    assert(count != null);
139
    _widgetProvider.prepareChildren(this, base, count);
Adam Barth's avatar
Adam Barth committed
140 141 142
    List<Widget> newWidgets = new List<Widget>(count);
    for (int i = 0; i < count; ++i) {
      int childIndex = base + i;
143
      Widget child = _widgetProvider.getChild(childIndex);
Adam Barth's avatar
Adam Barth committed
144 145 146 147 148 149 150
      Key key = new ValueKey(child.key ?? childIndex);
      newWidgets[i] = new RepaintBoundary(key: key, child: child);
    }
    _materializedChildren = updateChildren(_materializedChildren, newWidgets);
  }

  void insertChildRenderObject(RenderObject child, Element slot) {
151
    renderObject.insert(child, after: slot?.renderObject);
Adam Barth's avatar
Adam Barth committed
152 153 154 155
  }

  void moveChildRenderObject(RenderObject child, Element slot) {
    assert(child.parent == renderObject);
156
    renderObject.move(child, after: slot?.renderObject);
Adam Barth's avatar
Adam Barth committed
157 158 159 160 161 162 163
  }

  void removeChildRenderObject(RenderObject child) {
    assert(child.parent == renderObject);
    renderObject.remove(child);
  }
}
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244

abstract class VirtualViewportIterableMixin extends VirtualViewport {
  Iterable<Widget> get children;

  _IterableWidgetProvider _createWidgetProvider() => new _IterableWidgetProvider();
}

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

  void didUpdateWidget(VirtualViewportIterableMixin oldWidget, VirtualViewportIterableMixin newWidget) {
    if (oldWidget == null || newWidget.children != oldWidget.children) {
      _iterator = null;
      _widgets = <Widget>[];
      _length = newWidget.children.length;
    }
  }

  int get virtualChildCount => _length;

  void prepareChildren(VirtualViewportElement context, int base, int count) {
    int limit = base < 0 ? _length : math.min(_length, base + count);
    if (limit <= _widgets.length)
      return;
    VirtualViewportIterableMixin widget = context.widget;
    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);
    }
  }

  Widget getChild(int i) => _widgets[(i % _length).abs()];
}

typedef List<Widget> ItemListBuilder(BuildContext context, int start, int count);

abstract class VirtualViewportLazyMixin extends VirtualViewport {
  int get itemCount;
  ItemListBuilder get itemBuilder;

  _LazyWidgetProvider _createWidgetProvider() => new _LazyWidgetProvider();
}

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

  void didUpdateWidget(VirtualViewportLazyMixin oldWidget, VirtualViewportLazyMixin newWidget) {
    if (_length != newWidget.itemCount || oldWidget?.itemBuilder != newWidget.itemBuilder) {
      _length = newWidget.itemCount;
      _base = null;
      _widgets = null;
    }
  }

  int get virtualChildCount => _length;

  void prepareChildren(VirtualViewportElement context, int base, int count) {
    if (_widgets != null && _widgets.length == count && _base == base)
      return;
    VirtualViewportLazyMixin widget = context.widget;
    _base = base;
    _widgets = widget.itemBuilder(context, base, count);
  }

  Widget getChild(int i) {
    int n = _length ?? _widgets.length;
    return _widgets[(i % n).abs()];
  }
}