| // 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'; |
| |
| import 'package:sky/rendering/block.dart'; |
| import 'package:sky/rendering/box.dart'; |
| import 'package:sky/rendering/object.dart'; |
| import 'package:sky/widgets/framework.dart'; |
| |
| // 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; |
| final Key key; |
| bool operator ==(other) => other is _Key && other.type == type && other.key == key; |
| int get hashCode => 373 * 37 * type.hashCode + key.hashCode; |
| String toString() => "_Key(type: $type, key: $key)"; |
| } |
| |
| typedef void LayoutChangedCallback(); |
| |
| class MixedViewportLayoutState { |
| MixedViewportLayoutState() |
| : _childOffsets = <double>[0.0], |
| _firstVisibleChildIndex = 0, |
| _visibleChildCount = 0, |
| _didReachLastChild = false |
| { |
| _readOnlyChildOffsets = new UnmodifiableListView<double>(_childOffsets); |
| } |
| |
| Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>(); |
| 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(); |
| } |
| } |
| |
| class MixedViewport extends RenderObjectWrapper { |
| MixedViewport({ this.builder, this.startOffset, this.token, this.layoutState, Key key }) |
| : super(key: key) { |
| assert(this.layoutState != null); |
| } |
| |
| IndexedBuilder builder; |
| double startOffset; |
| Object token; |
| MixedViewportLayoutState layoutState; |
| |
| RenderBlockViewport get renderObject => super.renderObject; |
| RenderBlockViewport createNode() => new RenderBlockViewport(); |
| |
| void walkChildren(WidgetTreeWalker walker) { |
| for (Widget child in layoutState._childrenByKey.values) |
| walker(child); |
| } |
| |
| static const _omit = const Object(); // used as a slot when it's not yet time to attach the child |
| |
| void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) { |
| if (slot == _omit) |
| return; |
| final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer |
| assert(slot == null || slot is RenderObject); |
| assert(renderObject is ContainerRenderObjectMixin); |
| renderObject.add(child.renderObject, before: slot); |
| assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer |
| } |
| |
| void detachChildRenderObject(RenderObjectWrapper child) { |
| final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer |
| assert(renderObject is ContainerRenderObjectMixin); |
| if (child.renderObject.parent != renderObject) |
| return; // probably had slot == _omit when inserted |
| renderObject.remove(child.renderObject); |
| assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer |
| } |
| |
| void didMount() { |
| renderObject.callback = layout; |
| super.didMount(); |
| } |
| |
| void didUnmount() { |
| renderObject.callback = null; |
| super.didUnmount(); |
| } |
| |
| int _findIndexForOffsetBeforeOrAt(double offset) { |
| final List<double> offsets = layoutState._childOffsets; |
| int left = 0; |
| int right = offsets.length - 1; |
| while (right >= left) { |
| int middle = left + ((right - left) ~/ 2); |
| if (offsets[middle] < offset) { |
| left = middle + 1; |
| } else if (offsets[middle] > offset) { |
| right = middle - 1; |
| } else { |
| return middle; |
| } |
| } |
| return right; |
| } |
| |
| bool retainStatefulNodeIfPossible(MixedViewport newNode) { |
| assert(layoutState == newNode.layoutState); |
| retainStatefulRenderObjectWrapper(newNode); |
| if (startOffset != newNode.startOffset) { |
| layoutState._dirty = true; |
| startOffset = newNode.startOffset; |
| } |
| if (token != newNode.token || builder != newNode.builder) { |
| layoutState._dirty = true; |
| builder = newNode.builder; |
| token = newNode.token; |
| layoutState._didReachLastChild = false; |
| layoutState._childOffsets = <double>[0.0]; |
| layoutState._invalidIndices = new Set<int>(); |
| } |
| return true; |
| } |
| |
| void syncRenderObject(MixedViewport old) { |
| super.syncRenderObject(old); |
| if (layoutState._dirty || !layoutState.isValid) { |
| renderObject.markNeedsLayout(); |
| } else { |
| if (layoutState._visibleChildCount > 0) { |
| assert(layoutState.firstVisibleChildIndex >= 0); |
| assert(builder != null); |
| assert(renderObject != null); |
| final int startIndex = layoutState._firstVisibleChildIndex; |
| int lastIndex = startIndex + layoutState._visibleChildCount - 1; |
| for (int index = startIndex; index <= lastIndex; index += 1) { |
| Widget widget = builder(index); |
| assert(widget != null); |
| assert(widget.key != null); |
| _Key key = new _Key.fromWidget(widget); |
| Widget oldWidget = layoutState._childrenByKey[key]; |
| assert(oldWidget != null); |
| assert(oldWidget.renderObject.parent == renderObject); |
| widget = syncChild(widget, oldWidget, renderObject.childAfter(oldWidget.renderObject)); |
| assert(widget != null); |
| layoutState._childrenByKey[key] = widget; |
| } |
| } |
| } |
| } |
| |
| // 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); |
| final _Key key = new _Key.fromWidget(newWidget); |
| Widget oldWidget = layoutState._childrenByKey[key]; |
| newWidget = syncChild(newWidget, oldWidget, _omit); |
| assert(newWidget != null); |
| // Update the offsets based on the newWidget's height. |
| RenderBox widgetRoot = newWidget.renderObject; |
| assert(widgetRoot is RenderBox); |
| double newHeight = widgetRoot.getMaxIntrinsicHeight(innerConstraints); |
| double oldHeight = offsets[index + 1] - offsets[index]; |
| double heightDelta = newHeight - oldHeight; |
| for (int i = index + 1; i <= endIndex; i++) |
| offsets[i] += heightDelta; |
| return newWidget; |
| } |
| |
| Widget _getWidget(int index, BoxConstraints innerConstraints) { |
| 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); |
| Widget oldWidget = layoutState._childrenByKey[key]; |
| widget = syncChild(widget, oldWidget, _omit); |
| if (index >= offsets.length - 1) { |
| assert(index == offsets.length - 1); |
| final double widgetStartOffset = offsets[index]; |
| RenderBox widgetRoot = widget.renderObject; |
| assert(widgetRoot is RenderBox); |
| final double widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints); |
| offsets.add(widgetEndOffset); |
| } |
| return widget; |
| } |
| |
| void layout(BoxConstraints constraints) { |
| if (!layoutState._dirty && layoutState.isValid) |
| return; |
| layoutState._dirty = false; |
| |
| LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder(); |
| try { |
| _doLayout(constraints); |
| } finally { |
| exitLayoutCallbackBuilder(handle); |
| } |
| |
| layoutState._notifyListeners(); |
| } |
| |
| void _doLayout(BoxConstraints constraints) { |
| Map<_Key, Widget> newChildren = new Map<_Key, Widget>(); |
| Map<int, Widget> builtChildren = new Map<int, Widget>(); |
| |
| final List<double> offsets = layoutState._childOffsets; |
| final Map<_Key, Widget> childrenByKey = layoutState._childrenByKey; |
| final double height = renderObject.size.height; |
| final double endOffset = startOffset + height; |
| BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth()); |
| |
| // 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); |
| syncChild(null, widget, null); |
| } |
| } |
| } |
| layoutState._invalidIndices.clear(); |
| |
| int startIndex; |
| bool haveChildren; |
| if (startOffset <= 0.0) { |
| startIndex = 0; |
| if (offsets.length > 1) { |
| 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; |
| layoutState._didReachLastChild = true; |
| } |
| } |
| } else { |
| startIndex = _findIndexForOffsetBeforeOrAt(startOffset); |
| if (startIndex == offsets.length - 1) { |
| // We don't have an offset on the list that is beyond the start offset. |
| assert(offsets.last <= startOffset); |
| // 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) { |
| layoutState._didReachLastChild = true; |
| break; |
| } |
| _Key widgetKey = new _Key.fromWidget(widget); |
| if (offsets.last > startOffset) { |
| newChildren[widgetKey] = widget; |
| builtChildren[startIndex] = widget; |
| break; |
| } |
| if (!childrenByKey.containsKey(widgetKey)) { |
| // we don't actually need this one, release it |
| syncChild(null, widget, null); |
| } // else we'll get rid of it later, when we remove old children |
| startIndex += 1; |
| assert(startIndex == offsets.length - 1); |
| } |
| if (offsets.last > startOffset) { |
| // 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. |
| assert(offsets.length >= 2); |
| assert(startIndex == offsets.length - 2); |
| haveChildren = true; |
| } else { |
| // If we're here, there are no children to show. |
| haveChildren = false; |
| } |
| } else { |
| haveChildren = true; |
| } |
| } |
| assert(haveChildren != null); |
| assert(haveChildren || layoutState._didReachLastChild); |
| |
| assert(startIndex >= 0); |
| assert(startIndex < offsets.length); |
| |
| int index = startIndex; |
| if (haveChildren) { |
| // Build all the widgets we need. |
| renderObject.startOffset = offsets[index] - startOffset; |
| while (offsets[index] < endOffset) { |
| if (!builtChildren.containsKey(index)) { |
| Widget widget = _getWidget(index, innerConstraints); |
| if (widget == null) { |
| layoutState._didReachLastChild = true; |
| break; |
| } |
| newChildren[new _Key.fromWidget(widget)] = widget; |
| builtChildren[index] = widget; |
| } |
| assert(builtChildren[index] != null); |
| index += 1; |
| } |
| } |
| |
| // Remove any old children. |
| for (_Key oldChildKey in childrenByKey.keys) { |
| if (!newChildren.containsKey(oldChildKey)) |
| syncChild(null, childrenByKey[oldChildKey], null); // calls detachChildRenderObject() |
| } |
| |
| 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]; |
| if (widget.renderObject.parent == renderObject) { |
| renderObject.move(widget.renderObject, before: nextSibling); |
| } else { |
| assert(widget.renderObject.parent == null); |
| renderObject.add(widget.renderObject, before: nextSibling); |
| } |
| widget.updateSlot(nextSibling); |
| nextSibling = widget.renderObject; |
| } |
| } |
| |
| layoutState._childrenByKey = newChildren; |
| layoutState._firstVisibleChildIndex = startIndex; |
| layoutState._visibleChildCount = newChildren.length; |
| } |
| |
| } |