mixed_viewport.dart 18.1 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
  MixedViewport({ Key key, this.startOffset, this.direction: ScrollDirection.vertical, this.builder, this.token, this.layoutState })
82 83 84
    : super(key: key) {
    assert(this.layoutState != null);
  }
85 86

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

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

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

  RenderBlockViewport createNode() {
97 98
    // 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
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    RenderBlockViewport result = new RenderBlockViewport();
    result.callback = layout;
    result.totalExtentCallback = _noIntrinsicDimensions;
    result.maxCrossAxisDimensionCallback = _noIntrinsicDimensions;
    result.minCrossAxisDimensionCallback = _noIntrinsicDimensions;
    return result;
  }

  void remove() {
    renderObject.callback = null;
    renderObject.totalExtentCallback = null;
    renderObject.maxCrossAxisDimensionCallback = null;
    renderObject.minCrossAxisDimensionCallback = null;
    super.remove();
    _childrenByKey.clear();
    layoutState._dirty = true;
  }
116 117

  void walkChildren(WidgetTreeWalker walker) {
118
    for (Widget child in _childrenByKey.values)
119 120 121 122 123
      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
124
  void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
125 126
    if (slot == _omit)
      return;
127
    final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
128
    assert(slot == null || slot is RenderObject);
129 130 131
    assert(renderObject is ContainerRenderObjectMixin);
    renderObject.add(child.renderObject, before: slot);
    assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
132 133
  }

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

143 144 145 146 147 148 149 150 151 152
  double _noIntrinsicDimensions(BoxConstraints constraints) {
    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;
  }

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

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

189
  void syncRenderObject(MixedViewport old) {
190
    super.syncRenderObject(old);
191
    if (layoutState._dirty || !layoutState.isValid) {
192
      renderObject.markNeedsLayout();
193
    } else {
194 195
      if (layoutState._visibleChildCount > 0) {
        assert(layoutState.firstVisibleChildIndex >= 0);
196
        assert(builder != null);
197
        assert(renderObject != null);
198 199 200
        final int startIndex = layoutState._firstVisibleChildIndex;
        int lastIndex = startIndex + layoutState._visibleChildCount - 1;
        for (int index = startIndex; index <= lastIndex; index += 1) {
201 202 203
          Widget widget = builder(index);
          assert(widget != null);
          assert(widget.key != null);
204
          assert(widget.isFromOldGeneration);
205
          _Key key = new _Key.fromWidget(widget);
206
          Widget oldWidget = _childrenByKey[key];
207
          assert(oldWidget != null);
208 209 210 211 212
          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;
          });
213 214
          assert(oldWidget.renderObject.parent == renderObject);
          widget = syncChild(widget, oldWidget, renderObject.childAfter(oldWidget.renderObject));
215
          assert(widget != null);
216
          _childrenByKey[key] = widget;
217 218 219 220 221
        }
      }
    }
  }

222 223 224 225 226 227 228 229 230 231 232 233
  // 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);
234
    assert(newWidget.isFromOldGeneration);
235
    final _Key key = new _Key.fromWidget(newWidget);
236
    Widget oldWidget = _childrenByKey[key];
237 238
    if (oldWidget != null && !oldWidget.isFromOldGeneration)
      oldWidget = null;
239 240
    newWidget = syncChild(newWidget, oldWidget, _omit);
    assert(newWidget != null);
241
    // Update the offsets based on the newWidget's dimensions.
242
    RenderBox widgetRoot = newWidget.renderObject;
243
    assert(widgetRoot is RenderBox);
244 245 246 247 248 249 250 251
    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;
252
    for (int i = index + 1; i <= endIndex; i++)
253
      offsets[i] += offsetDelta;
254 255 256
    return newWidget;
  }

257
  Widget _getWidget(int index, BoxConstraints innerConstraints) {
258 259 260 261 262 263 264
    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);
265
    Widget oldWidget = _childrenByKey[key];
266 267
    if (oldWidget != null && !oldWidget.isFromOldGeneration)
      oldWidget = null;
268 269 270 271
    widget = syncChild(widget, oldWidget, _omit);
    if (index >= offsets.length - 1) {
      assert(index == offsets.length - 1);
      final double widgetStartOffset = offsets[index];
272
      RenderBox widgetRoot = widget.renderObject;
273
      assert(widgetRoot is RenderBox);
274 275 276 277 278 279
      double widgetEndOffset;
      if (direction == ScrollDirection.vertical) {
        widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints);
      } else {
        widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicWidth(innerConstraints);
      }
280 281 282 283 284 285 286 287 288 289
      offsets.add(widgetEndOffset);
    }
    return widget;
  }

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

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

297 298
    layoutState._notifyListeners();
  }
299

300 301 302 303 304 305 306 307 308 309 310 311
  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();
  }

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

316
    final List<double> offsets = layoutState._childOffsets;
317
    final Map<_Key, Widget> childrenByKey = _childrenByKey;
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    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;
    
    BoxConstraints innerConstraints;
    if (direction == ScrollDirection.vertical) {
      innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
    } else {
      innerConstraints = new BoxConstraints.tightFor(height: constraints.constrainHeight());
    }
340

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
    // 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);
360
          _unsyncChild(widget);
361 362 363 364 365
        }
      }
    }
    layoutState._invalidIndices.clear();

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

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

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

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

    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];
468 469
        if (widget.renderObject.parent == renderObject) {
          renderObject.move(widget.renderObject, before: nextSibling);
470
        } else {
471 472
          assert(widget.renderObject.parent == null);
          renderObject.add(widget.renderObject, before: nextSibling);
473 474
        }
        widget.updateSlot(nextSibling);
475
        nextSibling = widget.renderObject;
476 477 478
      }
    }

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

}