diff --git a/examples/widgets/media_query.dart b/examples/widgets/media_query.dart index 2a0c915bb6..cde055e73b 100644 --- a/examples/widgets/media_query.dart +++ b/examples/widgets/media_query.dart @@ -72,13 +72,14 @@ class MediaQueryExample extends StatelessComponent { items.add(new AdaptiveItem("Item $i")); if (MediaQuery.of(context).size.width < _gridViewBreakpoint) { - return new Block( - items.map((AdaptiveItem item) => item.toListItem()).toList() + return new ScrollableList2( + itemExtent: 50.0, + children: items.map((AdaptiveItem item) => item.toListItem()).toList() ); } else { return new ScrollableGrid( - children: items.map((AdaptiveItem item) => item.toCard()).toList(), - delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth) + delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth), + children: items.map((AdaptiveItem item) => item.toCard()).toList() ); } } diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index a9d343745b..3e67788a57 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -18,6 +18,7 @@ export 'src/rendering/flex.dart'; export 'src/rendering/grid.dart'; export 'src/rendering/image.dart'; export 'src/rendering/layer.dart'; +export 'src/rendering/list.dart'; export 'src/rendering/node.dart'; export 'src/rendering/object.dart'; export 'src/rendering/overflow.dart'; diff --git a/packages/flutter/lib/src/rendering/grid.dart b/packages/flutter/lib/src/rendering/grid.dart index b2019e9aaa..45388658e1 100644 --- a/packages/flutter/lib/src/rendering/grid.dart +++ b/packages/flutter/lib/src/rendering/grid.dart @@ -6,8 +6,7 @@ import 'dart:typed_data'; import 'box.dart'; import 'object.dart'; - -import 'package:vector_math/vector_math_64.dart'; +import 'viewport.dart'; bool _debugIsMonotonic(List offsets) { bool result = true; @@ -314,8 +313,7 @@ class GridParentData extends ContainerBoxParentDataMixin { /// /// Additionally, grid layout materializes all of its children, which makes it /// most useful for grids containing a moderate number of tiles. -class RenderGrid extends RenderBox with ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { +class RenderGrid extends RenderVirtualViewport { RenderGrid({ List children, GridDelegate delegate, @@ -323,11 +321,11 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin _virtualChildCount ?? childCount; - int _virtualChildCount; - void set virtualChildCount(int value) { - if (_virtualChildCount == value) - return; - _virtualChildCount = value; - markNeedsLayout(); - } - - /// The offset at which to paint the first tile. - /// - /// Note: you can modify this property from within [callback], if necessary. - Offset get paintOffset => _paintOffset; - Offset _paintOffset; - void set paintOffset(Offset value) { - assert(value != null); - if (value == _paintOffset) - return; - _paintOffset = value; - markNeedsPaint(); - } - - /// Called during [layout] to determine the grid's children. - /// - /// Typically the callback will mutate the child list appropriately, for - /// example so the child list contains only visible children. - LayoutCallback get callback => _callback; - LayoutCallback _callback; - void set callback(LayoutCallback value) { - if (value == _callback) - return; - _callback = value; - markNeedsLayout(); - } - void setupParentData(RenderBox child) { if (child.parentData is! GridParentData) child.parentData = new GridParentData(); @@ -447,17 +402,13 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin size.width || gridSize.height > size.height) - _hasVisualOverflow = true; - if (_callback != null) - invokeLayoutCallback(_callback); + if (callback != null) + invokeLayoutCallback(callback); double gridTopPadding = _specification.padding.top; double gridLeftPadding = _specification.padding.left; @@ -492,29 +443,10 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin { } + +class RenderList extends RenderVirtualViewport { + RenderList({ + List children, + double itemExtent, + int virtualChildCount, + Offset paintOffset: Offset.zero, + LayoutCallback callback + }) : _itemExtent = itemExtent, super( + virtualChildCount: virtualChildCount, + paintOffset: paintOffset, + callback: callback + ) { + assert(itemExtent != null); + addAll(children); + } + + double get itemExtent => _itemExtent; + double _itemExtent; + void set itemExtent (double newValue) { + assert(newValue != null); + if (_itemExtent == newValue) + return; + _itemExtent = newValue; + markNeedsLayout(); + } + + void setupParentData(RenderBox child) { + if (child.parentData is! ListParentData) + child.parentData = new ListParentData(); + } + + double get _preferredMainAxisExtent => itemExtent * virtualChildCount; + + double getMinIntrinsicWidth(BoxConstraints constraints) { + assert(constraints.isNormalized); + return constraints.constrainWidth(0.0); + } + + double getMaxIntrinsicWidth(BoxConstraints constraints) { + assert(constraints.isNormalized); + return constraints.constrainWidth(0.0); + } + + double getMinIntrinsicHeight(BoxConstraints constraints) { + assert(constraints.isNormalized); + return constraints.constrainHeight(_preferredMainAxisExtent); + } + + double getMaxIntrinsicHeight(BoxConstraints constraints) { + assert(constraints.isNormalized); + return constraints.constrainHeight(_preferredMainAxisExtent); + } + + void performLayout() { + double height = _preferredMainAxisExtent; + size = new Size(constraints.maxWidth, constraints.constrainHeight(height)); + + if (callback != null) + invokeLayoutCallback(callback); + + BoxConstraints innerConstraints = + new BoxConstraints.tightFor(width: size.width, height: itemExtent); + + int childIndex = 0; + RenderBox child = firstChild; + while (child != null) { + child.layout(innerConstraints); + final ListParentData childParentData = child.parentData; + childParentData.offset = new Offset(0.0, childIndex * itemExtent); + childIndex += 1; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + } +} diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 510251ac2e..10b002bbf9 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -178,3 +178,66 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin> + extends RenderBox with ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderVirtualViewport({ + int virtualChildCount, + Offset paintOffset, + LayoutCallback callback + }) : _virtualChildCount = virtualChildCount, + _paintOffset = paintOffset, + _callback = callback; + + int get virtualChildCount => _virtualChildCount ?? childCount; + int _virtualChildCount; + void set virtualChildCount(int value) { + if (_virtualChildCount == value) + return; + _virtualChildCount = value; + markNeedsLayout(); + } + + /// The offset at which to paint the first item. + /// + /// Note: you can modify this property from within [callback], if necessary. + Offset get paintOffset => _paintOffset; + Offset _paintOffset; + void set paintOffset(Offset value) { + assert(value != null); + if (value == _paintOffset) + return; + _paintOffset = value; + markNeedsPaint(); + } + + /// Called during [layout] to determine the grid's children. + /// + /// Typically the callback will mutate the child list appropriately, for + /// example so the child list contains only visible children. + LayoutCallback get callback => _callback; + LayoutCallback _callback; + void set callback(LayoutCallback value) { + if (value == _callback) + return; + _callback = value; + markNeedsLayout(); + } + + void applyPaintTransform(RenderBox child, Matrix4 transform) { + super.applyPaintTransform(child, transform.translate(paintOffset)); + } + + bool hitTestChildren(HitTestResult result, { Point position }) { + return defaultHitTestChildren(result, position: position + -paintOffset); + } + + void _paintContents(PaintingContext context, Offset offset) { + defaultPaint(context, offset + paintOffset); + } + + void paint(PaintingContext context, Offset offset) { + context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents); + } +} diff --git a/packages/flutter/lib/src/widgets/scrollable_grid.dart b/packages/flutter/lib/src/widgets/scrollable_grid.dart index 7823047d4a..39c23fbab2 100644 --- a/packages/flutter/lib/src/widgets/scrollable_grid.dart +++ b/packages/flutter/lib/src/widgets/scrollable_grid.dart @@ -4,9 +4,9 @@ import 'dart:math' as math; -import 'basic.dart'; import 'framework.dart'; import 'scrollable.dart'; +import 'virtual_viewport.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; @@ -38,11 +38,11 @@ class ScrollableGrid extends Scrollable { final GridDelegate delegate; final List children; - ScrollableState createState() => new _ScrollableGrid(); + ScrollableState createState() => new _ScrollableGridState(); } -class _ScrollableGrid extends ScrollableState { - ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(); +class _ScrollableGridState extends ScrollableState { + ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; void _handleExtentsChanged(double contentExtent, double containerExtent) { @@ -65,9 +65,7 @@ class _ScrollableGrid extends ScrollableState { } } -typedef void ExtentsChangedCallback(double contentExtent, double containerExtent); - -class GridViewport extends RenderObjectWidget { +class GridViewport extends VirtualViewport { GridViewport({ Key key, this.startOffset, @@ -87,6 +85,7 @@ class GridViewport extends RenderObjectWidget { } // TODO(abarth): This function should go somewhere more general. +// See https://github.com/dart-lang/collection/pull/16 int _lowerBound(List sortedList, var value, { int begin: 0 }) { int current = begin; int count = sortedList.length - current; @@ -103,82 +102,31 @@ int _lowerBound(List sortedList, var value, { int begin: 0 }) { return current; } -class _GridViewportElement extends RenderObjectElement { +class _GridViewportElement extends VirtualViewportElement { _GridViewportElement(GridViewport widget) : super(widget); - double _contentExtent; - double _containerExtent; - - int _materializedChildBase; - int _materializedChildCount; - - List _materializedChildren = const []; - - GridSpecification _specification; - double _repaintOffsetBase; - double _repaintOffsetLimit; - RenderGrid get renderObject => super.renderObject; - void visitChildren(ElementVisitor visitor) { - if (_materializedChildren == null) - return; - for (Element child in _materializedChildren) - visitor(child); - } + int get materializedChildBase => _materializedChildBase; + int _materializedChildBase; - void mount(Element parent, dynamic newSlot) { - super.mount(parent, newSlot); - renderObject.callback = layout; - _updateRenderObject(); - } + int get materializedChildCount => _materializedChildCount; + int _materializedChildCount; - void unmount() { - renderObject.callback = null; - super.unmount(); - } + double get repaintOffsetBase => _repaintOffsetBase; + double _repaintOffsetBase; - void update(GridViewport newWidget) { - super.update(newWidget); - _updateRenderObject(); - if (!renderObject.needsLayout) - _materializeChildren(); - } + double get repaintOffsetLimit =>_repaintOffsetLimit; + double _repaintOffsetLimit; - void _updatePaintOffset() { - renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - _repaintOffsetBase)); - } - - void _updateRenderObject() { + void updateRenderObject() { renderObject.delegate = widget.delegate; - renderObject.virtualChildCount = widget.children.length; - - if (_specification != null) { - _updatePaintOffset(); - - // If we don't already need layout, we need to request a layout if the - // viewport has shifted to expose a new row. - if (!renderObject.needsLayout) { - if (_repaintOffsetBase != null && widget.startOffset < _repaintOffsetBase) - renderObject.markNeedsLayout(); - else if (_repaintOffsetLimit != null && widget.startOffset + _containerExtent > _repaintOffsetLimit) - renderObject.markNeedsLayout(); - } - } + super.updateRenderObject(); } - void _materializeChildren() { - assert(_materializedChildBase != null); - assert(_materializedChildCount != null); - List newWidgets = new List(_materializedChildCount); - for (int i = 0; i < _materializedChildCount; ++i) { - int childIndex = _materializedChildBase + i; - Widget child = widget.children[childIndex]; - Key key = new ValueKey(child.key ?? childIndex); - newWidgets[i] = new RepaintBoundary(key: key, child: child); - } - _materializedChildren = updateChildren(_materializedChildren, newWidgets); - } + double _contentExtent; + double _containerExtent; + GridSpecification _specification; void layout(BoxConstraints constraints) { _specification = renderObject.specification; @@ -188,13 +136,12 @@ class _GridViewportElement extends RenderObjectElement { int materializedRowBase = math.max(0, _lowerBound(_specification.rowOffsets, widget.startOffset) - 1); int materializedRowLimit = math.min(_specification.rowCount, _lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent)); - _materializedChildBase = materializedRowBase * _specification.columnCount; - _materializedChildCount = math.min(widget.children.length, materializedRowLimit * _specification.columnCount) - _materializedChildBase; + _materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, widget.children.length); + _materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, widget.children.length) - _materializedChildBase; _repaintOffsetBase = _specification.rowOffsets[materializedRowBase]; _repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit]; - _updatePaintOffset(); - BuildableElement.lockState(_materializeChildren); + super.layout(constraints); if (contentExtent != _contentExtent || containerExtent != _containerExtent) { _contentExtent = contentExtent; @@ -202,20 +149,4 @@ class _GridViewportElement extends RenderObjectElement { widget.onExtentsChanged(_contentExtent, _containerExtent); } } - - void insertChildRenderObject(RenderObject child, Element slot) { - RenderObject nextSibling = slot?.renderObject; - renderObject.add(child, before: nextSibling); - } - - void moveChildRenderObject(RenderObject child, Element slot) { - assert(child.parent == renderObject); - RenderObject nextSibling = slot?.renderObject; - renderObject.move(child, before: nextSibling); - } - - void removeChildRenderObject(RenderObject child) { - assert(child.parent == renderObject); - renderObject.remove(child); - } } diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart new file mode 100644 index 0000000000..753b46323d --- /dev/null +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -0,0 +1,124 @@ +// 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 'framework.dart'; +import 'scrollable.dart'; +import 'virtual_viewport.dart'; + +import 'package:flutter/animation.dart'; +import 'package:flutter/rendering.dart'; + +class ScrollableList2 extends Scrollable { + ScrollableList2({ + Key key, + double initialScrollOffset, + ScrollListener onScroll, + SnapOffsetCallback snapOffsetCallback, + double snapAlignmentOffset: 0.0, + this.itemExtent, + this.children + }) : super( + key: key, + initialScrollOffset: initialScrollOffset, + // TODO(abarth): Support horizontal offsets. + scrollDirection: ScrollDirection.vertical, + onScroll: onScroll, + snapOffsetCallback: snapOffsetCallback, + snapAlignmentOffset: snapAlignmentOffset + ); + + final double itemExtent; + final List children; + + ScrollableState createState() => new _ScrollableList2State(); +} + +class _ScrollableList2State extends ScrollableState { + ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); + ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; + + void _handleExtentsChanged(double contentExtent, double containerExtent) { + setState(() { + scrollTo(scrollBehavior.updateExtents( + contentExtent: contentExtent, + containerExtent: containerExtent, + scrollOffset: scrollOffset + )); + }); + } + + Widget buildContent(BuildContext context) { + return new ListViewport( + startOffset: scrollOffset, + itemExtent: config.itemExtent, + onExtentsChanged: _handleExtentsChanged, + children: config.children + ); + } +} + +class ListViewport extends VirtualViewport { + ListViewport({ + Key key, + this.startOffset, + this.itemExtent, + this.onExtentsChanged, + this.children + }); + + final double startOffset; + final double itemExtent; + final ExtentsChangedCallback onExtentsChanged; + final List children; + + RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); + + _ListViewportElement createElement() => new _ListViewportElement(this); +} + +class _ListViewportElement extends VirtualViewportElement { + _ListViewportElement(ListViewport widget) : super(widget); + + RenderList get renderObject => super.renderObject; + + int get materializedChildBase => _materializedChildBase; + int _materializedChildBase; + + int get materializedChildCount => _materializedChildCount; + int _materializedChildCount; + + double get repaintOffsetBase => _repaintOffsetBase; + double _repaintOffsetBase; + + double get repaintOffsetLimit =>_repaintOffsetLimit; + double _repaintOffsetLimit; + + void updateRenderObject() { + renderObject.itemExtent = widget.itemExtent; + super.updateRenderObject(); + } + + double _contentExtent; + double _containerExtent; + + void layout(BoxConstraints constraints) { + double contentExtent = widget.itemExtent * widget.children.length; + double containerExtent = renderObject.size.height; + + _materializedChildBase = (widget.startOffset ~/ widget.itemExtent).clamp(0, widget.children.length); + int materializedChildLimit = ((widget.startOffset + containerExtent) / widget.itemExtent).ceil().clamp(0, widget.children.length); + _materializedChildCount = materializedChildLimit - _materializedChildBase; + + _repaintOffsetBase = _materializedChildBase * widget.itemExtent; + _repaintOffsetLimit = materializedChildLimit * widget.itemExtent; + + super.layout(constraints); + + if (contentExtent != _contentExtent || containerExtent != _containerExtent) { + _contentExtent = contentExtent; + _containerExtent = containerExtent; + widget.onExtentsChanged(_contentExtent, _containerExtent); + } + } +} diff --git a/packages/flutter/lib/src/widgets/virtual_viewport.dart b/packages/flutter/lib/src/widgets/virtual_viewport.dart new file mode 100644 index 0000000000..db15b3b9ad --- /dev/null +++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart @@ -0,0 +1,113 @@ +// 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 '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; + List get children; +} + +abstract class VirtualViewportElement extends RenderObjectElement { + VirtualViewportElement(T widget) : super(widget); + + int get materializedChildBase; + int get materializedChildCount; + double get repaintOffsetBase; + double get repaintOffsetLimit; + + List _materializedChildren = const []; + + RenderVirtualViewport get renderObject => super.renderObject; + + void visitChildren(ElementVisitor visitor) { + if (_materializedChildren == null) + return; + for (Element child in _materializedChildren) + visitor(child); + } + + void mount(Element parent, dynamic newSlot) { + super.mount(parent, newSlot); + renderObject.callback = layout; + updateRenderObject(); + } + + void unmount() { + renderObject.callback = null; + super.unmount(); + } + + void update(T newWidget) { + super.update(newWidget); + updateRenderObject(); + if (!renderObject.needsLayout) + _materializeChildren(); + } + + void _updatePaintOffset() { + renderObject.paintOffset = + renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase)); + } + + void updateRenderObject() { + renderObject.virtualChildCount = widget.children.length; + + if (repaintOffsetBase != 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) { + if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase) + renderObject.markNeedsLayout(); + else if (repaintOffsetLimit != null && widget.startOffset + renderObject.size.height > repaintOffsetLimit) + renderObject.markNeedsLayout(); + } + } + } + + void layout(BoxConstraints constraints) { + assert(repaintOffsetBase != null); + assert(repaintOffsetLimit != null); + _updatePaintOffset(); + BuildableElement.lockState(_materializeChildren); + } + + void _materializeChildren() { + int base = materializedChildBase; + int count = materializedChildCount; + assert(base != null); + assert(count != null); + List newWidgets = new List(count); + for (int i = 0; i < count; ++i) { + int childIndex = base + i; + Widget child = widget.children[childIndex]; + 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) { + RenderObject nextSibling = slot?.renderObject; + renderObject.add(child, before: nextSibling); + } + + void moveChildRenderObject(RenderObject child, Element slot) { + assert(child.parent == renderObject); + RenderObject nextSibling = slot?.renderObject; + renderObject.move(child, before: nextSibling); + } + + void removeChildRenderObject(RenderObject child) { + assert(child.parent == renderObject); + renderObject.remove(child); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index b43c67894c..a5ba6610d1 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -33,10 +33,12 @@ export 'src/widgets/placeholder.dart'; export 'src/widgets/routes.dart'; export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable_grid.dart'; +export 'src/widgets/scrollable_list.dart'; export 'src/widgets/statistics_overlay.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/title.dart'; export 'src/widgets/transitions.dart'; export 'src/widgets/unique_component.dart'; +export 'src/widgets/virtual_viewport.dart'; export 'package:vector_math/vector_math_64.dart' show Matrix4;