mixed_viewport.dart 18.2 KB
Newer Older
1 2 3 4 5 6
// 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.

import 'dart:collection';

7 8 9 10 11
import 'package:sky/src/rendering/block.dart';
import 'package:sky/src/rendering/box.dart';
import 'package:sky/src/rendering/object.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/basic.dart';
12 13 14 15 16 17 18 19

// return null if index is greater than index of last entry
typedef Widget IndexedBuilder(int index);

class _Key {
  const _Key(this.type, this.key);
  factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key);
  final Type type;
20
  final Key key;
21 22
  bool operator ==(other) => other is _Key && other.type == type && other.key == key;
  int get hashCode => 373 * 37 * type.hashCode + key.hashCode;
23 24 25 26 27
  String toString() => "_Key(type: $type, key: $key)";
}

typedef void LayoutChangedCallback();

28 29
class MixedViewportLayoutState {
  MixedViewportLayoutState()
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
    : _childOffsets = <double>[0.0],
      _firstVisibleChildIndex = 0,
      _visibleChildCount = 0,
      _didReachLastChild = false
  {
    _readOnlyChildOffsets = new UnmodifiableListView<double>(_childOffsets);
  }

  bool _dirty = true;

  int _firstVisibleChildIndex;
  int get firstVisibleChildIndex => _firstVisibleChildIndex;

  int _visibleChildCount;
  int get visibleChildCount => _visibleChildCount;

  // childOffsets contains the offsets of each child from the top of the
  // list up to the last one we've ever created, and the offset of the
  // end of the last one. If there are no children, then the only offset
  // is 0.0.
  List<double> _childOffsets;
  UnmodifiableListView<double> _readOnlyChildOffsets;
  UnmodifiableListView<double> get childOffsets => _readOnlyChildOffsets;
  double get contentsSize => _childOffsets.last;

  bool _didReachLastChild;
  bool get didReachLastChild => _didReachLastChild;

  Set<int> _invalidIndices = new Set<int>();
  bool get isValid => _invalidIndices.length == 0;
  // Notify the BlockViewport that the children at indices have either
  // changed size and/or changed type.
  void invalidate(Iterable<int> indices) {
    _invalidIndices.addAll(indices);
  }

  final List<Function> _listeners = new List<Function>();
  void addListener(Function listener) {
    _listeners.add(listener);
  }
  void removeListener(Function listener) {
    _listeners.remove(listener);
  }
  void _notifyListeners() {
    List<Function> localListeners = new List<Function>.from(_listeners);
    for (Function listener in localListeners)
      listener();
  }
78 79
}

80
class MixedViewport extends RenderObjectWrapper {
81 82 83
  MixedViewport({ this.startOffset, this.direction: ScrollDirection.vertical, this.builder, this.token, MixedViewportLayoutState layoutState })
    : layoutState = layoutState, super(key: new ObjectKey(layoutState)) {
    // using the layout state as the key is important to prevent us from being synced with someone with a different layout state
84 85
    assert(this.layoutState != null);
  }
86 87

  double startOffset;
88 89
  ScrollDirection direction;
  IndexedBuilder builder;
90
  Object token;
91
  MixedViewportLayoutState layoutState;
92

93 94
  Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>();

95
  RenderBlockViewport get renderObject => super.renderObject;
96 97

  RenderBlockViewport createNode() {
98 99
    // we don't pass the direction or offset to the render object when we
    // create it, because the render object is empty so it will not matter
100 101
    RenderBlockViewport result = new RenderBlockViewport();
    result.callback = layout;
102 103 104
    result.totalExtentCallback = _noIntrinsicExtent;
    result.maxCrossAxisExtentCallback = _noIntrinsicExtent;
    result.minCrossAxisExtentCallback = _noIntrinsicExtent;
105 106 107 108 109 110
    return result;
  }

  void remove() {
    renderObject.callback = null;
    renderObject.totalExtentCallback = null;
111 112
    renderObject.maxCrossAxisExtentCallback = null;
    renderObject.minCrossAxisExtentCallback = null;
113 114 115 116
    super.remove();
    _childrenByKey.clear();
    layoutState._dirty = true;
  }
117 118

  void walkChildren(WidgetTreeWalker walker) {
119
    for (Widget child in _childrenByKey.values)
120 121 122 123 124
      walker(child);
  }

  static const _omit = const Object(); // used as a slot when it's not yet time to attach the child

Adam Barth's avatar
Adam Barth committed
125
  void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
126 127
    if (slot == _omit)
      return;
128
    final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
129
    assert(slot == null || slot is RenderObject);
130 131 132
    assert(renderObject is ContainerRenderObjectMixin);
    renderObject.add(child.renderObject, before: slot);
    assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
133 134
  }

Adam Barth's avatar
Adam Barth committed
135
  void detachChildRenderObject(RenderObjectWrapper child) {
136 137 138
    final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
    assert(renderObject is ContainerRenderObjectMixin);
    if (child.renderObject.parent != renderObject)
139
      return; // probably had slot == _omit when inserted
140 141
    renderObject.remove(child.renderObject);
    assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
142 143
  }

144
  double _noIntrinsicExtent(BoxConstraints constraints) {
145 146 147 148 149 150 151 152 153
    assert(() {
      'MixedViewport does not support returning intrinsic dimensions. ' +
      'Calculating the intrinsic dimensions would require walking the entire child list, ' +
      'which defeats the entire point of having a lazily-built list of children.';
      return false;
    });
    return null;
  }

154
  int _findIndexForOffsetBeforeOrAt(double offset) {
155
    final List<double> offsets = layoutState._childOffsets;
156
    int left = 0;
157
    int right = offsets.length - 1;
158 159
    while (right >= left) {
      int middle = left + ((right - left) ~/ 2);
160
      if (offsets[middle] < offset) {
161
        left = middle + 1;
162
      } else if (offsets[middle] > offset) {
163 164 165 166 167 168 169 170
        right = middle - 1;
      } else {
        return middle;
      }
    }
    return right;
  }

171
  bool retainStatefulNodeIfPossible(MixedViewport newNode) {
172
    assert(layoutState == newNode.layoutState);
173 174
    retainStatefulRenderObjectWrapper(newNode);
    if (startOffset != newNode.startOffset) {
175
      layoutState._dirty = true;
176 177
      startOffset = newNode.startOffset;
    }
178
    if (direction != newNode.direction || builder != newNode.builder || token != newNode.token) {
179 180 181 182
      layoutState._dirty = true;
      layoutState._didReachLastChild = false;
      layoutState._childOffsets = <double>[0.0];
      layoutState._invalidIndices = new Set<int>();
183 184 185
      direction = newNode.direction;
      builder = newNode.builder;
      token = newNode.token;
186 187 188 189
    }
    return true;
  }

190
  void syncRenderObject(MixedViewport old) {
191
    super.syncRenderObject(old);
192
    if (layoutState._dirty || !layoutState.isValid) {
193
      renderObject.markNeedsLayout();
194
    } else {
195 196
      if (layoutState._visibleChildCount > 0) {
        assert(layoutState.firstVisibleChildIndex >= 0);
197
        assert(builder != null);
198
        assert(renderObject != null);
199 200 201
        final int startIndex = layoutState._firstVisibleChildIndex;
        int lastIndex = startIndex + layoutState._visibleChildCount - 1;
        for (int index = startIndex; index <= lastIndex; index += 1) {
202 203 204
          Widget widget = builder(index);
          assert(widget != null);
          assert(widget.key != null);
205
          assert(widget.isFromOldGeneration);
206
          _Key key = new _Key.fromWidget(widget);
207
          Widget oldWidget = _childrenByKey[key];
208
          assert(oldWidget != null);
209 210 211 212 213
          assert(() {
            'One of the nodes that was in this MixedViewport was placed in another part of the tree, without the MixedViewport\'s token or builder being changed ' +
            'and without the MixedViewport\'s MixedViewportLayoutState object being told about that any of the children were invalid.';
            return oldWidget.isFromOldGeneration;
          });
214 215
          assert(oldWidget.renderObject.parent == renderObject);
          widget = syncChild(widget, oldWidget, renderObject.childAfter(oldWidget.renderObject));
216
          assert(widget != null);
217
          _childrenByKey[key] = widget;
218 219 220 221 222
        }
      }
    }
  }

223 224 225 226 227 228 229 230 231 232 233 234
  // Build the widget at index, and use its maxIntrinsicHeight to fix up
  // the offsets from index+1 to endIndex. Return the newWidget.
  Widget _getWidgetAndRecomputeOffsets(int index, int endIndex, BoxConstraints innerConstraints) {
    final List<double> offsets = layoutState._childOffsets;
    // Create the newWidget at index.
    assert(index >= 0);
    assert(endIndex > index);
    assert(endIndex < offsets.length);
    assert(builder != null);
    Widget newWidget = builder(index);
    assert(newWidget != null);
    assert(newWidget.key != null);
235
    assert(newWidget.isFromOldGeneration);
236
    final _Key key = new _Key.fromWidget(newWidget);
237
    Widget oldWidget = _childrenByKey[key];
238 239
    if (oldWidget != null && !oldWidget.isFromOldGeneration)
      oldWidget = null;
240 241
    newWidget = syncChild(newWidget, oldWidget, _omit);
    assert(newWidget != null);
242
    // Update the offsets based on the newWidget's dimensions.
243
    RenderBox widgetRoot = newWidget.renderObject;
244
    assert(widgetRoot is RenderBox);
245 246 247 248 249 250 251 252
    double newOffset;
    if (direction == ScrollDirection.vertical) {
      newOffset = widgetRoot.getMaxIntrinsicHeight(innerConstraints);
    } else {
      newOffset = widgetRoot.getMaxIntrinsicWidth(innerConstraints);
    }
    double oldOffset = offsets[index + 1] - offsets[index];
    double offsetDelta = newOffset - oldOffset;
253
    for (int i = index + 1; i <= endIndex; i++)
254
      offsets[i] += offsetDelta;
255 256 257
    return newWidget;
  }

258
  Widget _getWidget(int index, BoxConstraints innerConstraints) {
259 260 261 262 263 264 265
    final List<double> offsets = layoutState._childOffsets;
    assert(index >= 0);
    Widget widget = builder == null ? null : builder(index);
    if (widget == null)
      return null;
    assert(widget.key != null); // items in lists must have keys
    final _Key key = new _Key.fromWidget(widget);
266
    Widget oldWidget = _childrenByKey[key];
267 268
    if (oldWidget != null && !oldWidget.isFromOldGeneration)
      oldWidget = null;
269 270 271 272
    widget = syncChild(widget, oldWidget, _omit);
    if (index >= offsets.length - 1) {
      assert(index == offsets.length - 1);
      final double widgetStartOffset = offsets[index];
273
      RenderBox widgetRoot = widget.renderObject;
274
      assert(widgetRoot is RenderBox);
275 276 277 278 279 280
      double widgetEndOffset;
      if (direction == ScrollDirection.vertical) {
        widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints);
      } else {
        widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicWidth(innerConstraints);
      }
281 282 283 284 285 286 287 288 289 290
      offsets.add(widgetEndOffset);
    }
    return widget;
  }

  void layout(BoxConstraints constraints) {
    if (!layoutState._dirty && layoutState.isValid)
      return;
    layoutState._dirty = false;

291 292
    LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder();
    try {
293
      _doLayout(constraints);
294 295 296 297
    } finally {
      exitLayoutCallbackBuilder(handle);
    }

298 299
    layoutState._notifyListeners();
  }
300

301 302 303 304 305 306 307 308 309 310 311 312
  void _unsyncChild(Widget widget) {
    assert(!widget.isFromOldGeneration);
    // The following two lines are the equivalent of "syncChild(null,
    // widget, null)", but actually doing that wouldn't work because
    // widget is now from the new generation and so syncChild() would
    // assume that that means someone else has already sync()ed with it
    // and that it's wanted. But it's not wanted! We want to get rid of
    // it. So we do it manually.
    widget.detachRenderObject();
    widget.remove();
  }

313
  void _doLayout(BoxConstraints constraints) {
314 315 316
    Map<_Key, Widget> newChildren = new Map<_Key, Widget>();
    Map<int, Widget> builtChildren = new Map<int, Widget>();

317
    final List<double> offsets = layoutState._childOffsets;
318
    final Map<_Key, Widget> childrenByKey = _childrenByKey;
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
    double extent;
    if (direction == ScrollDirection.vertical) {
      extent = constraints.maxHeight;
      assert(extent < double.INFINITY &&
        'There is no point putting a lazily-built vertical MixedViewport inside a box with infinite internal ' +
        'height (e.g. inside something else that scrolls vertically), because it would then just eagerly build ' +
        'all the children. You probably want to put the MixedViewport inside a Container with a fixed height.' is String);
    } else {
      extent = constraints.maxWidth;
      assert(extent < double.INFINITY &&
        'There is no point putting a lazily-built horizontal MixedViewport inside a box with infinite internal ' +
        'width (e.g. inside something else that scrolls horizontally), because it would then just eagerly build ' +
        'all the children. You probably want to put the MixedViewport inside a Container with a fixed width.' is String);
    }
    final double endOffset = startOffset + extent;
334

335 336 337 338 339 340
    BoxConstraints innerConstraints;
    if (direction == ScrollDirection.vertical) {
      innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
    } else {
      innerConstraints = new BoxConstraints.tightFor(height: constraints.constrainHeight());
    }
341

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    // Before doing the actual layout, fix the offsets for the widgets
    // whose size or type has changed.
    if (!layoutState.isValid && offsets.length > 0) {
      List<int> invalidIndices = layoutState._invalidIndices.toList();
      invalidIndices.sort();
      // Ensure all of the offsets after invalidIndices[0] are updated.
      if (invalidIndices.last < offsets.length - 1)
        invalidIndices.add(offsets.length - 1);
      for (int i = 0; i < invalidIndices.length - 1; i += 1) {
        int index = invalidIndices[i];
        int endIndex = invalidIndices[i + 1];
        Widget widget = _getWidgetAndRecomputeOffsets(index, endIndex, innerConstraints);
        _Key widgetKey = new _Key.fromWidget(widget);
        bool isVisible = offsets[index] < endOffset && offsets[index + 1] >= startOffset;
        if (isVisible) {
          newChildren[widgetKey] = widget;
          builtChildren[index] = widget;
        } else {
          childrenByKey.remove(widgetKey);
361
          _unsyncChild(widget);
362 363 364 365 366
        }
      }
    }
    layoutState._invalidIndices.clear();

367 368 369 370
    int startIndex;
    bool haveChildren;
    if (startOffset <= 0.0) {
      startIndex = 0;
371
      if (offsets.length > 1) {
372 373 374 375 376 377 378 379 380
        haveChildren = true;
      } else {
        Widget widget = _getWidget(startIndex, innerConstraints);
        if (widget != null) {
          newChildren[new _Key.fromWidget(widget)] = widget;
          builtChildren[startIndex] = widget;
          haveChildren = true;
        } else {
          haveChildren = false;
381
          layoutState._didReachLastChild = true;
382 383 384 385
        }
      }
    } else {
      startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
386
      if (startIndex == offsets.length - 1) {
387
        // We don't have an offset on the list that is beyond the start offset.
388
        assert(offsets.last <= startOffset);
389 390 391 392 393
        // Fill the list until this isn't true or until we know that the
        // list is complete (and thus we are overscrolled).
        while (true) {
          Widget widget = _getWidget(startIndex, innerConstraints);
          if (widget == null) {
394
            layoutState._didReachLastChild = true;
395 396 397
            break;
          }
          _Key widgetKey = new _Key.fromWidget(widget);
398
          if (offsets.last > startOffset) {
399
            // it's visible
400 401 402 403
            newChildren[widgetKey] = widget;
            builtChildren[startIndex] = widget;
            break;
          }
404
          childrenByKey.remove(widgetKey);
405
          _unsyncChild(widget);
406
          startIndex += 1;
407
          assert(startIndex == offsets.length - 1);
408
        }
409
        if (offsets.last > startOffset) {
410 411 412
          // If we're here, we have at least one child, so our list has
          // at least two offsets, the top of the child and the bottom
          // of the child.
413 414
          assert(offsets.length >= 2);
          assert(startIndex == offsets.length - 2);
415 416 417 418 419 420 421 422 423 424
          haveChildren = true;
        } else {
          // If we're here, there are no children to show.
          haveChildren = false;
        }
      } else {
        haveChildren = true;
      }
    }
    assert(haveChildren != null);
425
    assert(haveChildren || layoutState._didReachLastChild);
426 427

    assert(startIndex >= 0);
428
    assert(startIndex < offsets.length);
429 430 431

    int index = startIndex;
    if (haveChildren) {
432 433 434 435 436 437
      // Update the renderObject configuration
      if (direction == ScrollDirection.vertical) {
        renderObject.direction = BlockDirection.vertical;
      } else {
        renderObject.direction = BlockDirection.horizontal;
      }
438
      renderObject.startOffset = offsets[index] - startOffset;
439
      // Build all the widgets we still need.
440
      while (offsets[index] < endOffset) {
441 442 443
        if (!builtChildren.containsKey(index)) {
          Widget widget = _getWidget(index, innerConstraints);
          if (widget == null) {
444
            layoutState._didReachLastChild = true;
445 446 447 448 449 450 451 452 453 454 455
            break;
          }
          newChildren[new _Key.fromWidget(widget)] = widget;
          builtChildren[index] = widget;
        }
        assert(builtChildren[index] != null);
        index += 1;
      }
    }

    // Remove any old children.
456
    for (_Key oldChildKey in childrenByKey.keys) {
457
      if (!newChildren.containsKey(oldChildKey))
Adam Barth's avatar
Adam Barth committed
458
        syncChild(null, childrenByKey[oldChildKey], null); // calls detachChildRenderObject()
459 460 461 462 463 464 465 466 467 468
    }

    if (haveChildren) {
      // Place all our children in our RenderObject.
      // All the children we are placing are in builtChildren and newChildren.
      // We will walk them backwards so we can set the siblings at the same time.
      RenderBox nextSibling = null;
      while (index > startIndex) {
        index -= 1;
        Widget widget = builtChildren[index];
469 470
        if (widget.renderObject.parent == renderObject) {
          renderObject.move(widget.renderObject, before: nextSibling);
471
        } else {
472 473
          assert(widget.renderObject.parent == null);
          renderObject.add(widget.renderObject, before: nextSibling);
474 475
        }
        widget.updateSlot(nextSibling);
476
        nextSibling = widget.renderObject;
477 478 479
      }
    }

480
    _childrenByKey = newChildren;
481 482
    layoutState._firstVisibleChildIndex = startIndex;
    layoutState._visibleChildCount = newChildren.length;
483 484 485
  }

}