mirror of
https://github.com/flutter/flutter
synced 2024-11-05 18:37:51 +00:00
CustomScrollView (#7881)
Also, use CustomScrollView in Shrine and fix a bug with one-line grids not painting properly due to their reporiting zero paintExtent.
This commit is contained in:
parent
667424659d
commit
ff14f35d6d
7 changed files with 177 additions and 68 deletions
|
@ -39,9 +39,9 @@ class OrderItem extends StatelessWidget {
|
|||
height: 248.0,
|
||||
child: new Hero(
|
||||
tag: product.tag,
|
||||
child: new Image.asset(product.imageAsset, fit: ImageFit.contain)
|
||||
)
|
||||
)
|
||||
child: new Image.asset(product.imageAsset, fit: ImageFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
new SizedBox(height: 24.0),
|
||||
new Row(
|
||||
|
@ -52,14 +52,14 @@ class OrderItem extends StatelessWidget {
|
|||
child: new Icon(
|
||||
Icons.info_outline,
|
||||
size: 24.0,
|
||||
color: const Color(0xFFFFE0E0)
|
||||
)
|
||||
)
|
||||
color: const Color(0xFFFFE0E0),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Expanded(
|
||||
child: new Text(product.name, style: theme.featureTitleStyle)
|
||||
)
|
||||
]
|
||||
child: new Text(product.name, style: theme.featureTitleStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
new Padding(
|
||||
padding: const EdgeInsets.only(left: 56.0),
|
||||
|
@ -75,8 +75,8 @@ class OrderItem extends StatelessWidget {
|
|||
child: new Container(
|
||||
decoration: new BoxDecoration(
|
||||
border: new Border.all(
|
||||
color: const Color(0xFFD9D9D9)
|
||||
)
|
||||
color: const Color(0xFFD9D9D9),
|
||||
),
|
||||
),
|
||||
child: new DropdownButton<int>(
|
||||
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
|
||||
|
@ -84,33 +84,33 @@ class OrderItem extends StatelessWidget {
|
|||
value: value,
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: new Text('Quantity $value', style: theme.quantityMenuStyle)
|
||||
)
|
||||
child: new Text('Quantity $value', style: theme.quantityMenuStyle),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: quantity,
|
||||
onChanged: quantityChanged
|
||||
)
|
||||
)
|
||||
)
|
||||
onChanged: quantityChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
new SizedBox(height: 16.0),
|
||||
new SizedBox(
|
||||
height: 24.0,
|
||||
child: new Align(
|
||||
alignment: FractionalOffset.bottomLeft,
|
||||
child: new Text(product.vendor.name, style: theme.vendorTitleStyle)
|
||||
)
|
||||
child: new Text(product.vendor.name, style: theme.vendorTitleStyle),
|
||||
),
|
||||
),
|
||||
new SizedBox(height: 16.0),
|
||||
new Text(product.vendor.description, style: theme.vendorStyle),
|
||||
new SizedBox(height: 24.0)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
new SizedBox(height: 24.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -174,39 +174,47 @@ class _OrderPageState extends State<OrderPage> {
|
|||
backgroundColor: const Color(0xFF16F0F0),
|
||||
child: new Icon(
|
||||
Icons.add_shopping_cart,
|
||||
color: Colors.black
|
||||
)
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
body: new Block(
|
||||
children: <Widget>[
|
||||
new OrderItem(
|
||||
product: config.order.product,
|
||||
quantity: currentOrder.quantity,
|
||||
quantityChanged: (int value) { updateOrder(quantity: value); }
|
||||
body: new CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
new SliverList(
|
||||
delegate: new SliverChildListDelegate(<Widget>[
|
||||
new OrderItem(
|
||||
product: config.order.product,
|
||||
quantity: currentOrder.quantity,
|
||||
quantityChanged: (int value) { updateOrder(quantity: value); },
|
||||
),
|
||||
new SizedBox(height: 24.0),
|
||||
]),
|
||||
),
|
||||
new SizedBox(height: 24.0),
|
||||
new FixedColumnCountGrid(
|
||||
columnCount: 2,
|
||||
rowSpacing: 8.0,
|
||||
columnSpacing: 8.0,
|
||||
new SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
tileAspectRatio: 160.0 / 216.0, // width/height
|
||||
children: config.products
|
||||
.where((Product product) => product != config.order.product)
|
||||
.map((Product product) {
|
||||
return new RepaintBoundary(
|
||||
child: new Card(
|
||||
elevation: 1,
|
||||
child: new Image.asset(
|
||||
product.imageAsset,
|
||||
fit: ImageFit.contain
|
||||
)
|
||||
)
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
]
|
||||
)
|
||||
child: new SliverGrid(
|
||||
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
childAspectRatio: 160.0 / 216.0, // width/height
|
||||
),
|
||||
delegate: new SliverChildListDelegate(
|
||||
config.products
|
||||
.where((Product product) => product != config.order.product)
|
||||
.map((Product product) {
|
||||
return new Card(
|
||||
elevation: 1,
|
||||
child: new Image.asset(
|
||||
product.imageAsset,
|
||||
fit: ImageFit.contain,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +228,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> {
|
|||
ShrineOrderRoute({
|
||||
this.order,
|
||||
WidgetBuilder builder,
|
||||
RouteSettings settings: const RouteSettings()
|
||||
RouteSettings settings: const RouteSettings(),
|
||||
}) : super(builder: builder, settings: settings) {
|
||||
assert(order != null);
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
|
|||
trailingScrollOffset: trailingScrollOffset,
|
||||
);
|
||||
|
||||
final double paintedExtent = calculatePaintOffset(
|
||||
final double paintExtent = calculatePaintOffset(
|
||||
constraints,
|
||||
from: leadingScrollOffset,
|
||||
to: trailingScrollOffset,
|
||||
|
@ -114,7 +114,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
|
|||
|
||||
geometry = new SliverGeometry(
|
||||
scrollExtent: estimatedMaxScrollOffset,
|
||||
paintExtent: paintedExtent,
|
||||
paintExtent: paintExtent,
|
||||
maxPaintExtent: estimatedMaxScrollOffset,
|
||||
// Conservative to avoid flickering away the clip during scroll.
|
||||
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,
|
||||
|
|
|
@ -52,6 +52,10 @@ class SliverGridGeometry {
|
|||
/// scroll axis is horizontal, this extent is the child's height.
|
||||
final double crossAxisExtent;
|
||||
|
||||
/// The scroll offset of the trailing edge of the child relative to the
|
||||
/// leading edge of the parent.
|
||||
double get trailingScrollOffset => scrollOffset + mainAxisExtent;
|
||||
|
||||
/// Returns a tight [BoxConstraints] that forces the child to have the
|
||||
/// required size.
|
||||
BoxConstraints getBoxConstraints(SliverConstraints constraints) {
|
||||
|
@ -61,6 +65,16 @@ class SliverGridGeometry {
|
|||
crossAxisExtent: crossAxisExtent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SliverGridGeometry('
|
||||
'scrollOffset: $scrollOffset, '
|
||||
'crossAxisOffset: $crossAxisOffset, '
|
||||
'mainAxisExtent: $mainAxisExtent, '
|
||||
'crossAxisExtent: $crossAxisExtent'
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
class SliverGridParentData extends SliverMultiBoxAdaptorParentData {
|
||||
|
@ -329,7 +343,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
|
|||
final SliverGridGeometry firstChildGridGeometry = _gridDelegate
|
||||
.getGeometryForChildIndex(constraints, firstIndex);
|
||||
double leadingScrollOffset = firstChildGridGeometry.scrollOffset;
|
||||
double trailingScrollOffset = firstChildGridGeometry.scrollOffset;
|
||||
double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
|
||||
|
||||
if (firstChild == null) {
|
||||
if (!addInitialChild(index: firstIndex,
|
||||
|
@ -352,8 +366,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
|
|||
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
|
||||
assert(childParentData.index == index);
|
||||
trailingChildWithLayout ??= child;
|
||||
if (gridGeometry.scrollOffset > trailingScrollOffset)
|
||||
trailingScrollOffset = gridGeometry.scrollOffset;
|
||||
trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
|
||||
}
|
||||
|
||||
assert(childScrollOffset(firstChild) <= scrollOffset);
|
||||
|
@ -389,8 +402,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
|
|||
childParentData.layoutOffset = gridGeometry.scrollOffset;
|
||||
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
|
||||
assert(childParentData.index == index);
|
||||
if (gridGeometry.scrollOffset > trailingScrollOffset)
|
||||
trailingScrollOffset = gridGeometry.scrollOffset;
|
||||
trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
|
||||
}
|
||||
|
||||
final int lastIndex = indexOf(lastChild);
|
||||
|
@ -407,7 +419,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
|
|||
trailingScrollOffset: trailingScrollOffset,
|
||||
);
|
||||
|
||||
final double paintedExtent = calculatePaintOffset(
|
||||
final double paintExtent = calculatePaintOffset(
|
||||
constraints,
|
||||
from: leadingScrollOffset,
|
||||
to: trailingScrollOffset,
|
||||
|
@ -415,7 +427,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
|
|||
|
||||
geometry = new SliverGeometry(
|
||||
scrollExtent: estimatedTotalExtent,
|
||||
paintExtent: paintedExtent,
|
||||
paintExtent: paintExtent,
|
||||
maxPaintExtent: estimatedTotalExtent,
|
||||
// Conservative to avoid complexity.
|
||||
hasVisualOverflow: true,
|
||||
|
|
|
@ -197,14 +197,14 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
|
|||
);
|
||||
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
|
||||
}
|
||||
final double paintedExtent = calculatePaintOffset(
|
||||
final double paintExtent = calculatePaintOffset(
|
||||
constraints,
|
||||
from: childScrollOffset(firstChild),
|
||||
to: endScrollOffset,
|
||||
);
|
||||
geometry = new SliverGeometry(
|
||||
scrollExtent: estimatedMaxScrollOffset,
|
||||
paintExtent: paintedExtent,
|
||||
paintExtent: paintExtent,
|
||||
maxPaintExtent: estimatedMaxScrollOffset,
|
||||
// Conservative to avoid flickering away the clip during scroll.
|
||||
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
|
||||
|
|
|
@ -82,6 +82,28 @@ abstract class ScrollView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class CustomScrollView extends ScrollView {
|
||||
CustomScrollView({
|
||||
Key key,
|
||||
Axis scrollDirection: Axis.vertical,
|
||||
bool reverse: false,
|
||||
ScrollPhysics physics,
|
||||
bool shrinkWrap: false,
|
||||
this.slivers: const <Widget>[],
|
||||
}) : super(
|
||||
key: key,
|
||||
scrollDirection: scrollDirection,
|
||||
reverse: reverse,
|
||||
physics: physics,
|
||||
shrinkWrap: shrinkWrap,
|
||||
);
|
||||
|
||||
final List<Widget> slivers;
|
||||
|
||||
@override
|
||||
List<Widget> buildSlivers(BuildContext context) => slivers;
|
||||
}
|
||||
|
||||
abstract class BoxScrollView extends ScrollView {
|
||||
BoxScrollView({
|
||||
Key key,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import 'states.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -304,6 +305,29 @@ void main() {
|
|||
expect(find.text('4'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('One-line GridView paints', (WidgetTester tester) async {
|
||||
const Color green = const Color(0xFF00FF00);
|
||||
|
||||
final Container container = new Container(
|
||||
decoration: const BoxDecoration(
|
||||
backgroundColor: green,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(new Center(
|
||||
child: new SizedBox(
|
||||
height: 200.0,
|
||||
child: new GridView.count(
|
||||
crossAxisCount: 2,
|
||||
children: <Widget>[ container, container, container, container ],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.byType(GridView), paints..rect(color: green)..rect(color: green));
|
||||
expect(find.byType(GridView), isNot(paints..rect(color: green)..rect(color: green)..rect(color: green)));
|
||||
});
|
||||
|
||||
// TODO(ianh): can you tap a grid cell that is slightly off the bottom of the screen?
|
||||
// (try it with the flutter_gallery Grid demo)
|
||||
}
|
|
@ -72,4 +72,47 @@ void main() {
|
|||
Viewport2 viewport = tester.widget(find.byType(Viewport2));
|
||||
expect(viewport.offset.pixels, equals(2400.0));
|
||||
});
|
||||
|
||||
testWidgets('CustomScrollView control test', (WidgetTester tester) async {
|
||||
List<String> log = <String>[];
|
||||
|
||||
await tester.pumpWidget(new CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
new SliverList(
|
||||
delegate: new SliverChildListDelegate(
|
||||
kStates.map<Widget>((String state) {
|
||||
return new GestureDetector(
|
||||
onTap: () {
|
||||
log.add(state);
|
||||
},
|
||||
child: new Container(
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
backgroundColor: const Color(0xFF0000FF),
|
||||
),
|
||||
child: new Text(state),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
await tester.tap(find.text('Alabama'));
|
||||
expect(log, equals(<String>['Alabama']));
|
||||
log.clear();
|
||||
|
||||
expect(find.text('Nevada'), findsNothing);
|
||||
|
||||
await tester.scroll(find.text('Alabama'), const Offset(0.0, -4000.0));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Alabama'), findsNothing);
|
||||
expect(tester.getCenter(find.text('Massachusetts')), equals(const Point(400.0, 100.0)));
|
||||
|
||||
await tester.tap(find.text('Massachusetts'));
|
||||
expect(log, equals(<String>['Massachusetts']));
|
||||
log.clear();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue