Commit 098af183 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add SliverList (#7727)

Add SliverList

A SliverList is a linear layout of box children in a viewport that all
have a common, fixed extent along the scroll axis. The layout is similar
to a SliverBlock but more efficient.
parent 6e30cae8
...@@ -47,6 +47,8 @@ export 'src/rendering/shifted_box.dart'; ...@@ -47,6 +47,8 @@ export 'src/rendering/shifted_box.dart';
export 'src/rendering/sliver.dart'; export 'src/rendering/sliver.dart';
export 'src/rendering/sliver_app_bar.dart'; export 'src/rendering/sliver_app_bar.dart';
export 'src/rendering/sliver_block.dart'; export 'src/rendering/sliver_block.dart';
export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart'; export 'src/rendering/sliver_padding.dart';
export 'src/rendering/stack.dart'; export 'src/rendering/stack.dart';
export 'src/rendering/table.dart'; export 'src/rendering/table.dart';
......
// Copyright 2016 The Chromium Authors. All rights reserved. // Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart'; import 'box.dart';
import 'binding.dart';
import 'object.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';
class SliverBlockParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderBox> { class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
int index; RenderSliverBlock({
@required RenderSliverBoxChildManager childManager
@override }) : super(childManager: childManager);
String toString() => 'index=$index; ${super.toString()}';
}
// ///
// ///
// /// The contract for adding and removing children from this render object is
// /// more strict than for normal render objects:
// ///
// /// - Children can be removed except during a layout pass if they have already
// /// been laid out during that layout pass.
// /// - Children cannot be added except during a call to [allowAdditionsFor], and
// /// then only if there is no child correspending to that index (or the child
// /// child corresponding to that index was first removed).
// ///
// /// ## Writing a RenderSliverBlock subclass
// ///
// /// There are three methods to override:
// ///
// /// - [createChild], which must create the child for the given index, and then
// /// insert it in the given location in the child list.
// ///
// /// - [removeChild], which is called when the given child is cleaned up,
// /// and which should remove the given child from the child list.
// ///
// /// - [estimateScrollOffsetExtent], which should return the total extent (e.g.
// /// the height, if this is a vertical block) of all the children.
abstract class RenderSliverBlock extends RenderSliver
with ContainerRenderObjectMixin<RenderBox, SliverBlockParentData>,
RenderSliverHelpers {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverBlockParentData)
child.parentData = new SliverBlockParentData();
}
/// Called during layout when a new child is needed. The child should be
/// inserted into the child list in the appropriate position, after the
/// `after` child (at the start of the list if `after` is null). Its index and
/// scroll offsets will automatically be set appropriately.
///
/// The `index` argument gives the index of the child to show. The first child
/// that will be requested will have index zero, and this should be the child
/// that is aligned with the zero scroll offset. Subsequent requests will be
/// for indices adjacent to previously requested indices. It is possible for
/// negative indices to be requested. For example: if the user scrolls from
/// child 0 to child 10, and then those children get much smaller, and then
/// the user scrolls back up again, this method will eventually be asked to
/// produce a child for index -1.
///
/// If no child corresponds to `index`, then do nothing.
///
/// Which child is indicated by index zero depends on the [GrowthDirection]
/// specified in the [constraints]. For example if the children are the
/// alphabet, then if [constraints.growthDirection] is
/// [GrowthDirection.forward] then index zero is A, and index 25 is Z. On the
/// other hand if [constraints.growthDirection] is [GrowthDirection.reverse]
/// then index zero is Z, and index 25 is A.
///
/// During a call to [createChild] it is valid to remove other children from
/// the [RenderSliverBlock] object if they were not created during this frame
/// and have not yet been updated during this frame. It is not valid to add
/// any children to this render object.
@protected
void createChild(int index, { @required RenderBox after });
/// Remove the given child from the child list.
///
/// Called by [collectGarbage], which itself is called from [performLayout],
/// after the layout algorithm has finished and the non-visible children are
/// to be removed.
///
/// The default implementation calls [remove], which removes the child from
/// the child list.
///
/// The index of the given child can be obtained using the [indexOf] method,
/// which reads it from the [SliverBlockParentData.index] field of the child's
/// [parentData].
@protected
void removeChild(RenderBox child) {
remove(child);
}
int _currentlyUpdatingChildIndex;
@protected
void allowAdditionsFor(int index, VoidCallback callback) {
assert(_currentlyUpdatingChildIndex == null);
assert(index != null);
_currentlyUpdatingChildIndex = index;
try {
callback();
} finally {
_currentlyUpdatingChildIndex = null;
}
}
@override
void adoptChild(RenderObject child) {
assert(_currentlyUpdatingChildIndex != null);
super.adoptChild(child);
final SliverBlockParentData childParentData = child.parentData;
childParentData.index = _currentlyUpdatingChildIndex;
}
@override
void insert(RenderBox child, { RenderBox after }) {
super.insert(child, after: after);
assert(firstChild != null);
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
assert(indexOf(child) > index);
index = indexOf(child);
child = childAfter(child);
}
return true;
});
}
/// Called to estimate the total scrollable extents of this object.
///
/// Must return the total distance from the start of the child with the
/// earliest possible index to the end of the child with the last possible
/// index.
@protected
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
});
/// Called during layout to create and add the child with index 0 and scroll
/// offset 0.0.
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns false if createChild did not add any child, otherwise returns
/// true.
///
/// Does not layout the new child.
///
/// When this is called, there are no children, so no children can be removed
/// during the call to createChild. No child should be added during that call
/// either, except for the one that is created and returned by [createChild].
@protected
bool addInitialChild() {
assert(_currentlyUpdatingChildIndex == null);
assert(firstChild == null);
bool result;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(0, () {
createChild(0, after: null);
if (firstChild != null) {
assert(firstChild == lastChild);
assert(indexOf(firstChild) == 0);
final SliverBlockParentData firstChildParentData = firstChild.parentData;
firstChildParentData.scrollOffset = 0.0;
result = true;
} else {
result = false;
}
});
});
return result;
}
/// Called during layout to create, add, and layout the child before
/// [firstChild].
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns the new child.
///
/// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except
/// for the one that is created and returned by [createChild].
@protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints) {
assert(_currentlyUpdatingChildIndex == null);
final int index = indexOf(firstChild) - 1;
final double endScrollOffset = offsetOf(firstChild);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(index, () {
createChild(index, after: null);
});
});
if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: true);
SliverBlockParentData firstChildParentData = firstChild.parentData;
firstChildParentData.scrollOffset = endScrollOffset - paintExtentOf(firstChild);
return firstChild;
}
return null;
}
/// Called during layout to create, add, and layout the child after
/// the given child.
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset.
///
/// Children after the `after` child may be removed in the process. Only the
/// new child may be added.
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, { @required RenderBox after }) {
assert(_currentlyUpdatingChildIndex == null);
assert(after != null);
final int index = indexOf(after) + 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(index, () {
createChild(index, after: after);
});
});
final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) {
assert(indexOf(child) == index);
child.layout(childConstraints, parentUsesSize: true);
return child;
}
return null;
}
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_currentlyUpdatingChildIndex == null);
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
removeChild(firstChild);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
removeChild(lastChild);
trailingGarbage -= 1;
}
});
}
/// Returns the index of the given child, as given by the
/// [SliverBlockParentData.index] field of the child's [parentData].
@protected
int indexOf(RenderBox child) {
assert(child != null);
final SliverBlockParentData childParentData = child.parentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the scroll offset of the given child, as given by the
/// [SliverBlockParentData.scrollOffset] field of the child's [parentData].
@protected
double offsetOf(RenderBox child) {
assert(child != null);
final SliverBlockParentData childParentData = child.parentData;
assert(childParentData.scrollOffset != null);
return childParentData.scrollOffset;
}
/// Returns the dimension of the given child in the main axis, as given by the
/// child's [RenderBox.size] property. This is only valid after layout.
@protected
double paintExtentOf(RenderBox child) {
assert(child != null);
assert(child.hasSize);
switch (constraints.axis) {
case Axis.horizontal:
return child.size.width;
case Axis.vertical:
return child.size.height;
}
return null;
}
@override @override
void performLayout() { void performLayout() {
assert(_currentlyUpdatingChildIndex == null); assert(childManager.debugAssertChildListLocked());
double scrollOffset = constraints.scrollOffset; double scrollOffset = constraints.scrollOffset;
assert(scrollOffset >= 0.0); assert(scrollOffset >= 0.0);
double remainingPaintExtent = constraints.remainingPaintExtent; double remainingPaintExtent = constraints.remainingPaintExtent;
...@@ -332,11 +47,7 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -332,11 +47,7 @@ abstract class RenderSliverBlock extends RenderSliver
if (firstChild == null) { if (firstChild == null) {
if (!addInitialChild()) { if (!addInitialChild()) {
// There are no children. // There are no children.
geometry = new SliverGeometry( geometry = SliverGeometry.zero;
scrollExtent: 0.0,
paintExtent: 0.0,
maxPaintExtent: 0.0,
);
return; return;
} }
} }
...@@ -350,9 +61,11 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -350,9 +61,11 @@ abstract class RenderSliverBlock extends RenderSliver
// Find the last child that is at or before the scrollOffset. // Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild; RenderBox earliestUsefulChild = firstChild;
while (offsetOf(earliestUsefulChild) > scrollOffset) { for (double earliestScrollOffset = offsetOf(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = offsetOf(earliestUsefulChild)) {
// We have to add children before the earliestUsefulChild. // We have to add children before the earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints); earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) { if (earliestUsefulChild == null) {
// We ran out of children before reaching the scroll offset. // We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill // We must inform our parent that this sliver cannot fulfill
...@@ -362,6 +75,8 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -362,6 +75,8 @@ abstract class RenderSliverBlock extends RenderSliver
); );
return; return;
} }
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData;
childParentData.scrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
assert(earliestUsefulChild == firstChild); assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild; leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild; trailingChildWithLayout ??= earliestUsefulChild;
...@@ -405,7 +120,10 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -405,7 +120,10 @@ abstract class RenderSliverBlock extends RenderSliver
if (!inLayoutRange) { if (!inLayoutRange) {
if (child == null || indexOf(child) != index) { if (child == null || indexOf(child) != index) {
// We are missing a child. Insert it (and lay it out) if possible. // We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) { if (child == null) {
// We have run out of children. // We have run out of children.
return false; return false;
...@@ -417,7 +135,7 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -417,7 +135,7 @@ abstract class RenderSliverBlock extends RenderSliver
trailingChildWithLayout = child; trailingChildWithLayout = child;
} }
assert(child != null); assert(child != null);
final SliverBlockParentData childParentData = child.parentData; final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = endScrollOffset; childParentData.scrollOffset = endScrollOffset;
assert(childParentData.index == index); assert(childParentData.index == index);
endScrollOffset = offsetOf(child) + paintExtentOf(child); endScrollOffset = offsetOf(child) + paintExtentOf(child);
...@@ -465,22 +183,12 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -465,22 +183,12 @@ abstract class RenderSliverBlock extends RenderSliver
collectGarbage(leadingGarbage, trailingGarbage); collectGarbage(leadingGarbage, trailingGarbage);
assert(firstChild != null); assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
index += 1;
assert(indexOf(child) == index);
child = childAfter(child);
}
return true;
});
double estimatedTotalExtent; double estimatedTotalExtent;
if (reachedEnd) { if (reachedEnd) {
estimatedTotalExtent = endScrollOffset; estimatedTotalExtent = endScrollOffset;
} else { } else {
estimatedTotalExtent = estimateScrollOffsetExtent( estimatedTotalExtent = childManager.estimateScrollOffsetExtent(
firstIndex: indexOf(firstChild), firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild), lastIndex: indexOf(lastChild),
leadingScrollOffset: offsetOf(firstChild), leadingScrollOffset: offsetOf(firstChild),
...@@ -488,7 +196,7 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -488,7 +196,7 @@ abstract class RenderSliverBlock extends RenderSliver
); );
assert(estimatedTotalExtent >= endScrollOffset - offsetOf(firstChild)); assert(estimatedTotalExtent >= endScrollOffset - offsetOf(firstChild));
} }
double paintedExtent = calculatePaintOffset( final double paintedExtent = calculatePaintOffset(
constraints, constraints,
from: offsetOf(firstChild), from: offsetOf(firstChild),
to: endScrollOffset, to: endScrollOffset,
...@@ -501,79 +209,6 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -501,79 +209,6 @@ abstract class RenderSliverBlock extends RenderSliver
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0, hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
); );
assert(_currentlyUpdatingChildIndex == null); assert(childManager.debugAssertChildListLocked());
}
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
RenderBox child = lastChild;
while (child != null) {
if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
return true;
child = childBefore(child);
}
return false;
}
@override
double childPosition(RenderBox child) {
return offsetOf(child) - constraints.scrollOffset;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
applyPaintTransformForBoxChild(child, transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset unitOffset, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
unitOffset = const Offset(0.0, -1.0);
originOffset = offset + new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
unitOffset = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
unitOffset = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
unitOffset = const Offset(-1.0, 0.0);
originOffset = offset + new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(unitOffset != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
Offset childOffset = originOffset + unitOffset * childPosition(child);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (firstChild != null) {
description.add('currently live children: ${indexOf(firstChild)} to ${indexOf(lastChild)}');
} else {
description.add('no children current live');
}
} }
} }
// Copyright 2017 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:math' as math;
import 'package:meta/meta.dart';
import 'box.dart';
import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
RenderSliverList({
@required RenderSliverBoxChildManager childManager,
double itemExtent,
}) : _itemExtent = itemExtent, super(childManager: childManager);
/// The main-axis extent of each item in the list.
double get itemExtent => _itemExtent;
double _itemExtent;
set itemExtent (double newValue) {
assert(newValue != null);
if (_itemExtent == newValue)
return;
_itemExtent = newValue;
markNeedsLayout();
}
double _indexToScrollOffset(int index) => _itemExtent * index;
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
final double scrollOffset = constraints.scrollOffset;
assert(scrollOffset >= 0.0);
final double remainingPaintExtent = constraints.remainingPaintExtent;
assert(remainingPaintExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingPaintExtent;
BoxConstraints childConstraints = constraints.asBoxConstraints(
minExtent: itemExtent,
maxExtent: itemExtent,
);
final int firstIndex = math.max(0, scrollOffset ~/ _itemExtent);
final int targetLastIndex = math.max(0, (targetEndScrollOffset / itemExtent).ceil());
if (firstChild != null) {
final int oldFirstIndex = indexOf(firstChild);
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = (oldLastIndex - targetLastIndex).clamp(0, childCount);
if (leadingGarbage + trailingGarbage > 0)
collectGarbage(leadingGarbage, trailingGarbage);
}
if (firstChild == null) {
if (!addInitialChild(index: firstIndex, scrollOffset: _indexToScrollOffset(firstIndex))) {
// There are no children.
geometry = SliverGeometry.zero;
return;
}
}
RenderBox trailingChildWithLayout;
for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) {
final RenderBox child = insertAndLayoutLeadingChild(childConstraints);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = _indexToScrollOffset(index);
assert(childParentData.index == index);
trailingChildWithLayout ??= child;
}
assert(offsetOf(firstChild) <= scrollOffset);
if (trailingChildWithLayout == null) {
firstChild.layout(childConstraints);
trailingChildWithLayout = firstChild;
}
while (indexOf(trailingChildWithLayout) < targetLastIndex) {
RenderBox child = childAfter(trailingChildWithLayout);
if (child == null) {
child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
if (child == null) {
// We have run out of children.
break;
}
} else {
child.layout(childConstraints);
}
trailingChildWithLayout = child;
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = _indexToScrollOffset(childParentData.index);
}
final int lastIndex = indexOf(lastChild);
final double leadingScrollOffset = _indexToScrollOffset(firstIndex);
final double trailingScrollOffset = _indexToScrollOffset(lastIndex + 1);
assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(indexOf(firstChild) == firstIndex);
assert(lastIndex <= targetLastIndex);
final double estimatedTotalExtent = childManager.estimateScrollOffsetExtent(
firstIndex: firstIndex,
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset,
trailingScrollOffset: trailingScrollOffset,
);
final double paintedExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,
);
assert(childManager.debugAssertChildListLocked());
}
}
// Copyright 2016 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'binding.dart';
import 'object.dart';
import 'sliver.dart';
/// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children.
///
/// [RenderSliverMultiBoxAdaptor] objects reify their children lazily to avoid
/// spending resources on children that are not visible in the viewport. This
/// delegate lets these objects create and remove children as well as estimate
/// the total scroll offset extent occupied by the full child list.
abstract class RenderSliverBoxChildManager {
/// Called during layout when a new child is needed. The child should be
/// inserted into the child list in the appropriate position, after the
/// `after` child (at the start of the list if `after` is null). Its index and
/// scroll offsets will automatically be set appropriately.
///
/// The `index` argument gives the index of the child to show. It is possible
/// for negative indices to be requested. For example: if the user scrolls
/// from child 0 to child 10, and then those children get much smaller, and
/// then the user scrolls back up again, this method will eventually be asked
/// to produce a child for index -1.
///
/// If no child corresponds to `index`, then do nothing.
///
/// Which child is indicated by index zero depends on the [GrowthDirection]
/// specified in the [RenderSliverMultiBoxAdaptor.constraints]. For example
/// if the children are the alphabet, then if
/// [SliverConstraints.growthDirection] is [GrowthDirection.forward] then
/// index zero is A, and index 25 is Z. On the other hand if
/// [SliverConstraints.growthDirection] is [GrowthDirection.reverse]
/// then index zero is Z, and index 25 is A.
///
/// During a call to [createChild] it is valid to remove other children from
/// the [RenderSliverMultiBoxAdaptor] object if they were not created during
/// this frame and have not yet been updated during this frame. It is not
/// valid to add any other children to this render object.
void createChild(int index, { @required RenderBox after });
/// Remove the given child from the child list.
///
/// Called by [RenderSliverMultiBoxAdaptor.collectGarbage], which itself is
/// called from [RenderSliverMultiBoxAdaptor.performLayout].
///
/// The index of the given child can be obtained using the
/// [RenderSliverMultiBoxAdaptor.indexOf] method, which reads it from the
/// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData].
void removeChild(RenderBox child);
/// Called to estimate the total scrollable extents of this object.
///
/// Must return the total distance from the start of the child with the
/// earliest possible index to the end of the child with the last possible
/// index.
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
});
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild].
///
/// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index]
/// field of the child's [parentData] accurately reflects the child's index in
/// the child list after this function returns.
void didAdoptChild(RenderBox child);
/// In debug mode, asserts that this manager is not expecting any
/// modifications to the [RenderSliverMultiBoxAdaptor]'s child list.
///
/// This function always returns true.
///
/// The manager is not required to track whether it is expecting modifications
/// to the [RenderSliverMultiBoxAdaptor]'s child list and can simply return
/// true without making any assertions.
bool debugAssertChildListLocked() => true;
}
class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderBox> {
int index;
@override
String toString() => 'index=$index; ${super.toString()}';
}
// /// The contract for adding and removing children from this render object is
// /// more strict than for normal render objects:
// ///
// /// - Children can be removed except during a layout pass if they have already
// /// been laid out during that layout pass.
// /// - Children cannot be added except during a call to [childManager], and
// /// then only if there is no child correspending to that index (or the child
// /// child corresponding to that index was first removed).
abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
with ContainerRenderObjectMixin<RenderBox, SliverMultiBoxAdaptorParentData>,
RenderSliverHelpers {
RenderSliverMultiBoxAdaptor({
@required RenderSliverBoxChildManager childManager
}) : _childManager = childManager {
assert(childManager != null);
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverMultiBoxAdaptorParentData)
child.parentData = new SliverMultiBoxAdaptorParentData();
}
@protected
RenderSliverBoxChildManager get childManager => _childManager;
final RenderSliverBoxChildManager _childManager;
@override
void adoptChild(RenderObject child) {
super.adoptChild(child);
childManager.didAdoptChild(child);
}
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
@override
void insert(RenderBox child, { RenderBox after }) {
super.insert(child, after: after);
assert(firstChild != null);
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
assert(indexOf(child) > index);
index = indexOf(child);
child = childAfter(child);
}
return true;
});
}
/// Called during layout to create and add the child with the given index and
/// scroll offset.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child.
///
/// Returns false if createChild did not add any child, otherwise returns
/// true.
///
/// Does not layout the new child.
///
/// When this is called, there are no children, so no children can be removed
/// during the call to createChild. No child should be added during that call
/// either, except for the one that is created and returned by createChild.
@protected
bool addInitialChild({ int index: 0, double scrollOffset: 0.0 }) {
assert(_debugAssertChildListLocked());
assert(firstChild == null);
bool result;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
_childManager.createChild(index, after: null);
if (firstChild != null) {
assert(firstChild == lastChild);
assert(indexOf(firstChild) == index);
final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
firstChildParentData.scrollOffset = scrollOffset;
result = true;
} else {
result = false;
}
});
return result;
}
/// Called during layout to create, add, and layout the child before
/// [firstChild].
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child.
///
/// Returns the new child or null if no child is created.
///
/// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except
/// for the one that is created and returned by createChild.
@protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {
bool parentUsesSize: false,
}) {
assert(_debugAssertChildListLocked());
final int index = indexOf(firstChild) - 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
_childManager.createChild(index, after: null);
});
if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: parentUsesSize);
return firstChild;
}
return null;
}
/// Called during layout to create, add, and layout the child after
/// the given child.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child.
///
/// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset.
///
/// Children after the `after` child may be removed in the process. Only the
/// new child may be added.
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {
@required RenderBox after,
bool parentUsesSize: false,
}) {
assert(_debugAssertChildListLocked());
assert(after != null);
final int index = indexOf(after) + 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
_childManager.createChild(index, after: after);
});
final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) {
assert(indexOf(child) == index);
child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child;
}
return null;
}
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
_childManager.removeChild(firstChild);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
_childManager.removeChild(lastChild);
trailingGarbage -= 1;
}
});
}
/// Returns the index of the given child, as given by the
/// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData].
int indexOf(RenderBox child) {
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the scroll offset of the given child, as given by the
/// [SliverMultiBoxAdaptorParentData.scrollOffset] field of the child's [parentData].
double offsetOf(RenderBox child) {
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.scrollOffset != null);
return childParentData.scrollOffset;
}
/// Returns the dimension of the given child in the main axis, as given by the
/// child's [RenderBox.size] property. This is only valid after layout.
@protected
double paintExtentOf(RenderBox child) {
assert(child != null);
assert(child.hasSize);
switch (constraints.axis) {
case Axis.horizontal:
return child.size.width;
case Axis.vertical:
return child.size.height;
}
return null;
}
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
RenderBox child = lastChild;
while (child != null) {
if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
return true;
child = childBefore(child);
}
return false;
}
@override
double childPosition(RenderBox child) {
return offsetOf(child) - constraints.scrollOffset;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
applyPaintTransformForBoxChild(child, transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset unitOffset, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
unitOffset = const Offset(0.0, -1.0);
originOffset = offset + new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
unitOffset = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
unitOffset = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
unitOffset = const Offset(-1.0, 0.0);
originOffset = offset + new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(unitOffset != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
Offset childOffset = originOffset + unitOffset * childPosition(child);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (firstChild != null) {
description.add('currently live children: ${indexOf(firstChild)} to ${indexOf(lastChild)}');
} else {
description.add('no children current live');
}
}
bool debugAssertChildListIsNonEmptyAndContiguous() {
assert(() {
assert(firstChild != null);
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
index += 1;
assert(indexOf(child) == index);
child = childAfter(child);
}
return true;
});
return true;
}
}
// Copyright 2016 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 'package:flutter/rendering.dart';
import 'framework.dart';
import 'basic.dart';
import 'scrollable.dart';
import 'sliver.dart';
class ScrollView extends StatelessWidget {
ScrollView({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.anchor: 0.0,
this.initialScrollOffset: 0.0,
this.itemExtent,
this.center,
this.children,
}) : super(key: key);
final EdgeInsets padding;
final Axis scrollDirection;
final double anchor;
final double initialScrollOffset;
final double itemExtent;
final Key center;
final List<Widget> children;
AxisDirection _getDirection(BuildContext context) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return AxisDirection.right;
case Axis.vertical:
return AxisDirection.down;
}
return null;
}
@override
Widget build(BuildContext context) {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
Widget sliver;
if (itemExtent == null) {
sliver = new SliverBlock(delegate: delegate);
} else {
sliver = new SliverList(
delegate: delegate,
itemExtent: itemExtent,
);
}
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context),
anchor: anchor,
initialScrollOffset: initialScrollOffset,
center: center,
slivers: <Widget>[ sliver ],
);
}
}
...@@ -6,71 +6,18 @@ import 'dart:collection' show SplayTreeMap, HashMap; ...@@ -6,71 +6,18 @@ import 'dart:collection' show SplayTreeMap, HashMap;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'framework.dart'; import 'framework.dart';
import 'basic.dart'; import 'basic.dart';
import 'scrollable.dart';
class ScrollView extends StatelessWidget { abstract class SliverChildDelegate {
ScrollView({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.anchor: 0.0,
this.initialScrollOffset: 0.0,
this.center,
this.children,
}) : super(key: key);
final EdgeInsets padding;
final Axis scrollDirection;
final double anchor;
final double initialScrollOffset;
final Key center;
final List<Widget> children;
AxisDirection _getDirection(BuildContext context) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return AxisDirection.right;
case Axis.vertical:
return AxisDirection.down;
}
return null;
}
@override
Widget build(BuildContext context) {
Widget sliver = new SliverBlock(delegate: new SliverBlockChildListDelegate(children));
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context),
anchor: anchor,
initialScrollOffset: initialScrollOffset,
center: center,
slivers: <Widget>[ sliver ],
);
}
}
abstract class SliverBlockDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const SliverBlockDelegate(); const SliverChildDelegate();
Widget build(BuildContext context, int index); Widget build(BuildContext context, int index);
bool shouldRebuild(@checked SliverBlockDelegate oldDelegate); bool shouldRebuild(@checked SliverChildDelegate oldDelegate);
int get childCount; int get childCount;
...@@ -102,10 +49,10 @@ abstract class SliverBlockDelegate { ...@@ -102,10 +49,10 @@ abstract class SliverBlockDelegate {
// /// always visible (and thus there is nothing to be gained by building it on // /// always visible (and thus there is nothing to be gained by building it on
// /// demand). For example, the body of a dialog box might fit both of these // /// demand). For example, the body of a dialog box might fit both of these
// /// conditions. // /// conditions.
class SliverBlockChildListDelegate extends SliverBlockDelegate { class SliverChildListDelegate extends SliverChildDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const SliverBlockChildListDelegate(this.children); const SliverChildListDelegate(this.children);
final List<Widget> children; final List<Widget> children;
...@@ -118,7 +65,7 @@ class SliverBlockChildListDelegate extends SliverBlockDelegate { ...@@ -118,7 +65,7 @@ class SliverBlockChildListDelegate extends SliverBlockDelegate {
} }
@override @override
bool shouldRebuild(@checked SliverBlockChildListDelegate oldDelegate) { bool shouldRebuild(@checked SliverChildListDelegate oldDelegate) {
return children != oldDelegate.children; return children != oldDelegate.children;
} }
...@@ -126,21 +73,21 @@ class SliverBlockChildListDelegate extends SliverBlockDelegate { ...@@ -126,21 +73,21 @@ class SliverBlockChildListDelegate extends SliverBlockDelegate {
int get childCount => children.length; int get childCount => children.length;
} }
class SliverBlock extends RenderObjectWidget { abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
SliverBlock({ SliverMultiBoxAdaptorWidget({
Key key, Key key,
@required this.delegate, @required this.delegate,
}) : super(key: key) { }) : super(key: key) {
assert(delegate != null); assert(delegate != null);
} }
final SliverBlockDelegate delegate; final SliverChildDelegate delegate;
@override @override
_SliverBlockElement createElement() => new _SliverBlockElement(this); SliverMultiBoxAdaptorElement createElement() => new SliverMultiBoxAdaptorElement(this);
@override @override
_RenderSliverBlockForWidgets createRenderObject(BuildContext context) => new _RenderSliverBlockForWidgets(); RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
...@@ -149,33 +96,55 @@ class SliverBlock extends RenderObjectWidget { ...@@ -149,33 +96,55 @@ class SliverBlock extends RenderObjectWidget {
} }
} }
class _SliverBlockElement extends RenderObjectElement { class SliverBlock extends SliverMultiBoxAdaptorWidget {
_SliverBlockElement(SliverBlock widget) : super(widget); SliverBlock({
Key key,
@required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
@override @override
SliverBlock get widget => super.widget; RenderSliverBlock createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return new RenderSliverBlock(childManager: element);
}
}
@override class SliverList extends SliverMultiBoxAdaptorWidget {
_RenderSliverBlockForWidgets get renderObject => super.renderObject; SliverList({
Key key,
@required SliverChildDelegate delegate,
@required this.itemExtent,
}) : super(key: key, delegate: delegate);
final double itemExtent;
@override @override
void mount(Element parent, dynamic newSlot) { RenderSliverList createRenderObject(BuildContext context) {
super.mount(parent, newSlot); final SliverMultiBoxAdaptorElement element = context;
renderObject._element = this; return new RenderSliverList(childManager: element, itemExtent: itemExtent);
} }
@override @override
void unmount() { void updateRenderObject(BuildContext context, RenderSliverList renderObject) {
super.unmount(); renderObject.itemExtent = itemExtent;
renderObject._element = null;
} }
}
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget) : super(widget);
@override
SliverMultiBoxAdaptorWidget get widget => super.widget;
@override @override
void update(SliverBlock newWidget) { RenderSliverMultiBoxAdaptor get renderObject => super.renderObject;
final SliverBlock oldWidget = widget;
@override
void update(SliverMultiBoxAdaptorWidget newWidget) {
final SliverMultiBoxAdaptorWidget oldWidget = widget;
super.update(newWidget); super.update(newWidget);
final SliverBlockDelegate newDelegate = newWidget.delegate; final SliverChildDelegate newDelegate = newWidget.delegate;
final SliverBlockDelegate oldDelegate = oldWidget.delegate; final SliverChildDelegate oldDelegate = oldWidget.delegate;
if (newDelegate != oldDelegate && if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
performRebuild(); performRebuild();
...@@ -184,15 +153,13 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -184,15 +153,13 @@ class _SliverBlockElement extends RenderObjectElement {
Map<int, Element> _childElements = new SplayTreeMap<int, Element>(); Map<int, Element> _childElements = new SplayTreeMap<int, Element>();
Map<int, Widget> _childWidgets = new HashMap<int, Widget>(); Map<int, Widget> _childWidgets = new HashMap<int, Widget>();
RenderBox _currentBeforeChild; RenderBox _currentBeforeChild;
bool _debugOpenToChanges = false;
@override @override
void performRebuild() { void performRebuild() {
_childWidgets.clear(); _childWidgets.clear();
super.performRebuild(); super.performRebuild();
_currentBeforeChild = null; _currentBeforeChild = null;
assert(!_debugOpenToChanges); assert(_currentlyUpdatingChildIndex == null);
assert(() { _debugOpenToChanges = true; return true; });
try { try {
// The "toList()" below is to get a copy of the array so that we can // The "toList()" below is to get a copy of the array so that we can
// mutate _childElements within the loop. Basically we just update all the // mutate _childElements within the loop. Basically we just update all the
...@@ -202,10 +169,8 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -202,10 +169,8 @@ class _SliverBlockElement extends RenderObjectElement {
// delegate's results until the next time we need to rebuild the whole // delegate's results until the next time we need to rebuild the whole
// block widget.) // block widget.)
for (int index in _childElements.keys.toList()) { for (int index in _childElements.keys.toList()) {
Element newChild; _currentlyUpdatingChildIndex = index;
renderObject._rebuild(index, () { Element newChild = updateChild(_childElements[index], _build(index), index);
newChild = updateChild(_childElements[index], _build(index), index);
});
if (newChild != null) { if (newChild != null) {
_childElements[index] = newChild; _childElements[index] = newChild;
_currentBeforeChild = newChild.renderObject; _currentBeforeChild = newChild.renderObject;
...@@ -214,7 +179,7 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -214,7 +179,7 @@ class _SliverBlockElement extends RenderObjectElement {
} }
} }
} finally { } finally {
assert(() { _debugOpenToChanges = false; return true; }); _currentlyUpdatingChildIndex = null;
} }
} }
...@@ -227,17 +192,19 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -227,17 +192,19 @@ class _SliverBlockElement extends RenderObjectElement {
}); });
} }
void _createChild(int index, bool insertFirst) { @override
assert(!_debugOpenToChanges); void createChild(int index, { @required RenderBox after }) {
assert(_currentlyUpdatingChildIndex == null);
owner.buildScope(this, () { owner.buildScope(this, () {
final bool insertFirst = after == null;
assert(insertFirst || _childElements[index-1] != null); assert(insertFirst || _childElements[index-1] != null);
assert(() { _debugOpenToChanges = true; return true; });
_currentBeforeChild = insertFirst ? null : _childElements[index-1].renderObject; _currentBeforeChild = insertFirst ? null : _childElements[index-1].renderObject;
Element newChild; Element newChild;
try { try {
_currentlyUpdatingChildIndex = index;
newChild = updateChild(_childElements[index], _build(index), index); newChild = updateChild(_childElements[index], _build(index), index);
} finally { } finally {
assert(() { _debugOpenToChanges = false; return true; }); _currentlyUpdatingChildIndex = null;
} }
if (newChild != null) { if (newChild != null) {
_childElements[index] = newChild; _childElements[index] = newChild;
...@@ -255,29 +222,63 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -255,29 +222,63 @@ class _SliverBlockElement extends RenderObjectElement {
_childElements.remove(child.slot); _childElements.remove(child.slot);
} }
void _removeChild(int index) { @override
assert(!_debugOpenToChanges); void removeChild(RenderBox child) {
final int index = renderObject.indexOf(child);
assert(_currentlyUpdatingChildIndex == null);
assert(index >= 0); assert(index >= 0);
owner.buildScope(this, () { owner.buildScope(this, () {
assert(_childElements.containsKey(index)); assert(_childElements.containsKey(index));
assert(() { _debugOpenToChanges = true; return true; });
try { try {
Element result = updateChild(_childElements[index], null, index); _currentlyUpdatingChildIndex = index;
final Element result = updateChild(_childElements[index], null, index);
assert(result == null); assert(result == null);
} finally { } finally {
assert(() { _debugOpenToChanges = false; return true; }); _currentlyUpdatingChildIndex = null;
} }
_childElements.remove(index); _childElements.remove(index);
assert(!_childElements.containsKey(index)); assert(!_childElements.containsKey(index));
}); });
} }
@override
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
assert(lastIndex >= firstIndex);
return widget.delegate.estimateScrollOffsetExtent(
firstIndex,
lastIndex,
leadingScrollOffset,
trailingScrollOffset
);
}
int _currentlyUpdatingChildIndex;
@override
bool debugAssertChildListLocked() {
assert(_currentlyUpdatingChildIndex == null);
return true;
}
@override
void didAdoptChild(RenderBox child) {
assert(_currentlyUpdatingChildIndex != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.index = _currentlyUpdatingChildIndex;
}
@override @override
void insertChildRenderObject(@checked RenderObject child, int slot) { void insertChildRenderObject(@checked RenderObject child, int slot) {
assert(_debugOpenToChanges); assert(slot != null);
assert(_currentlyUpdatingChildIndex == slot);
renderObject.insert(child, after: _currentBeforeChild); renderObject.insert(child, after: _currentBeforeChild);
assert(() { assert(() {
SliverBlockParentData childParentData = child.parentData; SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(slot == childParentData.index); assert(slot == childParentData.index);
return true; return true;
}); });
...@@ -292,7 +293,7 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -292,7 +293,7 @@ class _SliverBlockElement extends RenderObjectElement {
@override @override
void removeChildRenderObject(@checked RenderObject child) { void removeChildRenderObject(@checked RenderObject child) {
assert(_debugOpenToChanges); assert(_currentlyUpdatingChildIndex != null);
renderObject.remove(child); renderObject.remove(child);
} }
...@@ -304,35 +305,3 @@ class _SliverBlockElement extends RenderObjectElement { ...@@ -304,35 +305,3 @@ class _SliverBlockElement extends RenderObjectElement {
_childElements.values.toList().forEach(visitor); _childElements.values.toList().forEach(visitor);
} }
} }
class _RenderSliverBlockForWidgets extends RenderSliverBlock {
_SliverBlockElement _element;
@override
void createChild(int index, { @required RenderBox after }) {
assert(_element != null);
_element._createChild(index, after == null);
}
@override
void removeChild(RenderBox child) {
assert(_element != null);
_element._removeChild(indexOf(child));
}
void _rebuild(int index, VoidCallback callback) {
allowAdditionsFor(index, callback);
}
@override
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
assert(lastIndex >= firstIndex);
assert(_element != null);
return _element.widget.delegate.estimateScrollOffsetExtent(firstIndex, lastIndex, leadingScrollOffset, trailingScrollOffset);
}
}
...@@ -14,7 +14,6 @@ export 'src/widgets/app_bar.dart'; ...@@ -14,7 +14,6 @@ export 'src/widgets/app_bar.dart';
export 'src/widgets/banner.dart'; export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
export 'src/widgets/block.dart';
export 'src/widgets/clamp_overscrolls.dart'; export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart'; export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
...@@ -51,12 +50,14 @@ export 'src/widgets/scroll_behavior.dart'; ...@@ -51,12 +50,14 @@ export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_notification.dart'; export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_simulation.dart'; export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart'; export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart'; export 'src/widgets/scrollable_list.dart';
export 'src/widgets/semantics_debugger.dart'; export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/single_child_scroll_view.dart'; export 'src/widgets/single_child_scroll_view.dart';
export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/sliver.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart'; export 'src/widgets/table.dart';
export 'src/widgets/text.dart'; export 'src/widgets/text.dart';
......
...@@ -8,19 +8,38 @@ import 'package:test/test.dart'; ...@@ -8,19 +8,38 @@ import 'package:test/test.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
class RenderSliverBlockTest extends RenderSliverBlock { class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager {
RenderSliverBlockTest({ TestRenderSliverBoxChildManager({
this.children, this.children,
}); });
RenderSliverBlock _renderObject;
List<RenderBox> children; List<RenderBox> children;
RenderSliverBlock createRenderObject() {
assert(_renderObject == null);
_renderObject = new RenderSliverBlock(childManager: this);
return _renderObject;
}
int _currentlyUpdatingChildIndex;
@override @override
void createChild(int index, { @required RenderBox after }) { void createChild(int index, { @required RenderBox after }) {
assert(index >= 0); assert(index >= 0);
if (index < 0 || index >= children.length) if (index < 0 || index >= children.length)
return null; return null;
insert(children[index], after: after); try {
_currentlyUpdatingChildIndex = index;
_renderObject.insert(children[index], after: after);
} finally {
_currentlyUpdatingChildIndex = null;
}
}
@override
void removeChild(RenderBox child) {
_renderObject.remove(child);
} }
@override @override
...@@ -33,25 +52,33 @@ class RenderSliverBlockTest extends RenderSliverBlock { ...@@ -33,25 +52,33 @@ class RenderSliverBlockTest extends RenderSliverBlock {
assert(lastIndex >= firstIndex); assert(lastIndex >= firstIndex);
return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1); return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1);
} }
@override
void didAdoptChild(RenderBox child) {
assert(_currentlyUpdatingChildIndex != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.index = _currentlyUpdatingChildIndex;
}
} }
void main() { void main() {
test('RenderSliverBlock basic test - down', () { test('RenderSliverBlock basic test - down', () {
RenderObject inner; RenderObject inner;
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
TestRenderSliverBoxChildManager childManager = new TestRenderSliverBoxChildManager(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
);
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
offset: new ViewportOffset.zero(), offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
inner = new RenderSliverBlockTest( inner = childManager.createRenderObject(),
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
], ],
); );
layout(root); layout(root);
...@@ -112,19 +139,20 @@ void main() { ...@@ -112,19 +139,20 @@ void main() {
test('RenderSliverBlock basic test - up', () { test('RenderSliverBlock basic test - up', () {
RenderObject inner; RenderObject inner;
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
TestRenderSliverBoxChildManager childManager = new TestRenderSliverBoxChildManager(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
);
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.up, axisDirection: AxisDirection.up,
offset: new ViewportOffset.zero(), offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
inner = new RenderSliverBlockTest( inner = childManager.createRenderObject(),
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
], ],
); );
layout(root); layout(root);
......
...@@ -121,7 +121,7 @@ void main() { ...@@ -121,7 +121,7 @@ void main() {
}); });
testWidgets('SliverBlockChildListDelegate.estimateScrollOffsetExtent hits end', (WidgetTester tester) async { testWidgets('SliverBlockChildListDelegate.estimateScrollOffsetExtent hits end', (WidgetTester tester) async {
SliverBlockChildListDelegate delegate = new SliverBlockChildListDelegate(<Widget>[ SliverChildListDelegate delegate = new SliverChildListDelegate(<Widget>[
new Container(), new Container(),
new Container(), new Container(),
new Container(), new Container(),
......
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
void main() {
testWidgets('ScrollView itemExtent control test', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollView(
itemExtent: 200.0,
children: new List<Widget>.generate(20, (int i) {
return new Container(
child: new Text('$i'),
);
}),
),
);
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).first);
expect(box.size.height, equals(200.0));
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsNothing);
await tester.scroll(find.byType(ScrollView), const Offset(0.0, -250.0));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing);
await tester.scroll(find.byType(ScrollView), const Offset(0.0, 200.0));
await tester.pump();
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsNothing);
});
testWidgets('ScrollView large scroll jump', (WidgetTester tester) async {
List<int> log = <int>[];
await tester.pumpWidget(
new ScrollView(
itemExtent: 200.0,
children: new List<Widget>.generate(20, (int i) {
return new Builder(
builder: (BuildContext context) {
log.add(i);
return new Container(
child: new Text('$i'),
);
}
);
}),
),
);
expect(log, equals(<int>[0, 1, 2, 3]));
log.clear();
Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position;
position.jumpTo(2025.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[10, 11, 12, 13, 14]));
log.clear();
position.jumpTo(975.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[4, 5, 6, 7, 8]));
log.clear();
});
}
...@@ -28,7 +28,7 @@ Future<Null> test(WidgetTester tester, double offset, List<int> keys) { ...@@ -28,7 +28,7 @@ Future<Null> test(WidgetTester tester, double offset, List<int> keys) {
offset: new ViewportOffset.fixed(offset), offset: new ViewportOffset.fixed(offset),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(keys.map((int key) { delegate: new SliverChildListDelegate(keys.map((int key) {
return new SizedBox(key: new GlobalObjectKey(key), height: 100.0, child: new GenerationText(key)); return new SizedBox(key: new GlobalObjectKey(key), height: 100.0, child: new GenerationText(key));
}).toList()), }).toList()),
), ),
......
...@@ -13,7 +13,7 @@ Future<Null> test(WidgetTester tester, double offset) { ...@@ -13,7 +13,7 @@ Future<Null> test(WidgetTester tester, double offset) {
offset: new ViewportOffset.fixed(offset), offset: new ViewportOffset.fixed(offset),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')), new SizedBox(height: 400.0, child: new Text('a')),
new SizedBox(height: 400.0, child: new Text('b')), new SizedBox(height: 400.0, child: new Text('b')),
new SizedBox(height: 400.0, child: new Text('c')), new SizedBox(height: 400.0, child: new Text('c')),
...@@ -78,7 +78,7 @@ void main() { ...@@ -78,7 +78,7 @@ void main() {
offset: offset, offset: offset,
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')), new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')), new SizedBox(height: 252.0, child: new Text('b')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')), new SizedBox(key: key1, height: 253.0, child: new Text('c')),
...@@ -95,7 +95,7 @@ void main() { ...@@ -95,7 +95,7 @@ void main() {
offset: offset, offset: offset,
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(key: key1, height: 253.0, child: new Text('c')), new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 251.0, child: new Text('a')), new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')), new SizedBox(height: 252.0, child: new Text('b')),
...@@ -112,7 +112,7 @@ void main() { ...@@ -112,7 +112,7 @@ void main() {
offset: offset, offset: offset,
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')), new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')), new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')), new SizedBox(height: 252.0, child: new Text('b')),
...@@ -129,7 +129,7 @@ void main() { ...@@ -129,7 +129,7 @@ void main() {
offset: offset, offset: offset,
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')), new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')), new SizedBox(height: 252.0, child: new Text('b')),
]), ]),
...@@ -144,7 +144,7 @@ void main() { ...@@ -144,7 +144,7 @@ void main() {
offset: offset, offset: offset,
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')), new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')), new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')), new SizedBox(height: 252.0, child: new Text('b')),
...@@ -210,7 +210,7 @@ void main() { ...@@ -210,7 +210,7 @@ void main() {
offset: new ViewportOffset.zero(), offset: new ViewportOffset.zero(),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')), new SizedBox(height: 400.0, child: new Text('a')),
]), ]),
), ),
...@@ -223,7 +223,7 @@ void main() { ...@@ -223,7 +223,7 @@ void main() {
offset: new ViewportOffset.fixed(100.0), offset: new ViewportOffset.fixed(100.0),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')), new SizedBox(height: 400.0, child: new Text('a')),
]), ]),
), ),
...@@ -236,7 +236,7 @@ void main() { ...@@ -236,7 +236,7 @@ void main() {
offset: new ViewportOffset.fixed(100.0), offset: new ViewportOffset.fixed(100.0),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 4000.0, child: new Text('a')), new SizedBox(height: 4000.0, child: new Text('a')),
]), ]),
), ),
...@@ -249,7 +249,7 @@ void main() { ...@@ -249,7 +249,7 @@ void main() {
offset: new ViewportOffset.zero(), offset: new ViewportOffset.zero(),
slivers: <Widget>[ slivers: <Widget>[
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 4000.0, child: new Text('a')), new SizedBox(height: 4000.0, child: new Text('a')),
]), ]),
), ),
......
...@@ -119,7 +119,7 @@ void main() { ...@@ -119,7 +119,7 @@ void main() {
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true), new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)), new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverBlock( new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[ delegate: new SliverChildListDelegate(<Widget>[
new Container(height: 50.0), new Container(height: 50.0),
new Container(height: 50.0), new Container(height: 50.0),
new Container(height: 50.0), new Container(height: 50.0),
......
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