From 99bca282c9268241c18c00b8bee58f35f52622fa Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 4 Jan 2016 14:38:44 -0800 Subject: [PATCH] Introduce ScrollableList2 ScrollableList2 uses the same pattern as ScrollableGrid, which requires the client to allocate widgets for every list item but doesn't inflate them unless they're actually needed for the view. It improves on the original ScrollableList by not requiring a rebuild of the whole visible portion of the list when scrolling. In fact, small scrolls can often be handled entirely by repainting. --- examples/widgets/media_query.dart | 9 +- packages/flutter/lib/rendering.dart | 1 + packages/flutter/lib/src/rendering/grid.dart | 88 ++----------- packages/flutter/lib/src/rendering/list.dart | 86 ++++++++++++ .../flutter/lib/src/rendering/viewport.dart | 63 +++++++++ .../lib/src/widgets/scrollable_grid.dart | 115 ++++------------ .../lib/src/widgets/scrollable_list.dart | 124 ++++++++++++++++++ .../lib/src/widgets/virtual_viewport.dart | 113 ++++++++++++++++ packages/flutter/lib/widgets.dart | 2 + 9 files changed, 427 insertions(+), 174 deletions(-) create mode 100644 packages/flutter/lib/src/rendering/list.dart create mode 100644 packages/flutter/lib/src/widgets/scrollable_list.dart create mode 100644 packages/flutter/lib/src/widgets/virtual_viewport.dart 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;