Commit eba5fb1a authored by Adam Barth's avatar Adam Barth Committed by GitHub

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.
parent efa1120a
......@@ -376,8 +376,8 @@ abstract class InkFeature {
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);
......
......@@ -999,14 +999,19 @@ abstract class RenderSliverHelpers implements RenderSliver {
/// 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;
}
}
......
......@@ -506,10 +506,8 @@ abstract class RenderSliverBlock extends RenderSliver
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 @@ abstract class RenderSliverBlock extends RenderSliver
@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 @@ abstract class RenderSliverBlock extends RenderSliver
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);
......
......@@ -10,6 +10,56 @@ import 'package:flutter/scheduler.dart';
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
......
......@@ -509,7 +509,7 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
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 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
recognizer.onEnd(new DragEndDetails());
recognizer.onEnd(new DragEndDetails(primaryVelocity: 0.0));
return;
}
}
......
......@@ -608,7 +608,7 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity {
}
void _end() {
position.beginIdleActivity();
position?.beginIdleActivity();
}
@override
......
......@@ -304,7 +304,9 @@ abstract class ScrollActivity {
bool get isScrolling;
@mustCallSuper
void dispose() { }
void dispose() {
_position = null;
}
@override
String toString() => '$runtimeType';
......
......@@ -18,7 +18,7 @@ void main() {
title: new Text('Home'),
),
drawer: new Drawer(
child: new Block(
child: new ScrollView(
children: <Widget>[
new AboutDrawerItem(
applicationVersion: '0.1.2',
......
......@@ -19,22 +19,20 @@ void main() {
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 @@ void main() {
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)
)
)
)
)
......
......@@ -12,7 +12,7 @@ void main() {
await tester.pumpWidget(
new Scaffold(
drawer: new Drawer(
child: new Block(
child: new ScrollView(
children: <Widget>[
new DrawerHeader(
child: new Container(
......
......@@ -50,6 +50,6 @@ void main() {
});
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()]));
});
}
......@@ -312,7 +312,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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();
});
......
......@@ -149,7 +149,7 @@ void main() {
autovalidate: true,
child: new Focus(
key: focusKey,
child: new Block(
child: new ScrollView(
children: <Widget>[
new TextField(
key: fieldKey
......
......@@ -11,31 +11,37 @@ Key thirdKey = new Key('third');
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 @@ void main() {
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'));
......
// 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();
});
}
......@@ -13,11 +13,11 @@ void main() {
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();
......
......@@ -126,20 +126,22 @@ void main() {
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 @@ void main() {
await tester.pumpWidget(
new SemanticsDebugger(
child: new Block(
child: new ScrollView(
children: <Widget>[
new Container(
key: childKey,
......@@ -173,22 +175,22 @@ void main() {
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 @@ void main() {
await tester.pumpWidget(
new SemanticsDebugger(
child: new Material(
child: new Block(
child: new ScrollView(
children: <Widget>[
new Checkbox(
key: keyTop,
......
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