Commit 37106ea6 authored by Adam Barth's avatar Adam Barth

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.
parent f72c8f6d
......@@ -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<double>(0.0, end: 1.0, curve: Curves.ease)
..addListener(() {
_opacity = _fade.value;
renderer?.markNeedsPaint();
renderObject?.markNeedsPaint();
});
return _fade.forward();
}
......
......@@ -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<RenderBox> { }
......@@ -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<RenderBox, BlockParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData> {
abstract class RenderBlockBase extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData>
implements RenderScrollable {
RenderBlockBase({
List<RenderBox> 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),
......
......@@ -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<RenderBox> { }
class RenderList extends RenderVirtualViewport<ListParentData> {
class RenderList extends RenderVirtualViewport<ListParentData> implements RenderScrollable {
RenderList({
List<RenderBox> 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<ListParentData> {
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 getMinIntrinsicWidth(BoxConstraints constraints) {
double get _preferredExtent {
double extent = itemExtent * virtualChildCount;
if (padding != null)
extent += _scrollAxisPadding;
return extent;
}
double _getIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainWidth(0.0);
switch (scrollDirection) {
case ScrollDirection.vertical:
return constraints.constrainWidth(0.0);
case ScrollDirection.horizontal:
return constraints.constrainWidth(_preferredExtent);
}
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
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);
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: size.width, height: itemExtent);
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;
}
......
......@@ -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<RenderBox> {
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>
implements RenderScrollable {
RenderViewport({
RenderBox child,
......@@ -177,10 +182,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
RenderVirtualViewport({
int virtualChildCount,
Offset paintOffset,
LayoutCallback callback
LayoutCallback callback,
Painter overlayPainter
}) : _virtualChildCount = virtualChildCount,
_paintOffset = paintOffset,
_callback = callback;
_callback = callback,
_overlayPainter = overlayPainter;
int get virtualChildCount => _virtualChildCount ?? childCount;
int _virtualChildCount;
......@@ -217,8 +224,31 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
markNeedsLayout();
}
Painter get overlayPainter => _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<T extends ContainerBoxParentDataMixin<Rende
void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + paintOffset);
_overlayPainter?.paint(context, offset);
}
void paint(PaintingContext context, Offset offset) {
......
......@@ -445,15 +445,18 @@ class Block extends StatelessComponent {
abstract class ScrollableListPainter extends Painter {
void attach(RenderObject renderObject) {
assert(renderObject is RenderBlockViewport);
assert(renderObject is RenderBox);
assert(renderObject is RenderScrollable);
super.attach(renderObject);
}
RenderBlockViewport get renderer => 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<T> extends ScrollableWidgetList {
double snapAlignmentOffset: 0.0,
this.items,
this.itemBuilder,
itemsWrap: false,
bool itemsWrap: false,
double itemExtent,
EdgeDims padding,
ScrollableListPainter scrollableListPainter
......
......@@ -79,6 +79,9 @@ class GridViewport extends VirtualViewport {
final ExtentsChangedCallback onExtentsChanged;
final List<Widget> children;
// TODO(abarth): Support horizontal scrolling;
ScrollDirection get scrollDirection => ScrollDirection.vertical;
RenderGrid createRenderObject() => new RenderGrid(delegate: delegate);
_GridViewportElement createElement() => new _GridViewportElement(this);
......
......@@ -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<Widget> children;
ScrollableState createState() => new _ScrollableList2State();
......@@ -39,20 +49,40 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
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(
onExtentsChanged: _handleExtentsChanged,
startOffset: scrollOffset,
scrollDirection: config.scrollDirection,
itemExtent: config.itemExtent,
onExtentsChanged: _handleExtentsChanged,
itemsWrap: config.itemsWrap,
padding: config.padding,
overlayPainter: config.scrollableListPainter,
children: config.children
);
}
......@@ -61,15 +91,26 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
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 ExtentsChangedCallback onExtentsChanged;
final double startOffset;
final ScrollDirection scrollDirection;
final double itemExtent;
final ExtentsChangedCallback onExtentsChanged;
final bool itemsWrap;
final EdgeDims padding;
final Painter overlayPainter;
final List<Widget> children;
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
......@@ -95,21 +136,39 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
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 = (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;
_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);
}
_materializedChildCount = materializedChildLimit - _materializedChildBase;
_repaintOffsetBase = _materializedChildBase * widget.itemExtent;
_repaintOffsetLimit = materializedChildLimit * widget.itemExtent;
......
......@@ -11,6 +11,7 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
abstract class VirtualViewport extends RenderObjectWidget {
double get startOffset;
ScrollDirection get scrollDirection;
List<Widget> get children;
}
......@@ -52,8 +53,23 @@ abstract class VirtualViewportElement<T extends VirtualViewport> 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<T extends VirtualViewport> 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<T extends VirtualViewport> extends RenderO
List<Widget> newWidgets = new List<Widget>(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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment