mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Remove MixedViewport
Also, port the MixedViewport tests to LazyBlockViewport. Fix bugs found by the tests.
This commit is contained in:
parent
2383d605cd
commit
2d49e8074c
@ -1241,6 +1241,11 @@ class ErrorWidget extends LeafRenderObjectWidget {
|
||||
|
||||
@override
|
||||
RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message);
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
description.add('message: ' + _stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for instantiations of widgets that have builders and can be
|
||||
|
||||
@ -377,18 +377,8 @@ class _LazyBlockElement extends RenderObjectElement {
|
||||
renderObject.mainAxis = widget.mainAxis;
|
||||
LazyBlockDelegate newDelegate = newWidget.delegate;
|
||||
LazyBlockDelegate oldDelegate = oldWidget.delegate;
|
||||
if (newDelegate != oldDelegate && (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) {
|
||||
IndexedBuilder builder = newDelegate.buildItem;
|
||||
List<Widget> widgets = <Widget>[];
|
||||
for (int i = 0; i < widgets.length; ++i) {
|
||||
int logicalIndex = _firstChildLogicalIndex + i;
|
||||
Widget childWidget = builder(this, logicalIndex);
|
||||
if (childWidget == null)
|
||||
break;
|
||||
widgets[i] = new RepaintBoundary.wrap(childWidget, logicalIndex);
|
||||
}
|
||||
_children = new List<Element>.from(updateChildren(_children, widgets));
|
||||
}
|
||||
if (newDelegate != oldDelegate && (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
|
||||
performRebuild();
|
||||
// If the new start offset can be displayed properly with the items
|
||||
// currently represented in _children, we just need to update the paint
|
||||
// offset. Otherwise, we need to trigger a layout in order to change the
|
||||
@ -405,6 +395,21 @@ class _LazyBlockElement extends RenderObjectElement {
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
@override
|
||||
void performRebuild() {
|
||||
IndexedBuilder builder = widget.delegate.buildItem;
|
||||
List<Widget> widgets = <Widget>[];
|
||||
for (int i = 0; i < _children.length; ++i) {
|
||||
int logicalIndex = _firstChildLogicalIndex + i;
|
||||
Widget childWidget = builder(this, logicalIndex);
|
||||
if (childWidget == null)
|
||||
break;
|
||||
widgets.add(new RepaintBoundary.wrap(childWidget, logicalIndex));
|
||||
}
|
||||
_children = new List<Element>.from(updateChildren(_children, widgets));
|
||||
super.performRebuild();
|
||||
}
|
||||
|
||||
void _layout(BoxConstraints constraints) {
|
||||
final double blockExtent = _getMainAxisExtent(renderObject.size);
|
||||
|
||||
|
||||
@ -1,666 +0,0 @@
|
||||
// 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 'dart:collection';
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'framework.dart';
|
||||
import 'basic.dart';
|
||||
|
||||
typedef void InvalidatorCallback(Iterable<int> indices);
|
||||
typedef void InvalidatorAvailableCallback(InvalidatorCallback invalidator);
|
||||
|
||||
enum _ChangeDescription { none, scrolled, resized }
|
||||
|
||||
class MixedViewport extends RenderObjectWidget {
|
||||
MixedViewport({
|
||||
Key key,
|
||||
this.startOffset: 0.0,
|
||||
this.direction: Axis.vertical,
|
||||
this.builder,
|
||||
this.token,
|
||||
this.onPaintOffsetUpdateNeeded,
|
||||
this.onInvalidatorAvailable
|
||||
}) : super(key: key);
|
||||
|
||||
final double startOffset;
|
||||
final Axis direction;
|
||||
final IndexedBuilder builder;
|
||||
final Object token; // change this if the list changed (i.e. there are added, removed, or resorted items)
|
||||
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
|
||||
final InvalidatorAvailableCallback onInvalidatorAvailable; // call the callback this gives to invalidate sizes
|
||||
|
||||
@override
|
||||
_MixedViewportElement createElement() => new _MixedViewportElement(this);
|
||||
|
||||
// we don't pass constructor arguments to the RenderBlockViewport() because until
|
||||
// we know our children, the constructor arguments we could give have no effect
|
||||
@override
|
||||
RenderBlockViewport createRenderObject(BuildContext context) => new RenderBlockViewport();
|
||||
|
||||
_ChangeDescription evaluateChangesFrom(MixedViewport oldWidget) {
|
||||
if (direction != oldWidget.direction ||
|
||||
builder != oldWidget.builder ||
|
||||
token != oldWidget.token)
|
||||
return _ChangeDescription.resized;
|
||||
if (startOffset != oldWidget.startOffset)
|
||||
return _ChangeDescription.scrolled;
|
||||
return _ChangeDescription.none;
|
||||
}
|
||||
|
||||
// all the actual work is done in the element
|
||||
}
|
||||
|
||||
class _ChildKey {
|
||||
const _ChildKey(this.type, this.key);
|
||||
factory _ChildKey.fromWidget(Widget widget) => new _ChildKey(widget.runtimeType, widget.key);
|
||||
|
||||
final Type type;
|
||||
final Key key;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! _ChildKey)
|
||||
return false;
|
||||
final _ChildKey typedOther = other;
|
||||
return type == typedOther.type &&
|
||||
key == typedOther.key;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(type, key);
|
||||
|
||||
@override
|
||||
String toString() => "_ChildKey(type: $type, key: $key)";
|
||||
}
|
||||
|
||||
class _MixedViewportElement extends RenderObjectElement {
|
||||
_MixedViewportElement(MixedViewport widget) : super(widget) {
|
||||
if (widget.onInvalidatorAvailable != null)
|
||||
widget.onInvalidatorAvailable(invalidate);
|
||||
}
|
||||
|
||||
@override
|
||||
MixedViewport get widget => super.widget;
|
||||
|
||||
/// _childExtents contains the extents of each child from the top of the list
|
||||
/// up to the last one we've ever created.
|
||||
final List<double> _childExtents = <double>[];
|
||||
|
||||
/// _childOffsets contains the offsets of the top of each child from the top
|
||||
/// of the list up to the last one we've ever created, and the offset of the
|
||||
/// end of the last one. The first value is always 0.0. If there are no
|
||||
/// children, that is the only value. The offset of the end of the last child
|
||||
/// created (the actual last child, if didReachLastChild is true), is also the
|
||||
/// distance from the top (left) of the first child to the bottom (right) of
|
||||
/// the last child created.
|
||||
final List<double> _childOffsets = <double>[0.0];
|
||||
|
||||
/// Whether childOffsets includes the offset of the last child.
|
||||
bool _didReachLastChild = false;
|
||||
|
||||
/// The index of the first child whose bottom edge is below the top of the
|
||||
/// viewport.
|
||||
int _firstVisibleChildIndex;
|
||||
|
||||
/// The currently visibly children.
|
||||
Map<_ChildKey, Element> _childrenByKey = new Map<_ChildKey, Element>();
|
||||
|
||||
/// The child offsets that we've been told are invalid.
|
||||
final Set<int> _invalidIndices = new HashSet<int>();
|
||||
|
||||
/// Returns false if any of the previously-cached offsets have been marked as
|
||||
/// invalid and need to be updated.
|
||||
bool get _isValid => _invalidIndices.isEmpty;
|
||||
|
||||
/// The constraints for which the current offsets are valid.
|
||||
BoxConstraints _lastLayoutConstraints;
|
||||
|
||||
/// The last value that was sent to onPaintOffsetUpdateNeeded.
|
||||
ViewportDimensions _lastReportedDimensions;
|
||||
|
||||
double _overrideStartOffset;
|
||||
double get startOffset => _overrideStartOffset ?? widget.startOffset;
|
||||
|
||||
@override
|
||||
RenderBlockViewport get renderObject => super.renderObject;
|
||||
|
||||
/// Notify the BlockViewport that the children at indices have, or might have,
|
||||
/// changed size. Call this whenever the dimensions of a particular child
|
||||
/// change, so that the rendering will be updated accordingly. A pointer to
|
||||
/// this method is provided via the onInvalidatorAvailable callback.
|
||||
void invalidate(Iterable<int> indices) {
|
||||
assert(indices.length > 0);
|
||||
_invalidIndices.addAll(indices);
|
||||
renderObject.markNeedsLayout();
|
||||
}
|
||||
|
||||
/// Forget all the known child offsets.
|
||||
void _resetCache() {
|
||||
_childExtents.clear();
|
||||
_childOffsets.clear();
|
||||
_childOffsets.add(0.0);
|
||||
_didReachLastChild = false;
|
||||
_invalidIndices.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(ElementVisitor visitor) {
|
||||
for (Element child in _childrenByKey.values)
|
||||
visitor(child);
|
||||
}
|
||||
|
||||
@override
|
||||
void mount(Element parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
renderObject
|
||||
..mainAxis = widget.direction
|
||||
..callback = layout
|
||||
..postLayoutCallback = postLayout
|
||||
..totalExtentCallback = _noIntrinsicExtent
|
||||
..maxCrossAxisExtentCallback = _noIntrinsicExtent
|
||||
..minCrossAxisExtentCallback = _noIntrinsicExtent;
|
||||
}
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
renderObject
|
||||
..callback = null
|
||||
..postLayoutCallback = null
|
||||
..totalExtentCallback = null
|
||||
..minCrossAxisExtentCallback = null
|
||||
..maxCrossAxisExtentCallback = null;
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
double _noIntrinsicExtent(BoxConstraints constraints) {
|
||||
assert(() {
|
||||
if (!RenderObject.debugCheckingIntrinsics) {
|
||||
throw new UnsupportedError(
|
||||
'MixedViewport does not support returning intrinsic dimensions.\n'
|
||||
'Calculating the intrinsic dimensions would require walking the entire child list,\n'
|
||||
'which defeats the entire point of having a lazily-built list of children.'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
static final Object _omit = new Object(); // used as a slot when it's not yet time to attach the child
|
||||
|
||||
@override
|
||||
void update(MixedViewport newWidget) {
|
||||
_ChangeDescription changes = newWidget.evaluateChangesFrom(widget);
|
||||
super.update(newWidget);
|
||||
renderObject.mainAxis = widget.direction;
|
||||
_overrideStartOffset = null;
|
||||
if (changes == _ChangeDescription.resized)
|
||||
_resetCache();
|
||||
if (changes != _ChangeDescription.none || !_isValid) {
|
||||
// we scrolled or changed in some other potentially layout-affecting way
|
||||
renderObject.markNeedsLayout();
|
||||
} else {
|
||||
// We have to reinvoke our builders because they might return new data.
|
||||
// Consider a stateful widget that owns us. The builder it gives us
|
||||
// includes some of the state from that widget. The widget calls
|
||||
// setState() on itself. It rebuilds. Part of that involves rebuilding
|
||||
// us, but now what? If we don't reinvoke the builders. then they will
|
||||
// not be rebuilt, and so the new state won't be used. Therefore, we use
|
||||
// the object identity of the widget to determine whether to reinvoke the
|
||||
// builders.
|
||||
//
|
||||
// If the builders are to change so much that the _sizes_ of
|
||||
// the children would change, then the parent must change the 'token'.
|
||||
if (!renderObject.needsLayout)
|
||||
performRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performRebuild() {
|
||||
// we just need to redraw our existing widgets as-is
|
||||
if (_childrenByKey.length > 0) {
|
||||
assert(_firstVisibleChildIndex >= 0);
|
||||
assert(renderObject != null);
|
||||
final int startIndex = _firstVisibleChildIndex;
|
||||
int lastIndex = startIndex + _childrenByKey.length - 1;
|
||||
Element previousChild;
|
||||
for (int index = startIndex; index <= lastIndex; index += 1) {
|
||||
final Widget newWidget = _buildWidgetAt(index);
|
||||
final _ChildKey key = new _ChildKey.fromWidget(newWidget);
|
||||
final Element oldElement = _childrenByKey[key];
|
||||
assert(oldElement != null);
|
||||
final Element newElement = updateChild(oldElement, newWidget, previousChild);
|
||||
assert(newElement != null);
|
||||
_childrenByKey[key] = newElement;
|
||||
// Verify that it hasn't changed size.
|
||||
// If this assertion fires, it means you didn't call "invalidate"
|
||||
// before changing the size of one of your items.
|
||||
assert(_debugIsSameSize(newElement, index, _lastLayoutConstraints));
|
||||
previousChild = newElement;
|
||||
}
|
||||
}
|
||||
super.performRebuild();
|
||||
}
|
||||
|
||||
void layout(BoxConstraints constraints) {
|
||||
if (constraints != _lastLayoutConstraints) {
|
||||
_resetCache();
|
||||
_lastLayoutConstraints = constraints;
|
||||
}
|
||||
owner.lockState(() {
|
||||
_doLayout(constraints);
|
||||
}, building: true, context: 'during $runtimeType layout');
|
||||
}
|
||||
|
||||
void postLayout() {
|
||||
assert(renderObject.hasSize);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary search to find the index of the child responsible for rendering a given pixel
|
||||
int _findIndexForOffsetBeforeOrAt(double offset) {
|
||||
int left = 0;
|
||||
int right = _childOffsets.length - 1;
|
||||
while (right >= left) {
|
||||
int middle = left + ((right - left) ~/ 2);
|
||||
if (_childOffsets[middle] < offset) {
|
||||
left = middle + 1;
|
||||
} else if (_childOffsets[middle] > offset) {
|
||||
right = middle - 1;
|
||||
} else {
|
||||
return middle;
|
||||
}
|
||||
}
|
||||
return right;
|
||||
}
|
||||
|
||||
/// Calls the builder. This is for the case where you don't know if you have a child at this index.
|
||||
Widget _maybeBuildWidgetAt(int index) {
|
||||
if (widget.builder == null)
|
||||
return null;
|
||||
final Widget newWidget = widget.builder(this, index);
|
||||
assert(() {
|
||||
'Every widget in a list must have a list-unique key.';
|
||||
return newWidget == null || newWidget.key != null;
|
||||
});
|
||||
return newWidget;
|
||||
}
|
||||
|
||||
/// Calls the builder. This is for the case where you know that you should have a child there.
|
||||
Widget _buildWidgetAt(int index) {
|
||||
final Widget newWidget = widget.builder(this, index);
|
||||
assert(newWidget != null);
|
||||
assert(newWidget.key != null); // every widget in a list must have a list-unique key
|
||||
return newWidget;
|
||||
}
|
||||
|
||||
/// Given an element configuration, inflates the element, updating the existing one if there was one.
|
||||
/// Returns the resulting element.
|
||||
Element _inflateOrUpdateWidget(Widget newWidget) {
|
||||
final _ChildKey key = new _ChildKey.fromWidget(newWidget);
|
||||
final Element oldElement = _childrenByKey[key];
|
||||
final Element newElement = updateChild(oldElement, newWidget, _omit);
|
||||
assert(newElement != null);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
// Build the widget at index.
|
||||
Element _getElement(int index, BoxConstraints innerConstraints) {
|
||||
assert(index <= _childOffsets.length - 1);
|
||||
final Widget newWidget = _buildWidgetAt(index);
|
||||
return _inflateOrUpdateWidget(newWidget);
|
||||
}
|
||||
|
||||
// Build the widget at index.
|
||||
Element _maybeGetElement(int index, BoxConstraints innerConstraints) {
|
||||
assert(index <= _childOffsets.length - 1);
|
||||
final Widget newWidget = _maybeBuildWidgetAt(index);
|
||||
if (newWidget == null)
|
||||
return null;
|
||||
return _inflateOrUpdateWidget(newWidget);
|
||||
}
|
||||
|
||||
// Build the widget at index, handling the case where there is no such widget.
|
||||
// Update the offset for that widget.
|
||||
Element _getElementAtLastKnownOffset(int index, BoxConstraints innerConstraints) {
|
||||
|
||||
// Inflate the new widget; if there isn't one, abort early.
|
||||
assert(index == _childOffsets.length - 1);
|
||||
final Widget newWidget = _maybeBuildWidgetAt(index);
|
||||
if (newWidget == null)
|
||||
return null;
|
||||
final Element newElement = _inflateOrUpdateWidget(newWidget);
|
||||
|
||||
// Update the offsets based on the newElement's dimensions.
|
||||
final double newExtent = _getElementExtent(newElement, innerConstraints);
|
||||
_childExtents.add(newExtent);
|
||||
_childOffsets.add(_childOffsets[index] + newExtent);
|
||||
assert(_childExtents.length == _childOffsets.length - 1);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
|
||||
/// Returns the intrinsic size of the given element in the scroll direction
|
||||
double _getElementExtent(Element element, BoxConstraints innerConstraints) {
|
||||
final RenderBox childRenderObject = element.renderObject;
|
||||
switch (widget.direction) {
|
||||
case Axis.vertical:
|
||||
return childRenderObject.getMaxIntrinsicHeight(innerConstraints);
|
||||
case Axis.horizontal:
|
||||
return childRenderObject.getMaxIntrinsicWidth(innerConstraints);
|
||||
}
|
||||
}
|
||||
|
||||
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
|
||||
switch (widget.direction) {
|
||||
case Axis.vertical:
|
||||
return new BoxConstraints.tightFor(width: constraints.constrainWidth());
|
||||
case Axis.horizontal:
|
||||
return new BoxConstraints.tightFor(height: constraints.constrainHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/// This compares the offsets we had for an element with its current
|
||||
/// intrinsic dimensions.
|
||||
bool _debugIsSameSize(Element element, int index, BoxConstraints constraints) {
|
||||
assert(_invalidIndices.isEmpty);
|
||||
BoxConstraints innerConstraints = _getInnerConstraints(constraints);
|
||||
double newExtent = _getElementExtent(element, innerConstraints);
|
||||
bool result = _childExtents[index] == newExtent;
|
||||
if (!result)
|
||||
debugPrint("Element $element at index $index was size ${_childExtents[index]} but is now size $newExtent yet no invalidate() was received to that effect");
|
||||
return result;
|
||||
}
|
||||
|
||||
double _getMaxExtent(BoxConstraints constraints) {
|
||||
switch (widget.direction) {
|
||||
case Axis.vertical:
|
||||
assert(constraints.maxHeight < double.INFINITY &&
|
||||
'There is no point putting a lazily-built vertical MixedViewport inside a box with infinite internal ' +
|
||||
'height (e.g. inside something else that scrolls vertically), because it would then just eagerly build ' +
|
||||
'all the children. You probably want to put the MixedViewport inside a Container with a fixed height.' is String);
|
||||
return constraints.maxHeight;
|
||||
case Axis.horizontal:
|
||||
assert(constraints.maxWidth < double.INFINITY &&
|
||||
'There is no point putting a lazily-built horizontal MixedViewport inside a box with infinite internal ' +
|
||||
'width (e.g. inside something else that scrolls horizontally), because it would then just eagerly build ' +
|
||||
'all the children. You probably want to put the MixedViewport inside a Container with a fixed width.' is String);
|
||||
return constraints.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the core lazy-build algorithm. It builds widgets incrementally
|
||||
/// from index 0 until it has built enough widgets to cover itself, and
|
||||
/// discards any widgets that are not displayed.
|
||||
void _doLayout(BoxConstraints constraints) {
|
||||
final Map<_ChildKey, Element> newChildren = new Map<_ChildKey, Element>();
|
||||
final Map<int, Element> builtChildren = new Map<int, Element>();
|
||||
|
||||
// Establish the start and end offsets based on our current constraints.
|
||||
final double endOffset = startOffset + _getMaxExtent(constraints);
|
||||
|
||||
// Create the constraints that we will use to measure the children.
|
||||
final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
|
||||
|
||||
// Before doing the actual layout, fix the offsets for the widgets whose
|
||||
// size has apparently changed.
|
||||
if (!_isValid) {
|
||||
assert(_childOffsets.length > 0);
|
||||
assert(_childOffsets.length == _childExtents.length + 1);
|
||||
List<int> invalidIndices = _invalidIndices.toList();
|
||||
invalidIndices.sort();
|
||||
for (int i = 0; i < invalidIndices.length; i += 1) {
|
||||
|
||||
// Determine the indices for this pass.
|
||||
final int widgetIndex = invalidIndices[i];
|
||||
if (widgetIndex >= _childExtents.length)
|
||||
break; // we don't have that child, so there's nothing to invalidate
|
||||
int endIndex; // the last index into _childOffsets that we want to update this round
|
||||
if (i == invalidIndices.length - 1) {
|
||||
// This is the last invalid index. Update all the remaining entries in _childOffsets.
|
||||
endIndex = _childOffsets.length - 1;
|
||||
} else {
|
||||
endIndex = invalidIndices[i + 1];
|
||||
if (endIndex > _childOffsets.length - 1)
|
||||
endIndex = _childOffsets.length - 1; // no point updating beyond the last offset we know of
|
||||
}
|
||||
assert(widgetIndex >= 0);
|
||||
assert(endIndex < _childOffsets.length);
|
||||
assert(widgetIndex < endIndex);
|
||||
|
||||
// Inflate the widget or update the existing element, as necessary.
|
||||
final Element newElement = _getElement(widgetIndex, innerConstraints);
|
||||
|
||||
// Update the offsets based on the newElement's dimensions.
|
||||
_childExtents[widgetIndex] = _getElementExtent(newElement, innerConstraints);
|
||||
for (int j = widgetIndex + 1; j <= endIndex; j++)
|
||||
_childOffsets[j] = _childOffsets[j - 1] + _childExtents[j - 1];
|
||||
assert(_childOffsets.length == _childExtents.length + 1);
|
||||
|
||||
// Decide if it's visible.
|
||||
final _ChildKey key = new _ChildKey.fromWidget(newElement.widget);
|
||||
final bool isVisible = _childOffsets[widgetIndex] < endOffset && _childOffsets[widgetIndex + 1] >= startOffset;
|
||||
if (isVisible) {
|
||||
// Keep it.
|
||||
newChildren[key] = newElement;
|
||||
builtChildren[widgetIndex] = newElement;
|
||||
} else {
|
||||
// Drop it.
|
||||
_childrenByKey.remove(key);
|
||||
updateChild(newElement, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
_invalidIndices.clear();
|
||||
}
|
||||
|
||||
// Decide what the first child to render should be (startIndex), if any (haveChildren).
|
||||
int startIndex;
|
||||
bool haveChildren;
|
||||
if (endOffset < 0.0) {
|
||||
// We're so far scrolled up that nothing is visible.
|
||||
haveChildren = false;
|
||||
} else if (startOffset <= 0.0) {
|
||||
startIndex = 0;
|
||||
// If we're scrolled up past the top, then our first visible widget, if
|
||||
// any, is the first widget.
|
||||
if (_childExtents.length > 0) {
|
||||
haveChildren = true;
|
||||
} else {
|
||||
final Element element = _getElementAtLastKnownOffset(startIndex, innerConstraints);
|
||||
if (element != null) {
|
||||
newChildren[new _ChildKey.fromWidget(element.widget)] = element;
|
||||
builtChildren[startIndex] = element;
|
||||
haveChildren = true;
|
||||
} else {
|
||||
haveChildren = false;
|
||||
_didReachLastChild = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We're at some sane (not higher than the top) scroll offset.
|
||||
// See if we can already find the offset in our cache.
|
||||
startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
|
||||
if (startIndex < _childExtents.length) {
|
||||
// We already know of a child that would be visible at this offset.
|
||||
haveChildren = true;
|
||||
} else {
|
||||
// We don't have an offset on the list that is beyond the start offset.
|
||||
assert(_childOffsets.last <= startOffset);
|
||||
// Fill the list until this isn't true or until we know that the
|
||||
// list is complete (and thus we are overscrolled).
|
||||
while (true) {
|
||||
// Get the next element and cache its offset.
|
||||
final Element element = _getElementAtLastKnownOffset(startIndex, innerConstraints);
|
||||
if (element == null) {
|
||||
// Reached the end of the list. We are so far overscrolled, there's nothing to show.
|
||||
_didReachLastChild = true;
|
||||
haveChildren = false;
|
||||
break;
|
||||
}
|
||||
final _ChildKey key = new _ChildKey.fromWidget(element.widget);
|
||||
if (_childOffsets.last > startOffset) {
|
||||
// This element is visible! It must thus be our first visible child.
|
||||
newChildren[key] = element;
|
||||
builtChildren[startIndex] = element;
|
||||
haveChildren = true;
|
||||
break;
|
||||
}
|
||||
// This element is not visible. Drop the inflated element.
|
||||
// (We've already cached its offset for later use.)
|
||||
_childrenByKey.remove(key);
|
||||
updateChild(element, null, null);
|
||||
startIndex += 1;
|
||||
assert(startIndex == _childExtents.length);
|
||||
}
|
||||
assert(haveChildren == _childOffsets.last > startOffset);
|
||||
assert(() {
|
||||
if (haveChildren) {
|
||||
// We found a child to render. It's the last one for which we have an
|
||||
// offset in _childOffsets.
|
||||
// If we're here, we have at least one child, so our list has
|
||||
// at least two offsets, the top of the child and the bottom
|
||||
// of the child.
|
||||
assert(_childExtents.length >= 1);
|
||||
assert(_childOffsets.length == _childExtents.length + 1);
|
||||
assert(startIndex == _childExtents.length - 1);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
assert(haveChildren != null);
|
||||
assert(haveChildren || _didReachLastChild || endOffset < 0.0);
|
||||
assert(startIndex >= 0);
|
||||
assert(!haveChildren || startIndex < _childExtents.length);
|
||||
|
||||
// Build the other widgets that are visible.
|
||||
int index;
|
||||
if (haveChildren) {
|
||||
// Build all the widgets we still need.
|
||||
for (index = startIndex; _childOffsets[index] < endOffset; index += 1) {
|
||||
if (!builtChildren.containsKey(index)) {
|
||||
Element element = _maybeGetElement(index, innerConstraints);
|
||||
if (element == null) {
|
||||
_didReachLastChild = true;
|
||||
break;
|
||||
}
|
||||
if (index == _childExtents.length) {
|
||||
// Remember this element's offset.
|
||||
final double newExtent = _getElementExtent(element, innerConstraints);
|
||||
_childExtents.add(newExtent);
|
||||
_childOffsets.add(_childOffsets[index] + newExtent);
|
||||
assert(_childOffsets.length == _childExtents.length + 1);
|
||||
} else {
|
||||
// Verify that it hasn't changed size.
|
||||
// If this assertion fires, it means you didn't call "invalidate"
|
||||
// before changing the size of one of your items.
|
||||
assert(_debugIsSameSize(element, index, constraints));
|
||||
}
|
||||
// Remember the element for when we place the children.
|
||||
final _ChildKey key = new _ChildKey.fromWidget(element.widget);
|
||||
newChildren[key] = element;
|
||||
builtChildren[index] = element;
|
||||
}
|
||||
assert(builtChildren[index] != null);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any old children.
|
||||
for (_ChildKey oldChildKey in _childrenByKey.keys) {
|
||||
if (!newChildren.containsKey(oldChildKey))
|
||||
updateChild(_childrenByKey[oldChildKey], null, null);
|
||||
}
|
||||
|
||||
if (haveChildren) {
|
||||
assert(index != null);
|
||||
// Place all our children in our RenderObject.
|
||||
// All the children we are placing are in builtChildren and newChildren.
|
||||
Element previousChild;
|
||||
for (int i = startIndex; i < index; ++i) {
|
||||
final Element element = builtChildren[i];
|
||||
if (element.slot != previousChild)
|
||||
updateSlotForChild(element, previousChild);
|
||||
previousChild = element;
|
||||
}
|
||||
}
|
||||
|
||||
// Update our internal state.
|
||||
_childrenByKey = newChildren;
|
||||
_firstVisibleChildIndex = startIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSlotForChild(Element element, dynamic newSlot) {
|
||||
assert(newSlot == null || newSlot == _omit || newSlot is Element);
|
||||
super.updateSlotForChild(element, newSlot);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertChildRenderObject(RenderObject child, dynamic slot) {
|
||||
if (slot == _omit)
|
||||
return;
|
||||
assert(slot == null || slot is Element);
|
||||
renderObject.insert(child, after: slot?.renderObject);
|
||||
}
|
||||
|
||||
@override
|
||||
void moveChildRenderObject(RenderObject child, dynamic slot) {
|
||||
if (slot == _omit)
|
||||
return;
|
||||
assert(slot == null || slot is Element);
|
||||
RenderObject previousSibling = slot?.renderObject;
|
||||
assert(previousSibling == null || previousSibling.parent == renderObject);
|
||||
if (child.parent == renderObject)
|
||||
renderObject.move(child, after: previousSibling);
|
||||
else
|
||||
renderObject.insert(child, after: previousSibling);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeChildRenderObject(RenderObject child) {
|
||||
if (child.parent != renderObject)
|
||||
return; // probably had slot == _omit when inserted
|
||||
renderObject.remove(child);
|
||||
}
|
||||
|
||||
}
|
||||
@ -13,7 +13,6 @@ import 'package:flutter/rendering.dart' show HasMainAxis;
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'mixed_viewport.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'page_storage.dart';
|
||||
import 'scroll_behavior.dart';
|
||||
@ -612,11 +611,10 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
||||
/// The widgets used by this method should be widgets that provide a
|
||||
/// layout-time callback that reports the sizes that are relevant to
|
||||
/// the scroll offset (typically the size of the scrollable
|
||||
/// container and the scrolled contents). [Viewport] and
|
||||
/// [MixedViewport] provide an [onPaintOffsetUpdateNeeded] callback
|
||||
/// for this purpose; [GridViewport], [ListViewport], and
|
||||
/// [LazyListViewport] provide an [onExtentsChanged] callback for
|
||||
/// this purpose.
|
||||
/// container and the scrolled contents). [Viewport] provides an
|
||||
/// [onPaintOffsetUpdateNeeded] callback for this purpose; [GridViewport],
|
||||
/// [ListViewport], [LazyListViewport], and [LazyBlockViewport] provide an
|
||||
/// [onExtentsChanged] callback for this purpose.
|
||||
///
|
||||
/// This callback should be used to update the scroll behavior, if
|
||||
/// necessary, and then to call [updateGestureDetector] to update
|
||||
@ -705,6 +703,10 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
|
||||
/// A mashup of [ScrollableViewport] and [BlockBody]. Useful when you have a small,
|
||||
/// fixed number of children that you wish to arrange in a block layout and that
|
||||
/// might exceed the height of its container (and therefore need to scroll).
|
||||
///
|
||||
/// If you have a large number of children, consider using [LazyBlock] (if the
|
||||
/// children have variable height) or [ScrollableList] (if the children all have
|
||||
/// the same fixed height).
|
||||
class Block extends StatelessWidget {
|
||||
Block({
|
||||
Key key,
|
||||
@ -794,76 +796,3 @@ abstract class ScrollableListPainter extends RenderObjectPainter {
|
||||
/// or was canceled by the user.
|
||||
Future<Null> scrollEnded() => new Future<Null>.value();
|
||||
}
|
||||
|
||||
/// A general scrollable list for a large number of children that might not all
|
||||
/// have the same height. Prefer [ScrollableList] when all the children
|
||||
/// have the same height because it can use that property to be more efficient.
|
||||
/// Prefer [ScrollableViewport] with a single child.
|
||||
///
|
||||
/// ScrollableMixedWidgetList only supports vertical scrolling.
|
||||
class ScrollableMixedWidgetList extends Scrollable {
|
||||
ScrollableMixedWidgetList({
|
||||
Key key,
|
||||
double initialScrollOffset,
|
||||
ScrollListener onScroll,
|
||||
SnapOffsetCallback snapOffsetCallback,
|
||||
this.builder,
|
||||
this.token,
|
||||
this.onInvalidatorAvailable
|
||||
}) : super(
|
||||
key: key,
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
onScroll: onScroll,
|
||||
snapOffsetCallback: snapOffsetCallback
|
||||
);
|
||||
|
||||
// TODO(ianh): Support horizontal scrolling.
|
||||
|
||||
final IndexedBuilder builder;
|
||||
final Object token;
|
||||
final InvalidatorAvailableCallback onInvalidatorAvailable;
|
||||
|
||||
@override
|
||||
ScrollableMixedWidgetListState createState() => new ScrollableMixedWidgetListState();
|
||||
}
|
||||
|
||||
class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidgetList> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollBehavior.updateExtents(
|
||||
contentExtent: double.INFINITY
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
OverscrollBehavior createScrollBehavior() => new OverscrollBehavior();
|
||||
|
||||
@override
|
||||
OverscrollBehavior get scrollBehavior => super.scrollBehavior;
|
||||
|
||||
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
|
||||
// We make various state changes here but don't have to do so in a
|
||||
// setState() callback because we are called during layout and all
|
||||
// we're updating is the new offset, which we are providing to the
|
||||
// render object via our return value.
|
||||
didUpdateScrollBehavior(scrollBehavior.updateExtents(
|
||||
contentExtent: dimensions.contentSize.height,
|
||||
containerExtent: dimensions.containerSize.height,
|
||||
scrollOffset: scrollOffset
|
||||
));
|
||||
updateGestureDetector();
|
||||
return scrollOffsetToPixelDelta(scrollOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
return new MixedViewport(
|
||||
startOffset: scrollOffset,
|
||||
builder: config.builder,
|
||||
token: config.token,
|
||||
onInvalidatorAvailable: config.onInvalidatorAvailable,
|
||||
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ export 'src/widgets/lazy_block.dart';
|
||||
export 'src/widgets/locale_query.dart';
|
||||
export 'src/widgets/media_query.dart';
|
||||
export 'src/widgets/mimic.dart';
|
||||
export 'src/widgets/mixed_viewport.dart';
|
||||
export 'src/widgets/modal_barrier.dart';
|
||||
export 'src/widgets/navigator.dart';
|
||||
export 'src/widgets/notification_listener.dart';
|
||||
|
||||
@ -9,7 +9,7 @@ import 'package:test/test.dart';
|
||||
import 'test_widgets.dart';
|
||||
|
||||
void main() {
|
||||
test('MixedViewport mount/dismount smoke test', () {
|
||||
test('LazyBlockViewport mount/dismount smoke test', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<int> callbackTracker = <int>[];
|
||||
|
||||
@ -18,15 +18,15 @@ void main() {
|
||||
|
||||
Widget builder() {
|
||||
return new FlipWidget(
|
||||
left: new MixedViewport(
|
||||
builder: (BuildContext context, int i) {
|
||||
left: new LazyBlockViewport(
|
||||
delegate: new LazyBlockBuilder(builder: (BuildContext context, int i) {
|
||||
callbackTracker.add(i);
|
||||
return new Container(
|
||||
key: new ValueKey<int>(i),
|
||||
height: 100.0,
|
||||
child: new Text("$i")
|
||||
);
|
||||
},
|
||||
}),
|
||||
startOffset: 0.0
|
||||
),
|
||||
right: new Text('Not Today')
|
||||
@ -54,7 +54,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('MixedViewport vertical', () {
|
||||
test('LazyBlockViewport vertical', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<int> callbackTracker = <int>[];
|
||||
|
||||
@ -76,8 +76,8 @@ void main() {
|
||||
|
||||
Widget builder() {
|
||||
return new FlipWidget(
|
||||
left: new MixedViewport(
|
||||
builder: itemBuilder,
|
||||
left: new LazyBlockViewport(
|
||||
delegate: new LazyBlockBuilder(builder: itemBuilder),
|
||||
startOffset: offset
|
||||
),
|
||||
right: new Text('Not Today')
|
||||
@ -86,23 +86,27 @@ void main() {
|
||||
|
||||
tester.pumpWidget(builder());
|
||||
|
||||
// 0 is built to find its width
|
||||
// 0 is built to find its height
|
||||
expect(callbackTracker, equals([0, 1, 2, 3, 4]));
|
||||
|
||||
callbackTracker.clear();
|
||||
|
||||
offset = 400.0; // now only 3 should fit, numbered 2-4.
|
||||
|
||||
tester.pumpWidget(builder());
|
||||
|
||||
// 0 and 1 aren't built, we know their size and nothing else changed
|
||||
expect(callbackTracker, equals([2, 3, 4]));
|
||||
// We build all the children to find their new size.
|
||||
expect(callbackTracker, equals([0, 1, 2, 3, 4]));
|
||||
callbackTracker.clear();
|
||||
|
||||
tester.pumpWidget(builder());
|
||||
|
||||
// 0 isn't built because they're not visible.
|
||||
expect(callbackTracker, equals([1, 2, 3, 4]));
|
||||
callbackTracker.clear();
|
||||
});
|
||||
});
|
||||
|
||||
test('MixedViewport horizontal', () {
|
||||
test('LazyBlockViewport horizontal', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<int> callbackTracker = <int>[];
|
||||
|
||||
@ -124,10 +128,10 @@ void main() {
|
||||
|
||||
Widget builder() {
|
||||
return new FlipWidget(
|
||||
left: new MixedViewport(
|
||||
builder: itemBuilder,
|
||||
left: new LazyBlockViewport(
|
||||
delegate: new LazyBlockBuilder(builder: itemBuilder),
|
||||
startOffset: offset,
|
||||
direction: Axis.horizontal
|
||||
mainAxis: Axis.horizontal
|
||||
),
|
||||
right: new Text('Not Today')
|
||||
);
|
||||
@ -144,14 +148,19 @@ void main() {
|
||||
|
||||
tester.pumpWidget(builder());
|
||||
|
||||
// 0 and 1 aren't built, we know their size and nothing else changed
|
||||
expect(callbackTracker, equals([2, 3, 4, 5]));
|
||||
// We build all the children to find their new size.
|
||||
expect(callbackTracker, equals([0, 1, 2, 3, 4, 5]));
|
||||
callbackTracker.clear();
|
||||
|
||||
tester.pumpWidget(builder());
|
||||
|
||||
// 0 isn't built because they're not visible.
|
||||
expect(callbackTracker, equals([1, 2, 3, 4, 5]));
|
||||
callbackTracker.clear();
|
||||
});
|
||||
});
|
||||
|
||||
test('MixedViewport reinvoke builders', () {
|
||||
test('LazyBlockViewport reinvoke builders', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<int> callbackTracker = <int>[];
|
||||
List<String> text = <String>[];
|
||||
@ -173,8 +182,8 @@ void main() {
|
||||
};
|
||||
|
||||
Widget builder() {
|
||||
return new MixedViewport(
|
||||
builder: itemBuilder,
|
||||
return new LazyBlockViewport(
|
||||
delegate: new LazyBlockBuilder(builder: itemBuilder),
|
||||
startOffset: 0.0
|
||||
);
|
||||
}
|
||||
@ -197,7 +206,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('MixedViewport reinvoke builders', () {
|
||||
test('LazyBlockViewport reinvoke builders', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
StateSetter setState;
|
||||
ThemeData themeData = new ThemeData.light();
|
||||
@ -214,7 +223,9 @@ void main() {
|
||||
);
|
||||
};
|
||||
|
||||
Widget viewport = new MixedViewport(builder: itemBuilder);
|
||||
Widget viewport = new LazyBlockViewport(
|
||||
delegate: new LazyBlockBuilder(builder: itemBuilder)
|
||||
);
|
||||
|
||||
tester.pumpWidget(
|
||||
new StatefulBuilder(
|
||||
@ -28,7 +28,7 @@ Widget buildFrame() {
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('MixedViewport is a build function (smoketest)', () {
|
||||
test('LazyBlock is a build function (smoketest)', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
tester.pumpWidget(buildFrame());
|
||||
expect(tester.findText('0'), isNotNull);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user