SizeObserver crusade: ScrollableMixedWidgetListState
This commit is contained in:
parent
b18047bfb3
commit
438f209088
@ -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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
/// 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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
_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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
// 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> {
|
|||||||
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> {
|
|||||||
} 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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
// 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> {
|
|||||||
// 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 {
|
|||||||
/// 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 {
|
|||||||
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
|
|||||||
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
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user