Commit 9119969a authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add SliverGrid and ScrollGrid (#7745)

This patch adds grid supports to slivers and introduces a ScrollGrid
convenience class for making the common types of scrollable grids.

This patch also deploys ScrollGrid in an example in the Flutter Gallery.
parent 8ef17e0a
......@@ -131,9 +131,9 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv
..translate(_offset.dx, _offset.dy)
..scale(_scale),
child: new ClipRect(
child: new Image.asset(config.photo.assetName, fit: ImageFit.cover)
)
)
child: new Image.asset(config.photo.assetName, fit: ImageFit.cover),
),
),
);
}
);
......@@ -200,11 +200,11 @@ class GridDemoPhotoItem extends StatelessWidget {
backgroundColor: Colors.black45,
leading: new Icon(
icon,
color: Colors.white
)
)
color: Colors.white,
),
),
),
child: image
child: image,
);
case GridDemoTileStyle.twoLine:
......@@ -217,11 +217,11 @@ class GridDemoPhotoItem extends StatelessWidget {
subtitle: new _GridTitleText(photo.caption),
trailing: new Icon(
icon,
color: Colors.white
)
)
color: Colors.white,
),
),
),
child: image
child: image,
);
}
assert(tileStyle != null);
......@@ -245,62 +245,62 @@ class GridListDemoState extends State<GridListDemo> {
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_0.jpg',
title: 'Philippines',
caption: 'Batad rice terraces'
caption: 'Batad rice terraces',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_1.jpg',
title: 'Italy',
caption: 'Ceresole Reale'
caption: 'Ceresole Reale',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_2.jpg',
title: 'Somewhere',
caption: 'Beautiful mountains'
caption: 'Beautiful mountains',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_3.jpg',
title: 'A place',
caption: 'Beautiful hills'
caption: 'Beautiful hills',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_4.jpg',
title: 'New Zealand',
caption: 'View from the van'
caption: 'View from the van',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_5.jpg',
title: 'Autumn',
caption: 'The golden season'
caption: 'The golden season',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_6.jpg',
title: 'Germany',
caption: 'Englischer Garten'
caption: 'Englischer Garten',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_7.jpg',
title: 'A country',
caption: 'Grass fields'
caption: 'Grass fields',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_8.jpg',
title: 'Mountain country',
caption: 'River forest'
caption: 'River forest',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_9.jpg',
title: 'Alpine place',
caption: 'Green hills'
caption: 'Green hills',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_10.jpg',
title: 'Desert land',
caption: 'Blue skies'
caption: 'Blue skies',
),
new Photo(
assetName: 'packages/flutter_gallery_assets/landscape_11.jpg',
title: 'Narnia',
caption: 'Rocks and rivers'
caption: 'Rocks and rivers',
),
];
......@@ -325,31 +325,29 @@ class GridListDemoState extends State<GridListDemo> {
itemBuilder: (BuildContext context) => <PopupMenuItem<GridDemoTileStyle>>[
new PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.imageOnly,
child: new Text('Image only')
child: new Text('Image only'),
),
new PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.oneLine,
child: new Text('One line')
child: new Text('One line'),
),
new PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.twoLine,
child: new Text('Two line')
)
]
)
]
child: new Text('Two line'),
),
],
),
],
),
body: new Column(
children: <Widget>[
new Expanded(
child: new ScrollableGrid(
delegate: new FixedColumnCountGridDelegate(
columnCount: (orientation == Orientation.portrait) ? 2 : 3,
rowSpacing: 4.0,
columnSpacing: 4.0,
padding: const EdgeInsets.all(4.0),
tileAspectRatio: (orientation == Orientation.portrait) ? 1.0 : 1.3
),
child: new ScrollGrid.count(
crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
padding: const EdgeInsets.all(4.0),
childAspectRatio: (orientation == Orientation.portrait) ? 1.0 : 1.3,
children: photos.map((Photo photo) {
return new GridDemoPhotoItem(
photo: photo,
......@@ -360,7 +358,7 @@ class GridListDemoState extends State<GridListDemo> {
});
}
);
})
}).toList(),
)
)
]
......
......@@ -47,6 +47,7 @@ export 'src/rendering/shifted_box.dart';
export 'src/rendering/sliver.dart';
export 'src/rendering/sliver_app_bar.dart';
export 'src/rendering/sliver_block.dart';
export 'src/rendering/sliver_grid.dart';
export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart';
......
......@@ -282,7 +282,9 @@ class SliverConstraints extends Constraints {
BoxConstraints asBoxConstraints({
double minExtent: 0.0,
double maxExtent: double.INFINITY,
double crossAxisExtent,
}) {
crossAxisExtent ??= this.crossAxisExtent;
switch (axis) {
case Axis.horizontal:
return new BoxConstraints(
......@@ -815,13 +817,16 @@ abstract class RenderSliver extends RenderObject {
/// the [RenderSliverHelpers] mixin and do not call this method yourself, you
/// do not need to implement this method.
@protected
double childPosition(@checked RenderObject child) {
double childMainAxisPosition(@checked RenderObject child) {
assert(() {
throw new FlutterError('$runtimeType does not implement childPosition.');
});
return 0.0;
}
@protected
double childCrossAxisPosition(@checked RenderObject child) => 0.0;
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(() {
......@@ -998,24 +1003,25 @@ abstract class RenderSliverHelpers implements RenderSliver {
/// This function takes care of converting the position from the sliver
/// coordinate system to the cartesian coordinate system used by [RenderBox].
///
/// This function relies on [childPosition] to determine the position of
/// This function relies on [childMainAxisPosition] to determine the position of
/// child in question.
///
/// Calling this for a child that is not visible is not valid.
@protected
bool hitTestBoxChild(HitTestResult result, RenderBox child, { @required double mainAxisPosition, @required double crossAxisPosition }) {
final bool rightWayUp = _getRightWayUp(constraints);
double absolutePosition = mainAxisPosition - childPosition(child);
double absolutePosition = mainAxisPosition - childMainAxisPosition(child);
final double absoluteCrossAxisPosition = crossAxisPosition - childCrossAxisPosition(child);
assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
if (!rightWayUp)
absolutePosition = child.size.width - absolutePosition;
return child.hitTest(result, position: new Point(absolutePosition, crossAxisPosition));
return child.hitTest(result, position: new Point(absolutePosition, absoluteCrossAxisPosition));
case Axis.vertical:
if (!rightWayUp)
absolutePosition = child.size.height - absolutePosition;
return child.hitTest(result, position: new Point(crossAxisPosition, absolutePosition));
return child.hitTest(result, position: new Point(absoluteCrossAxisPosition, absolutePosition));
}
return false;
}
......@@ -1023,25 +1029,27 @@ abstract class RenderSliverHelpers implements RenderSliver {
/// Utility function for [applyPaintTransform] for use when the children are
/// [RenderBox] widgets.
///
/// This function turns the value returned by [childPosition] for the child in
/// question into a translation that it then applies to the given matrix.
/// This function turns the value returned by [childMainAxisPosition] and
/// [childCrossAxisPosition]for the child in question into a translation that
/// it then applies to the given matrix.
///
/// Calling this for a child that is not visible is not valid.
@protected
void applyPaintTransformForBoxChild(RenderBox child, Matrix4 transform) {
final bool rightWayUp = _getRightWayUp(constraints);
double delta = childPosition(child);
double delta = childMainAxisPosition(child);
final double crossAxisDelta = childCrossAxisPosition(child);
assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
if (!rightWayUp)
delta = geometry.paintExtent - child.size.width - delta;
transform.translate(delta, 0.0);
transform.translate(delta, crossAxisDelta);
break;
case Axis.vertical:
if (!rightWayUp)
delta = geometry.paintExtent - child.size.height - delta;
transform.translate(0.0, delta);
transform.translate(crossAxisDelta, delta);
break;
}
}
......@@ -1838,7 +1846,7 @@ class RenderSliverToBoxAdapter extends RenderSliver with RenderObjectWithChildMi
}
@override
double childPosition(RenderBox child) {
double childMainAxisPosition(RenderBox child) {
return -constraints.scrollOffset;
}
......
......@@ -132,7 +132,7 @@ abstract class RenderSliverAppBar extends RenderSliver with RenderObjectWithChil
///
/// If there is no child, this should return 0.0.
@override
double childPosition(@checked RenderObject child) => super.childPosition(child);
double childMainAxisPosition(@checked RenderObject child) => super.childMainAxisPosition(child);
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
......@@ -155,16 +155,16 @@ abstract class RenderSliverAppBar extends RenderSliver with RenderObjectWithChil
assert(constraints.axisDirection != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
offset += new Offset(0.0, geometry.paintExtent - childPosition(child) - childExtent);
offset += new Offset(0.0, geometry.paintExtent - childMainAxisPosition(child) - childExtent);
break;
case AxisDirection.down:
offset += new Offset(0.0, childPosition(child));
offset += new Offset(0.0, childMainAxisPosition(child));
break;
case AxisDirection.left:
offset += new Offset(geometry.paintExtent - childPosition(child) - childExtent, 0.0);
offset += new Offset(geometry.paintExtent - childMainAxisPosition(child) - childExtent, 0.0);
break;
case AxisDirection.right:
offset += new Offset(childPosition(child), 0.0);
offset += new Offset(childMainAxisPosition(child), 0.0);
break;
}
context.paintChild(child, offset);
......@@ -180,7 +180,7 @@ abstract class RenderSliverAppBar extends RenderSliver with RenderObjectWithChil
description.add('maxExtent: EXCEPTION (${e.runtimeType}) WHILE COMPUTING MAX EXTENT');
}
try {
description.add('child position: ${childPosition(child).toStringAsFixed(1)}');
description.add('child position: ${childMainAxisPosition(child).toStringAsFixed(1)}');
} catch (e) {
description.add('child position: EXCEPTION (${e.runtimeType}) WHILE COMPUTING CHILD POSITION');
}
......@@ -216,7 +216,7 @@ abstract class RenderSliverScrollingAppBar extends RenderSliverAppBar {
}
@override
double childPosition(RenderBox child) {
double childMainAxisPosition(RenderBox child) {
assert(child == this.child);
return _childPosition;
}
......@@ -246,7 +246,7 @@ abstract class RenderSliverPinnedAppBar extends RenderSliverAppBar {
}
@override
double childPosition(RenderBox child) {
double childMainAxisPosition(RenderBox child) {
assert(child == this.child);
return constraints?.overlap;
}
......@@ -298,7 +298,7 @@ abstract class RenderSliverFloatingAppBar extends RenderSliverAppBar {
}
@override
double childPosition(RenderBox child) {
double childMainAxisPosition(RenderBox child) {
assert(child == this.child);
return _childPosition;
}
......
......@@ -184,17 +184,18 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
collectGarbage(leadingGarbage, trailingGarbage);
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedTotalExtent;
double estimatedMaxScrollOffset;
if (reachedEnd) {
estimatedTotalExtent = endScrollOffset;
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedTotalExtent = childManager.estimateScrollOffsetExtent(
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: offsetOf(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedTotalExtent >= endScrollOffset - offsetOf(firstChild));
assert(estimatedMaxScrollOffset >= endScrollOffset - offsetOf(firstChild));
}
final double paintedExtent = calculatePaintOffset(
constraints,
......@@ -202,9 +203,9 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
to: endScrollOffset,
);
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
);
......
This diff is collapsed.
......@@ -14,7 +14,9 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
RenderSliverList({
@required RenderSliverBoxChildManager childManager,
double itemExtent,
}) : _itemExtent = itemExtent, super(childManager: childManager);
}) : _itemExtent = itemExtent, super(childManager: childManager) {
assert(itemExtent != null);
}
/// The main-axis extent of each item in the list.
double get itemExtent => _itemExtent;
......@@ -105,7 +107,8 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
assert(indexOf(firstChild) == firstIndex);
assert(lastIndex <= targetLastIndex);
final double estimatedTotalExtent = childManager.estimateScrollOffsetExtent(
final double estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: firstIndex,
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset,
......@@ -119,9 +122,9 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
);
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,
);
......
......@@ -61,7 +61,7 @@ abstract class RenderSliverBoxChildManager {
/// 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({
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
......@@ -304,7 +304,7 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
}
@override
double childPosition(RenderBox child) {
double childMainAxisPosition(RenderBox child) {
return offsetOf(child) - constraints.scrollOffset;
}
......@@ -319,37 +319,46 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
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;
Offset mainAxisUnit, crossAxisUnit, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
unitOffset = const Offset(0.0, -1.0);
mainAxisUnit = const Offset(0.0, -1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset + new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
unitOffset = const Offset(1.0, 0.0);
mainAxisUnit = const Offset(1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
unitOffset = const Offset(0.0, 1.0);
mainAxisUnit = const Offset(0.0, 1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
unitOffset = const Offset(-1.0, 0.0);
mainAxisUnit = const Offset(-1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset + new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(unitOffset != null);
assert(mainAxisUnit != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
Offset childOffset = originOffset + unitOffset * childPosition(child);
final double mainAxisDelta = childMainAxisPosition(child);
final double crossAxisDelta = childCrossAxisPosition(child);
Offset childOffset = new Offset(
originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta,
originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta,
);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
childOffset += mainAxisUnit * paintExtentOf(child);
context.paintChild(child, childOffset);
child = childAfter(child);
}
......
......@@ -262,17 +262,24 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
if (child.geometry.hitTestExtent > 0.0)
return child.hitTest(result, mainAxisPosition: mainAxisPosition - childPosition(child), crossAxisPosition: crossAxisPosition - startPadding);
return child.hitTest(result, mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), crossAxisPosition: crossAxisPosition - childCrossAxisPosition(child));
return false;
}
@override
double childPosition(RenderSliver child) {
double childMainAxisPosition(RenderSliver child) {
assert(child != null);
assert(child == this.child);
return calculatePaintOffset(constraints, from: 0.0, to: beforePadding);
}
@override
double childCrossAxisPosition(RenderSliver child) {
assert(child != null);
assert(child == this.child);
return startPadding;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
......
......@@ -3,21 +3,31 @@
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'framework.dart';
import 'basic.dart';
import 'scrollable.dart';
import 'sliver.dart';
AxisDirection _getDirection(BuildContext context, Axis scrollDirection) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return AxisDirection.right;
case Axis.vertical:
return AxisDirection.down;
}
return null;
}
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);
......@@ -25,50 +35,109 @@ class ScrollView extends StatelessWidget {
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;
Widget _buildChildLayout() {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
if (itemExtent != null) {
return new SliverList(
delegate: delegate,
itemExtent: itemExtent,
);
}
return null;
return new SliverBlock(delegate: delegate);
}
@override
Widget build(BuildContext context) {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
Widget sliver = _buildChildLayout();
Widget sliver;
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
if (itemExtent == null) {
sliver = new SliverBlock(delegate: delegate);
} else {
sliver = new SliverList(
delegate: delegate,
itemExtent: itemExtent,
);
}
return new ScrollableViewport2(
axisDirection: _getDirection(context, scrollDirection),
initialScrollOffset: initialScrollOffset,
slivers: <Widget>[ sliver ],
);
}
}
class ScrollGrid extends StatelessWidget {
ScrollGrid({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
this.gridDelegate,
this.children,
}) : super(key: key);
ScrollGrid.count({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
@required int crossAxisCount,
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
this.children,
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
), super(key: key);
ScrollGrid.extent({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
@required double maxCrossAxisExtent,
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
this.children,
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
), super(key: key);
final EdgeInsets padding;
final Axis scrollDirection;
final double initialScrollOffset;
final SliverGridDelegate gridDelegate;
final List<Widget> children;
@override
Widget build(BuildContext context) {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
Widget sliver = new SliverGrid(
delegate: delegate,
gridDelegate: gridDelegate,
);
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context),
anchor: anchor,
axisDirection: _getDirection(context, scrollDirection),
initialScrollOffset: initialScrollOffset,
center: center,
slivers: <Widget>[ sliver ],
);
}
......
......@@ -10,6 +10,11 @@ import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'basic.dart';
export 'package:flutter/rendering.dart' show
SliverGridDelegate,
SliverGridDelegateWithFixedCrossAxisCount,
SliverGridDelegateWithMaxCrossAxisExtent;
abstract class SliverChildDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -17,24 +22,23 @@ abstract class SliverChildDelegate {
Widget build(BuildContext context, int index);
bool shouldRebuild(@checked SliverChildDelegate oldDelegate);
int get childCount;
/// Returns an estimate of the number of children this delegate will build.
///
/// Used to estimate the maximum scroll offset if [estimateMaxScrollOffset]
/// returns null.
///
/// Return null if there are an unbounded number of children or if it would
/// be too difficult to estimate the number of children.
int get estimatedChildCount => null;
double estimateScrollOffsetExtent(
double estimateMaxScrollOffset(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) {
final int childCount = this.childCount;
if (lastIndex == childCount - 1)
return trailingScrollOffset;
final int reifiedCount = lastIndex - firstIndex + 1;
final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
final int remainingCount = childCount - lastIndex - 1;
return trailingScrollOffset + averageExtent * remainingCount;
}
) => null;
bool shouldRebuild(@checked SliverChildDelegate oldDelegate);
}
// ///
......@@ -64,13 +68,13 @@ class SliverChildListDelegate extends SliverChildDelegate {
return children[index];
}
@override
int get estimatedChildCount => children.length;
@override
bool shouldRebuild(@checked SliverChildListDelegate oldDelegate) {
return children != oldDelegate.children;
}
@override
int get childCount => children.length;
}
abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
......@@ -89,6 +93,22 @@ abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
@override
RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
double estimateMaxScrollOffset(
SliverConstraints constraints,
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) {
assert(lastIndex >= firstIndex);
return delegate.estimateMaxScrollOffset(
firstIndex,
lastIndex,
leadingScrollOffset,
trailingScrollOffset,
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
......@@ -130,6 +150,44 @@ class SliverList extends SliverMultiBoxAdaptorWidget {
}
}
class SliverGrid extends SliverMultiBoxAdaptorWidget {
SliverGrid({
Key key,
@required SliverChildDelegate delegate,
@required this.gridDelegate,
}) : super(key: key, delegate: delegate);
final SliverGridDelegate gridDelegate;
@override
RenderSliverGrid createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return new RenderSliverGrid(childManager: element, gridDelegate: gridDelegate);
}
@override
void updateRenderObject(BuildContext context, RenderSliverGrid renderObject) {
renderObject.gridDelegate = gridDelegate;
}
@override
double estimateMaxScrollOffset(
SliverConstraints constraints,
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) {
return super.estimateMaxScrollOffset(
constraints,
firstIndex,
lastIndex,
leadingScrollOffset,
trailingScrollOffset,
) ?? gridDelegate.estimateMaxScrollOffset(constraints, delegate.estimatedChildCount);
}
}
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget) : super(widget);
......@@ -241,19 +299,41 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
});
}
double _extrapolateMaxScrollOffset(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) {
final int childCount = widget.delegate.estimatedChildCount;
if (childCount == null)
return double.INFINITY;
if (lastIndex == childCount - 1)
return trailingScrollOffset;
final int reifiedCount = lastIndex - firstIndex + 1;
final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
final int remainingCount = childCount - lastIndex - 1;
return trailingScrollOffset + averageExtent * remainingCount;
}
@override
double estimateScrollOffsetExtent({
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
assert(lastIndex >= firstIndex);
return widget.delegate.estimateScrollOffsetExtent(
return widget.estimateMaxScrollOffset(
constraints,
firstIndex,
lastIndex,
leadingScrollOffset,
trailingScrollOffset,
) ?? _extrapolateMaxScrollOffset(
firstIndex,
lastIndex,
leadingScrollOffset,
trailingScrollOffset
trailingScrollOffset,
);
}
......
......@@ -43,7 +43,7 @@ class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager {
}
@override
double estimateScrollOffsetExtent({
double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex,
int lastIndex,
double leadingScrollOffset,
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
final Key blockKey = new Key('test');
......@@ -120,7 +121,7 @@ void main() {
expect(key.currentState.scrollOffset, 0.0);
});
testWidgets('SliverBlockChildListDelegate.estimateScrollOffsetExtent hits end', (WidgetTester tester) async {
testWidgets('SliverBlockChildListDelegate.estimateMaxScrollOffset hits end', (WidgetTester tester) async {
SliverChildListDelegate delegate = new SliverChildListDelegate(<Widget>[
new Container(),
new Container(),
......@@ -129,6 +130,23 @@ void main() {
new Container(),
]);
expect(delegate.estimateScrollOffsetExtent(3, 4, 25.0, 26.0), equals(26.0));
await tester.pumpWidget(new ScrollableViewport2(
slivers: <Widget>[
new SliverBlock(
delegate: delegate,
),
],
));
final SliverMultiBoxAdaptorElement element = tester.element(find.byType(SliverBlock));
final double maxScrollOffset = element.estimateMaxScrollOffset(
null,
firstIndex: 3,
lastIndex: 4,
leadingScrollOffset: 25.0,
trailingScrollOffset: 26.0
);
expect(maxScrollOffset, equals(26.0));
});
}
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'states.dart';
void main() {
testWidgets('Empty ScrollGrid', (WidgetTester tester) async {
await tester.pumpWidget(new ScrollGrid.count(
crossAxisCount: 4,
children: const <Widget>[],
));
});
testWidgets('ScrollGrid.count control test', (WidgetTester tester) async {
List<String> log = <String>[];
await tester.pumpWidget(new ScrollGrid.count(
crossAxisCount: 4,
children: kStates.map((String state) {
return new GestureDetector(
onTap: () {
log.add(state);
},
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF0000FF),
),
child: new Text(state),
),
);
}).toList(),
));
expect(tester.getSize(find.text('Arkansas')), equals(const Size(200.0, 200.0)));
for (int i = 0; i < 8; ++i) {
await tester.tap(find.text(kStates[i]));
expect(log, equals(<String>[kStates[i]]));
log.clear();
}
expect(find.text(kStates[12]), findsNothing);
expect(find.text('Nevada'), findsNothing);
await tester.scroll(find.text('Arkansas'), const Offset(0.0, -200.0));
await tester.pump();
for (int i = 0; i < 4; ++i)
expect(find.text(kStates[i]), findsNothing);
for (int i = 4; i < 12; ++i) {
await tester.tap(find.text(kStates[i]));
expect(log, equals(<String>[kStates[i]]));
log.clear();
}
await tester.scroll(find.text('Delaware'), const Offset(0.0, -4000.0));
await tester.pump();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Pennsylvania'), findsNothing);
expect(tester.getCenter(find.text('Tennessee')),
equals(const Point(300.0, 100.0)));
await tester.tap(find.text('Tennessee'));
expect(log, equals(<String>['Tennessee']));
log.clear();
await tester.scroll(find.text('Tennessee'), const Offset(0.0, 200.0));
await tester.pump();
await tester.tap(find.text('Tennessee'));
expect(log, equals(<String>['Tennessee']));
log.clear();
await tester.tap(find.text('Pennsylvania'));
expect(log, equals(<String>['Pennsylvania']));
log.clear();
});
testWidgets('ScrollGrid.extent control test', (WidgetTester tester) async {
List<String> log = <String>[];
await tester.pumpWidget(new ScrollGrid.extent(
maxCrossAxisExtent: 200.0,
children: kStates.map((String state) {
return new GestureDetector(
onTap: () {
log.add(state);
},
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF0000FF),
),
child: new Text(state),
),
);
}).toList(),
));
expect(tester.getSize(find.text('Arkansas')), equals(const Size(200.0, 200.0)));
for (int i = 0; i < 8; ++i) {
await tester.tap(find.text(kStates[i]));
expect(log, equals(<String>[kStates[i]]));
log.clear();
}
expect(find.text('Nevada'), findsNothing);
await tester.scroll(find.text('Arkansas'), const Offset(0.0, -4000.0));
await tester.pump();
expect(find.text('Alabama'), findsNothing);
expect(tester.getCenter(find.text('Tennessee')),
equals(const Point(300.0, 100.0)));
await tester.tap(find.text('Tennessee'));
expect(log, equals(<String>['Tennessee']));
log.clear();
});
testWidgets('ScrollGrid large scroll jump', (WidgetTester tester) async {
List<int> log = <int>[];
await tester.pumpWidget(
new ScrollGrid.extent(
scrollDirection: Axis.horizontal,
maxCrossAxisExtent: 200.0,
childAspectRatio: 0.75,
children: new List<Widget>.generate(80, (int i) {
return new Builder(
builder: (BuildContext context) {
log.add(i);
return new Container(
child: new Text('$i'),
);
}
);
}),
),
);
expect(tester.getSize(find.text('4')), equals(const Size(200.0 / 0.75, 200.0)));
expect(log, equals(<int>[
0, 1, 2, // col 0
3, 4, 5, // col 1
6, 7, 8, // col 2
]));
log.clear();
Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position;
position.jumpTo(3025.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[
33, 34, 35, // col 11
36, 37, 38, // col 12
39, 40, 41, // col 13
42, 43, 44, // col 14
]));
log.clear();
position.jumpTo(975.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[
9, 10, 11, // col 3
12, 13, 14, // col 4
15, 16, 17, // col 5
18, 19, 20, // col 6
]));
log.clear();
});
testWidgets('ScrollGrid - change crossAxisCount', (WidgetTester tester) async {
List<int> log = <int>[];
await tester.pumpWidget(
new ScrollGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
children: new List<Widget>.generate(40, (int i) {
return new Builder(
builder: (BuildContext context) {
log.add(i);
return new Container(
child: new Text('$i'),
);
}
);
}),
),
);
expect(tester.getSize(find.text('4')), equals(const Size(200.0, 200.0)));
expect(log, equals(<int>[
0, 1, 2, 3, // row 0
4, 5, 6, 7, // row 1
8, 9, 10, 11, // row 2
]));
log.clear();
await tester.pumpWidget(
new ScrollGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
children: new List<Widget>.generate(40, (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, // row 0
4, 5, 6, 7, // row 1
8, 9, 10, 11, // row 2
]));
log.clear();
expect(tester.getSize(find.text('3')), equals(const Size(400.0, 400.0)));
expect(find.text('4'), findsNothing);
});
testWidgets('ScrollGrid - change maxChildCrossAxisExtent', (WidgetTester tester) async {
List<int> log = <int>[];
await tester.pumpWidget(
new ScrollGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
),
children: new List<Widget>.generate(40, (int i) {
return new Builder(
builder: (BuildContext context) {
log.add(i);
return new Container(
child: new Text('$i'),
);
}
);
}),
),
);
expect(tester.getSize(find.text('4')), equals(const Size(200.0, 200.0)));
expect(log, equals(<int>[
0, 1, 2, 3, // row 0
4, 5, 6, 7, // row 1
8, 9, 10, 11, // row 2
]));
log.clear();
await tester.pumpWidget(
new ScrollGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400.0,
),
children: new List<Widget>.generate(40, (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, // row 0
4, 5, 6, 7, // row 1
8, 9, 10, 11, // row 2
]));
log.clear();
expect(tester.getSize(find.text('3')), equals(const Size(400.0, 400.0)));
expect(find.text('4'), findsNothing);
});
}
......@@ -5,65 +5,14 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
const List<String> _kStates = const <String>[
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
];
import 'states.dart';
void main() {
testWidgets('ScrollView control test', (WidgetTester tester) async {
List<String> log = <String>[];
await tester.pumpWidget(new ScrollView(
children: _kStates.map<Widget>((String state) {
children: kStates.map<Widget>((String state) {
return new GestureDetector(
onTap: () {
log.add(state);
......@@ -99,7 +48,7 @@ void main() {
testWidgets('ScrollView restart ballistic activity out of range', (WidgetTester tester) async {
Widget buildScrollView(int n) {
return new ScrollView(
children: _kStates.take(n).map<Widget>((String state) {
children: kStates.take(n).map<Widget>((String state) {
return new Container(
height: 200.0,
decoration: const BoxDecoration(
......
// 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.
const List<String> kStates = const <String>[
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
];
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