// 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:math' as math; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'package:flutter/rendering.dart'; /// 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). typedef void ExtentsChangedCallback(double contentExtent, double containerExtent); /// An abstract widget whose children are not all materialized. abstract class VirtualViewport extends RenderObjectWidget { /// The offset from the [ViewportAnchor] at which the viewport should start painting children. double get startOffset; _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); } /// 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. abstract class VirtualViewportElement extends RenderObjectElement { VirtualViewportElement(VirtualViewport widget) : super(widget); @override VirtualViewport get widget => super.widget; /// The index of the first child to materialize. int get materializedChildBase; /// The number of children to materializes. int get materializedChildCount; /// The least offset for which [materializedChildBase] and [materializedChildCount] are valid. double get startOffsetBase; /// The greatest offset for which [materializedChildBase] and [materializedChildCount] are valid. double get startOffsetLimit; /// Returns the pixel offset for a scroll offset, accounting for the scroll /// anchor. double scrollOffsetToPixelOffset(double scrollOffset) { switch (renderObject.anchor) { 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.mainAxis) { case Axis.horizontal: return new Offset(scrollOffsetToPixelOffset(scrollOffset), 0.0); case Axis.vertical: return new Offset(0.0, scrollOffsetToPixelOffset(scrollOffset)); } } List<Element> _materializedChildren = const <Element>[]; @override RenderVirtualViewport<ContainerBoxParentDataMixin<RenderBox>> get renderObject => super.renderObject; @override void visitChildren(ElementVisitor visitor) { if (_materializedChildren == null) return; for (Element child in _materializedChildren) visitor(child); } _WidgetProvider _widgetProvider; @override void mount(Element parent, dynamic newSlot) { _widgetProvider = widget._createWidgetProvider(); _widgetProvider.didUpdateWidget(null, widget); super.mount(parent, newSlot); renderObject.callback = layout; updateRenderObject(null); } @override void unmount() { renderObject.callback = null; super.unmount(); } @override void update(VirtualViewport newWidget) { VirtualViewport oldWidget = widget; _widgetProvider.didUpdateWidget(oldWidget, newWidget); super.update(newWidget); updateRenderObject(oldWidget); if (!renderObject.needsLayout) _materializeChildren(); } void _updatePaintOffset() { renderObject.paintOffset = scrollOffsetToPixelDelta(widget.startOffset - startOffsetBase); } void updateRenderObject(VirtualViewport oldWidget) { renderObject.virtualChildCount = _widgetProvider.virtualChildCount; if (startOffsetBase != null) { _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) { final double startOffset = widget.startOffset; bool shouldLayout = false; if (startOffsetBase != null) { if (startOffset < startOffsetBase) shouldLayout = true; else if (startOffset == startOffsetBase && oldWidget?.startOffset != startOffsetBase) shouldLayout = true; } if (startOffsetLimit != null) { if (startOffset > startOffsetLimit) shouldLayout = true; else if (startOffset == startOffsetLimit && oldWidget?.startOffset != startOffsetLimit) shouldLayout = true; } if (shouldLayout) renderObject.markNeedsLayout(); } } } /// 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. void layout(BoxConstraints constraints) { assert(startOffsetBase != null); assert(startOffsetLimit != null); _updatePaintOffset(); owner.lockState(_materializeChildren, building: true); } void _materializeChildren() { int base = materializedChildBase; int count = materializedChildCount; assert(base != null); assert(count != null); _widgetProvider.prepareChildren(this, base, count); List<Widget> newWidgets = new List<Widget>(count); for (int i = 0; i < count; ++i) { int childIndex = base + i; Widget child = _widgetProvider.getChild(childIndex); newWidgets[i] = new RepaintBoundary.wrap(child, childIndex); } assert(!debugChildrenHaveDuplicateKeys(widget, newWidgets)); _materializedChildren = updateChildren(_materializedChildren, newWidgets.toList()); } @override void insertChildRenderObject(RenderObject child, Element slot) { renderObject.insert(child, after: slot?.renderObject); } @override void moveChildRenderObject(RenderObject child, Element slot) { assert(child.parent == renderObject); renderObject.move(child, after: slot?.renderObject); } @override void removeChildRenderObject(RenderObject child) { assert(child.parent == renderObject); renderObject.remove(child); } } /// 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. Iterable<Widget> get children; @override _IterableWidgetProvider _createWidgetProvider() => new _IterableWidgetProvider(); } class _IterableWidgetProvider extends _WidgetProvider { int _length; Iterator<Widget> _iterator; List<Widget> _widgets; @override void didUpdateWidget(VirtualViewportFromIterable oldWidget, VirtualViewportFromIterable newWidget) { if (oldWidget == null || newWidget.children != oldWidget.children) { _iterator = null; _widgets = <Widget>[]; _length = newWidget.children.length; } } @override int get virtualChildCount => _length; @override void prepareChildren(VirtualViewportElement context, int base, int count) { int limit = base < 0 ? _length : math.min(_length, base + count); if (limit <= _widgets.length) return; VirtualViewportFromIterable 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); } } @override Widget getChild(int i) => _widgets[(i % _length).abs()]; } /// Signature of a callback that returns the sublist of widgets in the given range. typedef List<Widget> ItemListBuilder(BuildContext context, int start, int count); /// 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. int get itemCount; /// 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). ItemListBuilder get itemBuilder; @override _LazyWidgetProvider _createWidgetProvider() => new _LazyWidgetProvider(); } class _LazyWidgetProvider extends _WidgetProvider { int _length; int _base; List<Widget> _widgets; @override void didUpdateWidget(VirtualViewportFromBuilder oldWidget, VirtualViewportFromBuilder newWidget) { // 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. if (_length != newWidget.itemCount || oldWidget?.itemBuilder != newWidget.itemBuilder) { _length = newWidget.itemCount; _base = null; _widgets = null; } } @override int get virtualChildCount => _length; @override void prepareChildren(VirtualViewportElement context, int base, int count) { if (_widgets != null && _widgets.length == count && _base == base) return; VirtualViewportFromBuilder widget = context.widget; _base = base; _widgets = widget.itemBuilder(context, base, count); } @override Widget getChild(int i) { final int childCount = virtualChildCount; final int index = childCount != null ? (i % childCount).abs() : i; return _widgets[index - _base]; } }