From 37106ea603d47566f62033096c3d8ade472f91a1 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 4 Jan 2016 17:33:15 -0800 Subject: [PATCH] Complete features of ScrollableList2 This patch implements the remaining missing features of ScrollableList2. It should now be nearly a drop-in replacement for ScrollableList. The next patch will switch callers over to the new machinery. --- .../lib/src/material/scrollbar_painter.dart | 37 +++--- packages/flutter/lib/src/rendering/block.dart | 10 +- packages/flutter/lib/src/rendering/list.dart | 123 +++++++++++++++--- .../flutter/lib/src/rendering/viewport.dart | 39 +++++- .../flutter/lib/src/widgets/scrollable.dart | 17 ++- .../lib/src/widgets/scrollable_grid.dart | 3 + .../lib/src/widgets/scrollable_list.dart | 89 ++++++++++--- .../lib/src/widgets/virtual_viewport.dart | 24 +++- 8 files changed, 272 insertions(+), 70 deletions(-) diff --git a/packages/flutter/lib/src/material/scrollbar_painter.dart b/packages/flutter/lib/src/material/scrollbar_painter.dart index 5eb8f72ef0..fa07ff351b 100644 --- a/packages/flutter/lib/src/material/scrollbar_painter.dart +++ b/packages/flutter/lib/src/material/scrollbar_painter.dart @@ -30,22 +30,25 @@ class ScrollbarPainter extends ScrollableListPainter { Point thumbOrigin; Size thumbSize; - if (isVertical) { - double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent; - thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height); - final double maxThumbTop = viewportBounds.height - thumbHeight; - double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop; - thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop); - thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop); - thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight); - } else { - double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent; - thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width); - final double maxThumbLeft = viewportBounds.width - thumbWidth; - double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft; - thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft); - thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth); - thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth); + switch (scrollDirection) { + case ScrollDirection.vertical: + double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent; + thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height); + final double maxThumbTop = viewportBounds.height - thumbHeight; + double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop; + thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop); + thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop); + thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight); + break; + case ScrollDirection.horizontal: + double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent; + thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width); + final double maxThumbLeft = viewportBounds.width - thumbWidth; + double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft; + thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft); + thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth); + thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth); + break; } paintThumb(context, thumbOrigin & thumbSize); @@ -65,7 +68,7 @@ class ScrollbarPainter extends ScrollableListPainter { ..variable = new AnimatedValue(0.0, end: 1.0, curve: Curves.ease) ..addListener(() { _opacity = _fade.value; - renderer?.markNeedsPaint(); + renderObject?.markNeedsPaint(); }); return _fade.forward(); } diff --git a/packages/flutter/lib/src/rendering/block.dart b/packages/flutter/lib/src/rendering/block.dart index b26dab339d..431acad188 100644 --- a/packages/flutter/lib/src/rendering/block.dart +++ b/packages/flutter/lib/src/rendering/block.dart @@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; import 'object.dart'; +import 'viewport.dart'; /// Parent data for use with [RenderBlockBase]. class BlockParentData extends ContainerBoxParentDataMixin { } @@ -32,8 +33,10 @@ typedef double _Constrainer(double value); /// children. Because blocks expand in the main axis, blocks must be given /// unlimited space in the main axis, typically by being contained in a /// viewport with a scrolling direction that matches the block's main axis. -abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { +abstract class RenderBlockBase extends RenderBox + with ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin + implements RenderScrollable { RenderBlockBase({ List children, @@ -82,6 +85,9 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin /// Whether the main axis is vertical. bool get isVertical => _direction == BlockDirection.vertical; + // TODO(abarth): Remove BlockDirection in favor of ScrollDirection. + ScrollDirection get scrollDirection => isVertical ? ScrollDirection.vertical : ScrollDirection.horizontal; + BoxConstraints _getInnerConstraints(BoxConstraints constraints) { if (isVertical) return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth), diff --git a/packages/flutter/lib/src/rendering/list.dart b/packages/flutter/lib/src/rendering/list.dart index 131520d068..a857e3c64d 100644 --- a/packages/flutter/lib/src/rendering/list.dart +++ b/packages/flutter/lib/src/rendering/list.dart @@ -2,6 +2,8 @@ // 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 'box.dart'; import 'object.dart'; import 'viewport.dart'; @@ -9,18 +11,23 @@ import 'viewport.dart'; /// Parent data for use with [RenderList]. class ListParentData extends ContainerBoxParentDataMixin { } -class RenderList extends RenderVirtualViewport { +class RenderList extends RenderVirtualViewport implements RenderScrollable { RenderList({ List children, double itemExtent, + EdgeDims padding, int virtualChildCount, Offset paintOffset: Offset.zero, + ScrollDirection scrollDirection: ScrollDirection.vertical, LayoutCallback callback - }) : _itemExtent = itemExtent, super( - virtualChildCount: virtualChildCount, - paintOffset: paintOffset, - callback: callback - ) { + }) : _itemExtent = itemExtent, + _padding = padding, + _scrollDirection = scrollDirection, + super( + virtualChildCount: virtualChildCount, + paintOffset: paintOffset, + callback: callback + ) { assert(itemExtent != null); addAll(children); } @@ -35,50 +42,124 @@ class RenderList extends RenderVirtualViewport { markNeedsLayout(); } + EdgeDims get padding => _padding; + EdgeDims _padding; + void set padding (EdgeDims newValue) { + if (_padding == newValue) + return; + _padding = newValue; + markNeedsLayout(); + } + + ScrollDirection get scrollDirection => _scrollDirection; + ScrollDirection _scrollDirection; + void set scrollDirection (ScrollDirection newValue) { + if (_scrollDirection == newValue) + return; + _scrollDirection = newValue; + markNeedsLayout(); + } + void setupParentData(RenderBox child) { if (child.parentData is! ListParentData) child.parentData = new ListParentData(); } - double get _preferredMainAxisExtent => itemExtent * virtualChildCount; + double get _scrollAxisPadding { + switch (scrollDirection) { + case ScrollDirection.vertical: + return padding.vertical; + case ScrollDirection.horizontal: + return padding.horizontal; + } + } + + double get _preferredExtent { + double extent = itemExtent * virtualChildCount; + if (padding != null) + extent += _scrollAxisPadding; + return extent; + } + + double _getIntrinsicWidth(BoxConstraints constraints) { + assert(constraints.isNormalized); + switch (scrollDirection) { + case ScrollDirection.vertical: + return constraints.constrainWidth(0.0); + case ScrollDirection.horizontal: + return constraints.constrainWidth(_preferredExtent); + } + } double getMinIntrinsicWidth(BoxConstraints constraints) { - assert(constraints.isNormalized); - return constraints.constrainWidth(0.0); + return _getIntrinsicWidth(constraints); } double getMaxIntrinsicWidth(BoxConstraints constraints) { + return _getIntrinsicWidth(constraints); + } + + double _getIntrinsicHeight(BoxConstraints constraints) { assert(constraints.isNormalized); - return constraints.constrainWidth(0.0); + switch (scrollDirection) { + case ScrollDirection.vertical: + return constraints.constrainHeight(_preferredExtent); + case ScrollDirection.horizontal: + return constraints.constrainHeight(0.0); + } } double getMinIntrinsicHeight(BoxConstraints constraints) { - assert(constraints.isNormalized); - return constraints.constrainHeight(_preferredMainAxisExtent); + return _getIntrinsicHeight(constraints); } double getMaxIntrinsicHeight(BoxConstraints constraints) { - assert(constraints.isNormalized); - return constraints.constrainHeight(_preferredMainAxisExtent); + return _getIntrinsicHeight(constraints); } void performLayout() { - double height = _preferredMainAxisExtent; - size = new Size(constraints.maxWidth, constraints.constrainHeight(height)); + size = new Size(constraints.maxWidth, + constraints.constrainHeight(_preferredExtent)); if (callback != null) invokeLayoutCallback(callback); - BoxConstraints innerConstraints = - new BoxConstraints.tightFor(width: size.width, height: itemExtent); + double itemWidth; + double itemHeight; + + double x = 0.0; + double dx = 0.0; + + double y = 0.0; + double dy = 0.0; + + switch (scrollDirection) { + case ScrollDirection.vertical: + itemWidth = math.max(0, size.width - (padding == null ? 0.0 : padding.horizontal)); + itemHeight = itemExtent; + y = padding != null ? padding.top : 0.0; + dy = itemExtent; + break; + case ScrollDirection.horizontal: + itemWidth = itemExtent; + itemHeight = math.max(0, size.height - (padding == null ? 0.0 : padding.vertical)); + x = padding != null ? padding.left : 0.0; + dx = itemExtent; + break; + } + + BoxConstraints innerConstraints = + new BoxConstraints.tightFor(width: itemWidth, height: itemHeight); - 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; + childParentData.offset = new Offset(x, y); + x += dx; + y += dy; + 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 90b405a2b1..d5240dae3d 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -18,6 +18,10 @@ enum ScrollDirection { vertical, } +abstract class RenderScrollable { + ScrollDirection get scrollDirection; +} + /// A render object that's bigger on the inside. /// /// The child of a viewport can layout to a larger size than the viewport @@ -27,7 +31,8 @@ enum ScrollDirection { /// /// Viewport is the core scrolling primitive in the system, but it can be used /// in other situations. -class RenderViewport extends RenderBox with RenderObjectWithChildMixin { +class RenderViewport extends RenderBox with RenderObjectWithChildMixin + implements RenderScrollable { RenderViewport({ RenderBox child, @@ -177,10 +182,12 @@ abstract class RenderVirtualViewport _virtualChildCount ?? childCount; int _virtualChildCount; @@ -217,8 +224,31 @@ abstract class RenderVirtualViewport _overlayPainter; + Painter _overlayPainter; + void set overlayPainter(Painter value) { + if (_overlayPainter == value) + return; + if (attached) + _overlayPainter?.detach(); + _overlayPainter = value; + if (attached) + _overlayPainter?.attach(this); + markNeedsPaint(); + } + + void attach() { + super.attach(); + _overlayPainter?.attach(this); + } + + void detach() { + super.detach(); + _overlayPainter?.detach(); + } + void applyPaintTransform(RenderBox child, Matrix4 transform) { - super.applyPaintTransform(child, transform.translate(paintOffset)); + super.applyPaintTransform(child, transform.translate(paintOffset.dx, paintOffset.dy)); } bool hitTestChildren(HitTestResult result, { Point position }) { @@ -227,6 +257,7 @@ abstract class RenderVirtualViewport renderObject; + RenderBox get renderObject => super.renderObject; - bool get isVertical => renderer.isVertical; + ScrollDirection get scrollDirection { + return (renderObject as RenderScrollable)?.scrollDirection; + } - Size get viewportSize => renderer.size; + Size get viewportSize => renderObject.size; double get contentExtent => _contentExtent; double _contentExtent = 0.0; @@ -463,7 +466,7 @@ abstract class ScrollableListPainter extends Painter { if (_contentExtent == value) return; _contentExtent = value; - renderer?.markNeedsPaint(); + renderObject?.markNeedsPaint(); } double get scrollOffset => _scrollOffset; @@ -473,7 +476,7 @@ abstract class ScrollableListPainter extends Painter { if (_scrollOffset == value) return; _scrollOffset = value; - renderer?.markNeedsPaint(); + renderObject?.markNeedsPaint(); } /// Called when a scroll starts. Subclasses may override this method to @@ -675,7 +678,7 @@ class ScrollableList extends ScrollableWidgetList { double snapAlignmentOffset: 0.0, this.items, this.itemBuilder, - itemsWrap: false, + bool itemsWrap: false, double itemExtent, EdgeDims padding, ScrollableListPainter scrollableListPainter diff --git a/packages/flutter/lib/src/widgets/scrollable_grid.dart b/packages/flutter/lib/src/widgets/scrollable_grid.dart index 39c23fbab2..93efa9e54d 100644 --- a/packages/flutter/lib/src/widgets/scrollable_grid.dart +++ b/packages/flutter/lib/src/widgets/scrollable_grid.dart @@ -79,6 +79,9 @@ class GridViewport extends VirtualViewport { final ExtentsChangedCallback onExtentsChanged; final List children; + // TODO(abarth): Support horizontal scrolling; + ScrollDirection get scrollDirection => ScrollDirection.vertical; + RenderGrid createRenderObject() => new RenderGrid(delegate: delegate); _GridViewportElement createElement() => new _GridViewportElement(this); diff --git a/packages/flutter/lib/src/widgets/scrollable_list.dart b/packages/flutter/lib/src/widgets/scrollable_list.dart index 753b46323d..6b4b30c7dc 100644 --- a/packages/flutter/lib/src/widgets/scrollable_list.dart +++ b/packages/flutter/lib/src/widgets/scrollable_list.dart @@ -2,6 +2,8 @@ // 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 'framework.dart'; import 'scrollable.dart'; import 'virtual_viewport.dart'; @@ -13,22 +15,30 @@ class ScrollableList2 extends Scrollable { ScrollableList2({ Key key, double initialScrollOffset, + ScrollDirection scrollDirection: ScrollDirection.vertical, ScrollListener onScroll, SnapOffsetCallback snapOffsetCallback, double snapAlignmentOffset: 0.0, this.itemExtent, + this.itemsWrap: false, + this.padding, + this.scrollableListPainter, this.children }) : super( key: key, initialScrollOffset: initialScrollOffset, - // TODO(abarth): Support horizontal offsets. - scrollDirection: ScrollDirection.vertical, + scrollDirection: scrollDirection, onScroll: onScroll, snapOffsetCallback: snapOffsetCallback, snapAlignmentOffset: snapAlignmentOffset - ); + ) { + assert(itemExtent != null); + } final double itemExtent; + final bool itemsWrap; + final EdgeDims padding; + final ScrollableListPainter scrollableListPainter; final List children; ScrollableState createState() => new _ScrollableList2State(); @@ -39,20 +49,40 @@ class _ScrollableList2State extends ScrollableState { ExtentScrollBehavior get scrollBehavior => super.scrollBehavior; void _handleExtentsChanged(double contentExtent, double containerExtent) { + config.scrollableListPainter?.contentExtent = contentExtent; setState(() { scrollTo(scrollBehavior.updateExtents( - contentExtent: contentExtent, + contentExtent: config.itemsWrap ? double.INFINITY : contentExtent, containerExtent: containerExtent, scrollOffset: scrollOffset )); }); } + void dispatchOnScrollStart() { + super.dispatchOnScrollStart(); + config.scrollableListPainter?.scrollStarted(); + } + + void dispatchOnScroll() { + super.dispatchOnScroll(); + config.scrollableListPainter?.scrollOffset = scrollOffset; + } + + void dispatchOnScrollEnd() { + super.dispatchOnScrollEnd(); + config.scrollableListPainter?.scrollEnded(); + } + Widget buildContent(BuildContext context) { return new ListViewport( - startOffset: scrollOffset, - itemExtent: config.itemExtent, onExtentsChanged: _handleExtentsChanged, + startOffset: scrollOffset, + scrollDirection: config.scrollDirection, + itemExtent: config.itemExtent, + itemsWrap: config.itemsWrap, + padding: config.padding, + overlayPainter: config.scrollableListPainter, children: config.children ); } @@ -61,15 +91,26 @@ class _ScrollableList2State extends ScrollableState { class ListViewport extends VirtualViewport { ListViewport({ Key key, - this.startOffset, - this.itemExtent, this.onExtentsChanged, + this.startOffset: 0.0, + this.scrollDirection: ScrollDirection.vertical, + this.itemExtent, + this.itemsWrap: false, + this.padding, + this.overlayPainter, this.children - }); + }) { + assert(scrollDirection != null); + assert(itemExtent != null); + } - final double startOffset; - final double itemExtent; final ExtentsChangedCallback onExtentsChanged; + final double startOffset; + final ScrollDirection scrollDirection; + final double itemExtent; + final bool itemsWrap; + final EdgeDims padding; + final Painter overlayPainter; final List children; RenderList createRenderObject() => new RenderList(itemExtent: itemExtent); @@ -95,21 +136,39 @@ class _ListViewportElement extends VirtualViewportElement { double _repaintOffsetLimit; void updateRenderObject() { + renderObject.scrollDirection = widget.scrollDirection; renderObject.itemExtent = widget.itemExtent; + renderObject.padding = widget.padding; + renderObject.overlayPainter = widget.overlayPainter; super.updateRenderObject(); } double _contentExtent; double _containerExtent; + double _getContainerExtentFromRenderObject() { + switch (widget.scrollDirection) { + case ScrollDirection.vertical: + return renderObject.size.height; + case ScrollDirection.horizontal: + return renderObject.size.width; + } + } + void layout(BoxConstraints constraints) { double contentExtent = widget.itemExtent * widget.children.length; - double containerExtent = renderObject.size.height; + double containerExtent = _getContainerExtentFromRenderObject(); + + _materializedChildBase = math.max(0, widget.startOffset ~/ widget.itemExtent); + int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / widget.itemExtent).ceil()); + + if (!widget.itemsWrap) { + int length = widget.children.length; + _materializedChildBase = math.min(length, _materializedChildBase); + materializedChildLimit = math.min(length, materializedChildLimit); + } - _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; diff --git a/packages/flutter/lib/src/widgets/virtual_viewport.dart b/packages/flutter/lib/src/widgets/virtual_viewport.dart index db15b3b9ad..ed13bd0a00 100644 --- a/packages/flutter/lib/src/widgets/virtual_viewport.dart +++ b/packages/flutter/lib/src/widgets/virtual_viewport.dart @@ -11,6 +11,7 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent abstract class VirtualViewport extends RenderObjectWidget { double get startOffset; + ScrollDirection get scrollDirection; List get children; } @@ -52,8 +53,23 @@ abstract class VirtualViewportElement extends RenderO } void _updatePaintOffset() { - renderObject.paintOffset = - renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase)); + switch (widget.scrollDirection) { + case ScrollDirection.vertical: + renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase)); + break; + case ScrollDirection.horizontal: + renderObject.paintOffset = new Offset(-(widget.startOffset - repaintOffsetBase), 0.0); + break; + } + } + + double get _containerExtent { + switch (widget.scrollDirection) { + case ScrollDirection.vertical: + return renderObject.size.height; + case ScrollDirection.horizontal: + return renderObject.size.width; + } } void updateRenderObject() { @@ -67,7 +83,7 @@ abstract class VirtualViewportElement extends RenderO if (!renderObject.needsLayout) { if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase) renderObject.markNeedsLayout(); - else if (repaintOffsetLimit != null && widget.startOffset + renderObject.size.height > repaintOffsetLimit) + else if (repaintOffsetLimit != null && widget.startOffset + _containerExtent > repaintOffsetLimit) renderObject.markNeedsLayout(); } } @@ -88,7 +104,7 @@ abstract class VirtualViewportElement extends RenderO List newWidgets = new List(count); for (int i = 0; i < count; ++i) { int childIndex = base + i; - Widget child = widget.children[childIndex]; + Widget child = widget.children[childIndex % widget.children.length]; Key key = new ValueKey(child.key ?? childIndex); newWidgets[i] = new RepaintBoundary(key: key, child: child); }