Commit 071f756a authored by Ian Hickson's avatar Ian Hickson

Merge pull request #2247 from Hixie/size-obs-9

SizeObserver crusade: ScrollableMixedWidgetListState
parents b18047bf 438f2090
...@@ -242,6 +242,7 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -242,6 +242,7 @@ class RenderBlockViewport extends RenderBlockBase {
RenderBlockViewport({ RenderBlockViewport({
LayoutCallback callback, LayoutCallback callback,
VoidCallback postLayoutCallback,
ExtentCallback totalExtentCallback, ExtentCallback totalExtentCallback,
ExtentCallback maxCrossAxisDimensionCallback, ExtentCallback maxCrossAxisDimensionCallback,
ExtentCallback minCrossAxisDimensionCallback, ExtentCallback minCrossAxisDimensionCallback,
...@@ -276,6 +277,12 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -276,6 +277,12 @@ class RenderBlockViewport extends RenderBlockBase {
markNeedsLayout(); markNeedsLayout();
} }
/// Called during after [layout].
///
/// This callback cannot mutate the tree. To mutate the tree during
/// layout, use [callback].
VoidCallback postLayoutCallback;
/// Returns the total main-axis extent of all the children that could be included by [callback] in one go. /// Returns the total main-axis extent of all the children that could be included by [callback] in one go.
ExtentCallback get totalExtentCallback => _totalExtentCallback; ExtentCallback get totalExtentCallback => _totalExtentCallback;
ExtentCallback _totalExtentCallback; ExtentCallback _totalExtentCallback;
...@@ -409,6 +416,8 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -409,6 +416,8 @@ class RenderBlockViewport extends RenderBlockBase {
} }
} }
super.performLayout(); super.performLayout();
if (postLayoutCallback != null)
postLayoutCallback();
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
......
...@@ -22,7 +22,7 @@ class MixedViewport extends RenderObjectWidget { ...@@ -22,7 +22,7 @@ class MixedViewport extends RenderObjectWidget {
this.direction: Axis.vertical, this.direction: Axis.vertical,
this.builder, this.builder,
this.token, this.token,
this.onExtentChanged, this.onPaintOffsetUpdateNeeded,
this.onInvalidatorAvailable this.onInvalidatorAvailable
}) : super(key: key); }) : super(key: key);
...@@ -30,7 +30,7 @@ class MixedViewport extends RenderObjectWidget { ...@@ -30,7 +30,7 @@ class MixedViewport extends RenderObjectWidget {
final Axis direction; final Axis direction;
final IndexedBuilder builder; final IndexedBuilder builder;
final Object token; // change this if the list changed (i.e. there are added, removed, or resorted items) final Object token; // change this if the list changed (i.e. there are added, removed, or resorted items)
final ValueChanged<double> onExtentChanged; final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
final InvalidatorAvailableCallback onInvalidatorAvailable; // call the callback this gives to invalidate sizes final InvalidatorAvailableCallback onInvalidatorAvailable; // call the callback this gives to invalidate sizes
_MixedViewportElement createElement() => new _MixedViewportElement(this); _MixedViewportElement createElement() => new _MixedViewportElement(this);
...@@ -107,8 +107,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -107,8 +107,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
/// The constraints for which the current offsets are valid. /// The constraints for which the current offsets are valid.
BoxConstraints _lastLayoutConstraints; BoxConstraints _lastLayoutConstraints;
/// The last value that was sent to onExtentChanged. /// The last value that was sent to onPaintOffsetUpdateNeeded.
double _lastReportedExtent; ViewportDimensions _lastReportedDimensions;
double _overrideStartOffset;
double get startOffset => _overrideStartOffset ?? widget.startOffset;
RenderBlockViewport get renderObject => super.renderObject; RenderBlockViewport get renderObject => super.renderObject;
...@@ -141,6 +144,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -141,6 +144,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
renderObject renderObject
..direction = widget.direction ..direction = widget.direction
..callback = layout ..callback = layout
..postLayoutCallback = postLayout
..totalExtentCallback = _noIntrinsicExtent ..totalExtentCallback = _noIntrinsicExtent
..maxCrossAxisExtentCallback = _noIntrinsicExtent ..maxCrossAxisExtentCallback = _noIntrinsicExtent
..minCrossAxisExtentCallback = _noIntrinsicExtent; ..minCrossAxisExtentCallback = _noIntrinsicExtent;
...@@ -149,6 +153,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -149,6 +153,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
void unmount() { void unmount() {
renderObject renderObject
..callback = null ..callback = null
..postLayoutCallback = null
..totalExtentCallback = null ..totalExtentCallback = null
..minCrossAxisExtentCallback = null ..minCrossAxisExtentCallback = null
..maxCrossAxisExtentCallback = null; ..maxCrossAxisExtentCallback = null;
...@@ -175,9 +180,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -175,9 +180,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
_ChangeDescription changes = newWidget.evaluateChangesFrom(widget); _ChangeDescription changes = newWidget.evaluateChangesFrom(widget);
super.update(newWidget); super.update(newWidget);
renderObject.direction = widget.direction; renderObject.direction = widget.direction;
_overrideStartOffset = null;
if (changes == _ChangeDescription.resized) if (changes == _ChangeDescription.resized)
_resetCache(); _resetCache();
if (changes != _ChangeDescription.none || !_isValid) { if (changes != _ChangeDescription.none || !_isValid) {
// we scrolled or changed in some other potentially layout-affecting way
renderObject.markNeedsLayout(); renderObject.markNeedsLayout();
} else { } else {
// We have to reinvoke our builders because they might return new data. // We have to reinvoke our builders because they might return new data.
...@@ -226,12 +233,45 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -226,12 +233,45 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
BuildableElement.lockState(() { BuildableElement.lockState(() {
_doLayout(constraints); _doLayout(constraints);
}, building: true); }, building: true);
if (widget.onExtentChanged != null) { }
final double newExtent = _didReachLastChild ? _childOffsets.last : null;
if (newExtent != _lastReportedExtent) { void postLayout() {
_lastReportedExtent = newExtent; assert(renderObject.hasSize);
widget.onExtentChanged(_lastReportedExtent); if (widget.onPaintOffsetUpdateNeeded != null) {
final Size containerSize = renderObject.size;
final double newExtent = _didReachLastChild ? _childOffsets.last : double.INFINITY;
Size contentSize;
switch (widget.direction) {
case Axis.vertical:
contentSize = new Size(containerSize.width, newExtent);
break;
case Axis.horizontal:
contentSize = new Size(newExtent, containerSize.height);
break;
} }
ViewportDimensions dimensions = new ViewportDimensions(
containerSize: containerSize,
contentSize: contentSize
);
if (dimensions != _lastReportedDimensions) {
_lastReportedDimensions = dimensions;
Offset overrideOffset = widget.onPaintOffsetUpdateNeeded(dimensions);
switch (widget.direction) {
case Axis.vertical:
assert(overrideOffset.dx == 0.0);
_overrideStartOffset = overrideOffset.dy;
break;
case Axis.horizontal:
assert(overrideOffset.dy == 0.0);
_overrideStartOffset = overrideOffset.dx;
break;
}
}
}
if (_childOffsets.length > 0) {
renderObject.startOffset = _childOffsets[_firstVisibleChildIndex] - startOffset;
} else {
renderObject.startOffset = 0.0;
} }
} }
...@@ -375,7 +415,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -375,7 +415,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
final Map<int, Element> builtChildren = new Map<int, Element>(); final Map<int, Element> builtChildren = new Map<int, Element>();
// Establish the start and end offsets based on our current constraints. // Establish the start and end offsets based on our current constraints.
final double endOffset = widget.startOffset + _getMaxExtent(constraints); final double endOffset = startOffset + _getMaxExtent(constraints);
// Create the constraints that we will use to measure the children. // Create the constraints that we will use to measure the children.
final BoxConstraints innerConstraints = _getInnerConstraints(constraints); final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
...@@ -417,7 +457,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -417,7 +457,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
// Decide if it's visible. // Decide if it's visible.
final _ChildKey key = new _ChildKey.fromWidget(newElement.widget); final _ChildKey key = new _ChildKey.fromWidget(newElement.widget);
final bool isVisible = _childOffsets[widgetIndex] < endOffset && _childOffsets[widgetIndex + 1] >= widget.startOffset; final bool isVisible = _childOffsets[widgetIndex] < endOffset && _childOffsets[widgetIndex + 1] >= startOffset;
if (isVisible) { if (isVisible) {
// Keep it. // Keep it.
newChildren[key] = newElement; newChildren[key] = newElement;
...@@ -438,7 +478,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -438,7 +478,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
if (endOffset < 0.0) { if (endOffset < 0.0) {
// We're so far scrolled up that nothing is visible. // We're so far scrolled up that nothing is visible.
haveChildren = false; haveChildren = false;
} else if (widget.startOffset <= 0.0) { } else if (startOffset <= 0.0) {
startIndex = 0; startIndex = 0;
// If we're scrolled up past the top, then our first visible widget, if // If we're scrolled up past the top, then our first visible widget, if
// any, is the first widget. // any, is the first widget.
...@@ -458,13 +498,13 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -458,13 +498,13 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
} else { } else {
// We're at some sane (not higher than the top) scroll offset. // We're at some sane (not higher than the top) scroll offset.
// See if we can already find the offset in our cache. // See if we can already find the offset in our cache.
startIndex = _findIndexForOffsetBeforeOrAt(widget.startOffset); startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
if (startIndex < _childExtents.length) { if (startIndex < _childExtents.length) {
// We already know of a child that would be visible at this offset. // We already know of a child that would be visible at this offset.
haveChildren = true; haveChildren = true;
} else { } else {
// We don't have an offset on the list that is beyond the start offset. // We don't have an offset on the list that is beyond the start offset.
assert(_childOffsets.last <= widget.startOffset); assert(_childOffsets.last <= startOffset);
// Fill the list until this isn't true or until we know that the // Fill the list until this isn't true or until we know that the
// list is complete (and thus we are overscrolled). // list is complete (and thus we are overscrolled).
while (true) { while (true) {
...@@ -477,7 +517,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -477,7 +517,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
break; break;
} }
final _ChildKey key = new _ChildKey.fromWidget(element.widget); final _ChildKey key = new _ChildKey.fromWidget(element.widget);
if (_childOffsets.last > widget.startOffset) { if (_childOffsets.last > startOffset) {
// This element is visible! It must thus be our first visible child. // This element is visible! It must thus be our first visible child.
newChildren[key] = element; newChildren[key] = element;
builtChildren[startIndex] = element; builtChildren[startIndex] = element;
...@@ -491,7 +531,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -491,7 +531,7 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
startIndex += 1; startIndex += 1;
assert(startIndex == _childExtents.length); assert(startIndex == _childExtents.length);
} }
assert(haveChildren == _childOffsets.last > widget.startOffset); assert(haveChildren == _childOffsets.last > startOffset);
assert(() { assert(() {
if (haveChildren) { if (haveChildren) {
// We found a child to render. It's the last one for which we have an // We found a child to render. It's the last one for which we have an
...@@ -515,8 +555,6 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -515,8 +555,6 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
// Build the other widgets that are visible. // Build the other widgets that are visible.
int index; int index;
if (haveChildren) { if (haveChildren) {
// Update the renderObject configuration
renderObject.startOffset = _childOffsets[startIndex] - widget.startOffset;
// Build all the widgets we still need. // Build all the widgets we still need.
for (index = startIndex; _childOffsets[index] < endOffset; index += 1) { for (index = startIndex; _childOffsets[index] < endOffset; index += 1) {
if (!builtChildren.containsKey(index)) { if (!builtChildren.containsKey(index)) {
......
...@@ -549,18 +549,13 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> { ...@@ -549,18 +549,13 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
// render object via our return value. // render object via our return value.
_viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width; _viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
_childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width; _childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
_updateScrollBehavior();
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
scrollTo(scrollBehavior.updateExtents( scrollTo(scrollBehavior.updateExtents(
contentExtent: _childSize, contentExtent: _childSize,
containerExtent: _viewportSize, containerExtent: _viewportSize,
scrollOffset: scrollOffset scrollOffset: scrollOffset
)); ));
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
} }
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
...@@ -668,6 +663,8 @@ abstract class ScrollableListPainter extends Painter { ...@@ -668,6 +663,8 @@ abstract class ScrollableListPainter extends Painter {
/// have the same height. Prefer [ScrollableWidgetList] when all the children /// have the same height. Prefer [ScrollableWidgetList] when all the children
/// have the same height because it can use that property to be more efficient. /// have the same height because it can use that property to be more efficient.
/// Prefer [ScrollableViewport] with a single child. /// Prefer [ScrollableViewport] with a single child.
///
/// ScrollableMixedWidgetList only supports vertical scrolling.
class ScrollableMixedWidgetList extends Scrollable { class ScrollableMixedWidgetList extends Scrollable {
ScrollableMixedWidgetList({ ScrollableMixedWidgetList({
Key key, Key key,
...@@ -684,6 +681,8 @@ class ScrollableMixedWidgetList extends Scrollable { ...@@ -684,6 +681,8 @@ class ScrollableMixedWidgetList extends Scrollable {
snapOffsetCallback: snapOffsetCallback snapOffsetCallback: snapOffsetCallback
); );
// TODO(ianh): Support horizontal scrolling.
final IndexedBuilder builder; final IndexedBuilder builder;
final Object token; final Object token;
final InvalidatorAvailableCallback onInvalidatorAvailable; final InvalidatorAvailableCallback onInvalidatorAvailable;
...@@ -702,52 +701,27 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg ...@@ -702,52 +701,27 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
OverscrollBehavior get scrollBehavior => super.scrollBehavior; OverscrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleSizeChanged(Size newSize) { Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
setState(() { // We make various state changes here but don't have to do so in a
scrollBy(scrollBehavior.updateExtents( // setState() callback because we are called during layout and all
containerExtent: newSize.height, // we're updating is the new offset, which we are providing to the
scrollOffset: scrollOffset // render object via our return value.
)); scrollTo(scrollBehavior.updateExtents(
}); contentExtent: dimensions.contentSize.height,
} containerExtent: dimensions.containerSize.height,
scrollOffset: scrollOffset
bool _contentChanged = false; ));
updateGestureDetector();
void didUpdateConfig(ScrollableMixedWidgetList oldConfig) { return scrollOffsetToPixelDelta(scrollOffset);
super.didUpdateConfig(oldConfig);
if (config.token != oldConfig.token) {
// When the token changes the scrollable's contents may have changed.
// Remember as much so that after the new contents have been laid out we
// can adjust the scrollOffset so that the last page of content is still
// visible.
_contentChanged = true;
}
}
void _handleExtentChanged(double newExtent) {
double newScrollOffset;
setState(() {
newScrollOffset = scrollBehavior.updateExtents(
contentExtent: newExtent ?? double.INFINITY,
scrollOffset: scrollOffset
);
});
if (_contentChanged) {
_contentChanged = false;
scrollTo(newScrollOffset);
}
} }
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
return new SizeObserver( return new MixedViewport(
onSizeChanged: _handleSizeChanged, startOffset: scrollOffset,
child: new MixedViewport( builder: config.builder,
startOffset: scrollOffset, token: config.token,
builder: config.builder, onInvalidatorAvailable: config.onInvalidatorAvailable,
token: config.token, onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded
onInvalidatorAvailable: config.onInvalidatorAvailable,
onExtentChanged: _handleExtentChanged
)
); );
} }
} }
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