Add a sliver-based ScrollView (#7627)
This patch introduces ScrollView, which is a convenience widget for using a
SliverBlock. This patch also switches a number of tests from Block to
ScrollView. Once we support more features of block (e.g., padding and
shrinkwrapping), we'll be able to move over more clients.
diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart
index 680b717..cdd8a52 100644
--- a/packages/flutter/lib/src/material/material.dart
+++ b/packages/flutter/lib/src/material/material.dart
@@ -376,8 +376,8 @@
assert(referenceBox.attached);
assert(!_debugDisposed);
// find the chain of renderers from us to the feature's referenceBox
- final List<RenderBox> descendants = <RenderBox>[referenceBox];
- RenderBox node = referenceBox;
+ final List<RenderObject> descendants = <RenderObject>[referenceBox];
+ RenderObject node = referenceBox;
while (node != _controller) {
node = node.parent;
assert(node != null);
diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart
index 39c388a..99fb57c 100644
--- a/packages/flutter/lib/src/rendering/sliver.dart
+++ b/packages/flutter/lib/src/rendering/sliver.dart
@@ -999,14 +999,19 @@
/// Calling this for a child that is not visible is not valid.
@protected
void applyPaintTransformForBoxChild(RenderBox child, Matrix4 transform) {
- final double sign = _getRightWayUp(constraints) ? 1.0 : -1.0;
+ final bool rightWayUp = _getRightWayUp(constraints);
+ double delta = childPosition(child);
assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
- transform.translate(childPosition(child) * sign, 0.0);
+ if (!rightWayUp)
+ delta = geometry.paintExtent - child.size.width - delta;
+ transform.translate(delta, 0.0);
break;
case Axis.vertical:
- transform.translate(0.0, childPosition(child) * sign);
+ if (!rightWayUp)
+ delta = geometry.paintExtent - child.size.height - delta;
+ transform.translate(0.0, delta);
break;
}
}
diff --git a/packages/flutter/lib/src/rendering/sliver_block.dart b/packages/flutter/lib/src/rendering/sliver_block.dart
index 1a9a031..e89fbda 100644
--- a/packages/flutter/lib/src/rendering/sliver_block.dart
+++ b/packages/flutter/lib/src/rendering/sliver_block.dart
@@ -506,10 +506,8 @@
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
RenderBox child = lastChild;
while (child != null) {
- if (child != null) {
- if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
- return true;
- }
+ if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
+ return true;
child = childBefore(child);
}
return false;
@@ -517,46 +515,12 @@
@override
double childPosition(RenderBox child) {
- return offsetOf(child);
+ return offsetOf(child) - constraints.scrollOffset;
}
- // TODO(ianh): There's a lot of duplicate code in the next two functions,
- // but I don't see a good way to avoid it, since both functions are hot.
-
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- // coordinate system origin here is at 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 = new Offset(0.0, geometry.paintExtent);
- addExtent = true;
- break;
- case AxisDirection.right:
- unitOffset = const Offset(1.0, 0.0);
- originOffset = Offset.zero;
- addExtent = false;
- break;
- case AxisDirection.down:
- unitOffset = const Offset(0.0, 1.0);
- originOffset = Offset.zero;
- addExtent = false;
- break;
- case AxisDirection.left:
- unitOffset = const Offset(-1.0, 0.0);
- originOffset = new Offset(geometry.paintExtent, 0.0);
- addExtent = true;
- break;
- }
- assert(unitOffset != null);
- assert(addExtent != null);
- Offset childOffset = originOffset + unitOffset * (offsetOf(child) - constraints.scrollOffset);
- if (addExtent)
- childOffset += unitOffset * paintExtentOf(child);
- transform.translate(childOffset.dx, childOffset.dy);
+ applyPaintTransformForBoxChild(child, transform);
}
@override
@@ -593,7 +557,7 @@
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
- Offset childOffset = originOffset + unitOffset * (offsetOf(child) - constraints.scrollOffset);
+ Offset childOffset = originOffset + unitOffset * childPosition(child);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
context.paintChild(child, childOffset);
diff --git a/packages/flutter/lib/src/widgets/block.dart b/packages/flutter/lib/src/widgets/block.dart
index c12ef4b..27b6431 100644
--- a/packages/flutter/lib/src/widgets/block.dart
+++ b/packages/flutter/lib/src/widgets/block.dart
@@ -10,6 +10,56 @@
import 'framework.dart';
import 'basic.dart';
+import 'scrollable.dart';
+
+class ScrollView extends StatelessWidget {
+ ScrollView({
+ Key key,
+ this.scrollDirection: Axis.vertical,
+ this.anchor: 0.0,
+ this.initialScrollOffset: 0.0,
+ this.scrollBehavior,
+ this.center,
+ this.children,
+ }) : super(key: key);
+
+ final Axis scrollDirection;
+
+ final double anchor;
+
+ final double initialScrollOffset;
+
+ final ScrollBehavior2 scrollBehavior;
+
+ 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) {
+ return new Scrollable2(
+ axisDirection: _getDirection(context),
+ anchor: anchor,
+ initialScrollOffset: initialScrollOffset,
+ scrollBehavior: scrollBehavior,
+ center: center,
+ children: <Widget>[
+ new SliverBlock(delegate: new SliverBlockChildListDelegate(children)),
+ ],
+ );
+ }
+}
abstract class SliverBlockDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart
index bfb852a..51bbe59 100644
--- a/packages/flutter/lib/src/widgets/gesture_detector.dart
+++ b/packages/flutter/lib/src/widgets/gesture_detector.dart
@@ -509,7 +509,7 @@
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
- recognizer.onEnd(new DragEndDetails());
+ recognizer.onEnd(new DragEndDetails(primaryVelocity: 0.0));
return;
}
}
@@ -536,7 +536,7 @@
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
- recognizer.onEnd(new DragEndDetails());
+ recognizer.onEnd(new DragEndDetails(primaryVelocity: 0.0));
return;
}
}
diff --git a/packages/flutter/lib/src/widgets/scroll_absolute.dart b/packages/flutter/lib/src/widgets/scroll_absolute.dart
index 6507673..3c92e60 100644
--- a/packages/flutter/lib/src/widgets/scroll_absolute.dart
+++ b/packages/flutter/lib/src/widgets/scroll_absolute.dart
@@ -608,7 +608,7 @@
}
void _end() {
- position.beginIdleActivity();
+ position?.beginIdleActivity();
}
@override
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index d4e6912..c4acaa9 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -304,7 +304,9 @@
bool get isScrolling;
@mustCallSuper
- void dispose() { }
+ void dispose() {
+ _position = null;
+ }
@override
String toString() => '$runtimeType';
diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart
index 325e7c9..6775ec2 100644
--- a/packages/flutter/test/material/about_test.dart
+++ b/packages/flutter/test/material/about_test.dart
@@ -18,7 +18,7 @@
title: new Text('Home'),
),
drawer: new Drawer(
- child: new Block(
+ child: new ScrollView(
children: <Widget>[
new AboutDrawerItem(
applicationVersion: '0.1.2',
diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart
index cbe534a..9ad4c25 100644
--- a/packages/flutter/test/material/date_picker_test.dart
+++ b/packages/flutter/test/material/date_picker_test.dart
@@ -19,22 +19,20 @@
builder: (BuildContext context, StateSetter setState) {
return new Positioned(
width: 400.0,
- child: new Block(
- children: <Widget>[
- new Material(
- child: new MonthPicker(
- firstDate: new DateTime(0),
- lastDate: new DateTime(9999),
- key: _datePickerKey,
- selectedDate: _selectedDate,
- onChanged: (DateTime value) {
- setState(() {
- _selectedDate = value;
- });
- }
- )
+ child: new SingleChildScrollView(
+ child: new Material(
+ child: new MonthPicker(
+ firstDate: new DateTime(0),
+ lastDate: new DateTime(9999),
+ key: _datePickerKey,
+ selectedDate: _selectedDate,
+ onChanged: (DateTime value) {
+ setState(() {
+ _selectedDate = value;
+ });
+ }
)
- ]
+ )
)
);
}
@@ -91,15 +89,13 @@
return new IntrinsicWidth(
child: new IntrinsicHeight(
child: new Material(
- child: new Block(
- children: <Widget>[
- new MonthPicker(
- firstDate: new DateTime(0),
- lastDate: new DateTime(9999),
- onChanged: (DateTime value) { },
- selectedDate: new DateTime(2000, DateTime.JANUARY, 1)
- )
- ]
+ child: new SingleChildScrollView(
+ child: new MonthPicker(
+ firstDate: new DateTime(0),
+ lastDate: new DateTime(9999),
+ onChanged: (DateTime value) { },
+ selectedDate: new DateTime(2000, DateTime.JANUARY, 1)
+ )
)
)
)
diff --git a/packages/flutter/test/material/drawer_test.dart b/packages/flutter/test/material/drawer_test.dart
index 4b4cd6f..98e8925 100644
--- a/packages/flutter/test/material/drawer_test.dart
+++ b/packages/flutter/test/material/drawer_test.dart
@@ -12,7 +12,7 @@
await tester.pumpWidget(
new Scaffold(
drawer: new Drawer(
- child: new Block(
+ child: new ScrollView(
children: <Widget>[
new DrawerHeader(
child: new Container(
diff --git a/packages/flutter/test/widgets/container_test.dart b/packages/flutter/test/widgets/container_test.dart
index ec250f9..3f195e6 100644
--- a/packages/flutter/test/widgets/container_test.dart
+++ b/packages/flutter/test/widgets/container_test.dart
@@ -50,6 +50,6 @@
});
testWidgets('Can be placed in an infinite box', (WidgetTester tester) async {
- await tester.pumpWidget(new Block(children: <Widget>[new Container()]));
+ await tester.pumpWidget(new ScrollView(children: <Widget>[new Container()]));
});
}
diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart
index cbcf02c..927bb74 100644
--- a/packages/flutter/test/widgets/draggable_test.dart
+++ b/packages/flutter/test/widgets/draggable_test.dart
@@ -312,7 +312,7 @@
Point firstLocation, secondLocation, thirdLocation;
await tester.pumpWidget(new MaterialApp(
- home: new Block(
+ home: new ScrollView(
children: <Widget>[
new DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
@@ -409,7 +409,7 @@
await gesture.up();
await tester.pump();
expect(events, equals(<String>[]));
- expect(tester.getCenter(find.text('Target')).y, lessThan(0.0));
+ expect(find.text('Target'), findsNothing);
events.clear();
});
@@ -418,7 +418,7 @@
Point firstLocation, secondLocation, thirdLocation;
await tester.pumpWidget(new MaterialApp(
- home: new Block(
+ home: new ScrollView(
scrollDirection: Axis.horizontal,
children: <Widget>[
new DragTarget<int>(
@@ -516,7 +516,7 @@
await gesture.up();
await tester.pump();
expect(events, equals(<String>[]));
- expect(tester.getCenter(find.text('Target')).x, lessThan(0.0));
+ expect(find.text('Target'), findsNothing);
events.clear();
});
diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart
index 7d8bd26..65fb2ff 100644
--- a/packages/flutter/test/widgets/form_test.dart
+++ b/packages/flutter/test/widgets/form_test.dart
@@ -149,7 +149,7 @@
autovalidate: true,
child: new Focus(
key: focusKey,
- child: new Block(
+ child: new ScrollView(
children: <Widget>[
new TextField(
key: fieldKey
diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart
index 25fff3d..1c76377 100644
--- a/packages/flutter/test/widgets/heroes_test.dart
+++ b/packages/flutter/test/widgets/heroes_test.dart
@@ -11,31 +11,37 @@
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new Material(
- child: new Block(children: <Widget>[
- new Container(height: 100.0, width: 100.0),
- new Card(child: new Hero(tag: 'a', child: new Container(height: 100.0, width: 100.0, key: firstKey))),
- new Container(height: 100.0, width: 100.0),
- new FlatButton(child: new Text('two'), onPressed: () => Navigator.pushNamed(context, '/two')),
- ])
+ child: new ScrollView(
+ children: <Widget>[
+ new Container(height: 100.0, width: 100.0),
+ new Card(child: new Hero(tag: 'a', child: new Container(height: 100.0, width: 100.0, key: firstKey))),
+ new Container(height: 100.0, width: 100.0),
+ new FlatButton(child: new Text('two'), onPressed: () => Navigator.pushNamed(context, '/two')),
+ ]
+ )
),
'/two': (BuildContext context) => new Material(
- child: new Block(children: <Widget>[
- new Container(height: 150.0, width: 150.0),
- new Card(child: new Hero(tag: 'a', child: new Container(height: 150.0, width: 150.0, key: secondKey))),
- new Container(height: 150.0, width: 150.0),
- new FlatButton(child: new Text('three'), onPressed: () => Navigator.push(context, new ThreeRoute())),
- ])
+ child: new ScrollView(
+ children: <Widget>[
+ new Container(height: 150.0, width: 150.0),
+ new Card(child: new Hero(tag: 'a', child: new Container(height: 150.0, width: 150.0, key: secondKey))),
+ new Container(height: 150.0, width: 150.0),
+ new FlatButton(child: new Text('three'), onPressed: () => Navigator.push(context, new ThreeRoute())),
+ ]
+ )
),
};
class ThreeRoute extends MaterialPageRoute<Null> {
ThreeRoute() : super(builder: (BuildContext context) {
return new Material(
- child: new Block(children: <Widget>[
- new Container(height: 200.0, width: 200.0),
- new Card(child: new Hero(tag: 'a', child: new Container(height: 200.0, width: 200.0, key: thirdKey))),
- new Container(height: 200.0, width: 200.0),
- ])
+ child: new ScrollView(
+ children: <Widget>[
+ new Container(height: 200.0, width: 200.0),
+ new Card(child: new Hero(tag: 'a', child: new Container(height: 200.0, width: 200.0, key: thirdKey))),
+ new Container(height: 200.0, width: 200.0),
+ ]
+ )
);
});
}
@@ -161,12 +167,16 @@
MutatingRoute route = new MutatingRoute();
await tester.pumpWidget(new MaterialApp(
- home: new Material(child: new Block(children: <Widget>[
- new Hero(tag: 'a', child: new Text('foo')),
- new Builder(builder: (BuildContext context) {
- return new FlatButton(child: new Text('two'), onPressed: () => Navigator.push(context, route));
- })
- ]))
+ home: new Material(
+ child: new ScrollView(
+ children: <Widget>[
+ new Hero(tag: 'a', child: new Text('foo')),
+ new Builder(builder: (BuildContext context) {
+ return new FlatButton(child: new Text('two'), onPressed: () => Navigator.push(context, route));
+ })
+ ]
+ )
+ )
));
await tester.tap(find.text('two'));
diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart
new file mode 100644
index 0000000..73762b8
--- /dev/null
+++ b/packages/flutter/test/widgets/scroll_view_test.dart
@@ -0,0 +1,98 @@
+// 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';
+
+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',
+];
+
+void main() {
+ testWidgets('ScrollView control test', (WidgetTester tester) async {
+ List<String> log = <String>[];
+
+ await tester.pumpWidget(new ClipRect(child: new ScrollView(
+ children: _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();
+ });
+}
diff --git a/packages/flutter/test/widgets/scrollable_dispose_test.dart b/packages/flutter/test/widgets/scrollable_dispose_test.dart
index ff61bdc..24aa922 100644
--- a/packages/flutter/test/widgets/scrollable_dispose_test.dart
+++ b/packages/flutter/test/widgets/scrollable_dispose_test.dart
@@ -13,11 +13,11 @@
for (int i = 0; i < 250; i++)
textWidgets.add(new Text('$i'));
await tester.pumpWidget(new FlipWidget(
- left: new Block(children: textWidgets),
+ left: new ScrollView(children: textWidgets),
right: new Container()
));
- await tester.fling(find.byType(Scrollable), const Offset(0.0, -200.0), 1000.0);
+ await tester.fling(find.byType(ScrollView), const Offset(0.0, -200.0), 1000.0);
await tester.pump();
tester.state<FlipWidgetState>(find.byType(FlipWidget)).flip();
diff --git a/packages/flutter/test/widgets/semantics_debugger_test.dart b/packages/flutter/test/widgets/semantics_debugger_test.dart
index 246582e..ce8be6f 100644
--- a/packages/flutter/test/widgets/semantics_debugger_test.dart
+++ b/packages/flutter/test/widgets/semantics_debugger_test.dart
@@ -126,20 +126,22 @@
await tester.pumpWidget(
new SemanticsDebugger(
child: new Material(
- child: new Block(children: <Widget>[
- new RaisedButton(
- onPressed: () {
- log.add('top');
- },
- child: new Text('TOP'),
- ),
- new RaisedButton(
- onPressed: () {
- log.add('bottom');
- },
- child: new Text('BOTTOM'),
- ),
- ]),
+ child: new ScrollView(
+ children: <Widget>[
+ new RaisedButton(
+ onPressed: () {
+ log.add('top');
+ },
+ child: new Text('TOP'),
+ ),
+ new RaisedButton(
+ onPressed: () {
+ log.add('bottom');
+ },
+ child: new Text('BOTTOM'),
+ ),
+ ],
+ ),
),
),
);
@@ -158,7 +160,7 @@
await tester.pumpWidget(
new SemanticsDebugger(
- child: new Block(
+ child: new ScrollView(
children: <Widget>[
new Container(
key: childKey,
@@ -173,22 +175,22 @@
expect(tester.getTopLeft(find.byKey(childKey)).y, equals(0.0));
- await tester.fling(find.byType(Block), const Offset(0.0, -200.0), 200.0);
+ await tester.fling(find.byType(ScrollView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).y, equals(-480.0));
- await tester.fling(find.byType(Block), const Offset(200.0, 0.0), 200.0);
+ await tester.fling(find.byType(ScrollView), const Offset(200.0, 0.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).y, equals(-480.0));
- await tester.fling(find.byType(Block), const Offset(-200.0, 0.0), 200.0);
+ await tester.fling(find.byType(ScrollView), const Offset(-200.0, 0.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).y, equals(-480.0));
- await tester.fling(find.byType(Block), const Offset(0.0, 200.0), 200.0);
+ await tester.fling(find.byType(ScrollView), const Offset(0.0, 200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).y, equals(0.0));
@@ -245,7 +247,7 @@
await tester.pumpWidget(
new SemanticsDebugger(
child: new Material(
- child: new Block(
+ child: new ScrollView(
children: <Widget>[
new Checkbox(
key: keyTop,