Commit f103d397 authored by Hixie's avatar Hixie

Make hit testing work in horizontal scrolling list

Previously, hitTestChildren in RenderBlockViewport just didn't support
horizontal scrolling, due to an oversight.
parent a3ecdc30
...@@ -589,7 +589,7 @@ class PageableList<T> extends ScrollableList<T> { ...@@ -589,7 +589,7 @@ class PageableList<T> extends ScrollableList<T> {
ItemBuilder<T> itemBuilder, ItemBuilder<T> itemBuilder,
bool itemsWrap: false, bool itemsWrap: false,
double itemExtent, double itemExtent,
PageChangedCallback this.pageChanged, this.onPageChanged,
EdgeDims padding, EdgeDims padding,
this.duration: const Duration(milliseconds: 200), this.duration: const Duration(milliseconds: 200),
this.curve: ease this.curve: ease
...@@ -607,7 +607,7 @@ class PageableList<T> extends ScrollableList<T> { ...@@ -607,7 +607,7 @@ class PageableList<T> extends ScrollableList<T> {
final Duration duration; final Duration duration;
final Curve curve; final Curve curve;
final PageChangedCallback pageChanged; final PageChangedCallback onPageChanged;
PageableListState<T> createState() => new PageableListState(); PageableListState<T> createState() => new PageableListState();
} }
...@@ -633,8 +633,8 @@ class PageableListState<T> extends ScrollableListState<T, PageableList<T>> { ...@@ -633,8 +633,8 @@ class PageableListState<T> extends ScrollableListState<T, PageableList<T>> {
int get currentPage => (scrollOffset / config.itemExtent).floor() % itemCount; int get currentPage => (scrollOffset / config.itemExtent).floor() % itemCount;
void _notifyPageChanged(_) { void _notifyPageChanged(_) {
if (config.pageChanged != null) if (config.onPageChanged != null)
config.pageChanged(currentPage); config.onPageChanged(currentPage);
} }
void settleScrollOffset() { void settleScrollOffset() {
......
...@@ -377,11 +377,17 @@ class RenderBlockViewport extends RenderBlockBase { ...@@ -377,11 +377,17 @@ class RenderBlockViewport extends RenderBlockBase {
void applyPaintTransform(Matrix4 transform) { void applyPaintTransform(Matrix4 transform) {
super.applyPaintTransform(transform); super.applyPaintTransform(transform);
transform.translate(0.0, startOffset); if (isVertical)
transform.translate(0.0, startOffset);
else
transform.translate(startOffset, 0.0);
} }
void hitTestChildren(HitTestResult result, { Point position }) { void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset)); if (isVertical)
defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
else
defaultHitTestChildren(result, position: position + new Offset(-startOffset, 0.0));
} }
String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}startOffset: ${startOffset}\n'; String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}startOffset: ${startOffset}\n';
......
...@@ -25,15 +25,21 @@ class RootComponentState extends State<RootComponent> { ...@@ -25,15 +25,21 @@ class RootComponentState extends State<RootComponent> {
class WidgetTester { class WidgetTester {
// See thttps://github.com/flutter/engine/issues/1084 regarding frameTimeMs vs FakeAsync
void pumpFrame(Widget widget, [ double frameTimeMs = 0.0 ]) { void pumpFrame(Widget widget, [ double frameTimeMs = 0.0 ]) {
runApp(widget); runApp(widget);
scheduler.beginFrame(frameTimeMs); // TODO(ianh): https://github.com/flutter/engine/issues/1084 scheduler.beginFrame(frameTimeMs);
} }
void pumpFrameWithoutChange([ double frameTimeMs = 0.0 ]) { void pumpFrameWithoutChange([ double frameTimeMs = 0.0 ]) {
scheduler.beginFrame(frameTimeMs); // TODO(ianh): https://github.com/flutter/engine/issues/1084 scheduler.beginFrame(frameTimeMs);
} }
void reset() {
runApp(new Container());
scheduler.beginFrame(0.0);
}
List<Layer> _layers(Layer layer) { List<Layer> _layers(Layer layer) {
List<Layer> result = [layer]; List<Layer> result = [layer];
......
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
import 'package:sky/widgets.dart'; import 'package:sky/src/fn3.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'widget_tester.dart'; import '../fn3/widget_tester.dart';
const Size pageSize = const Size(800.0, 600.0); const Size pageSize = const Size(800.0, 600.0);
const List<int> pages = const <int>[0, 1, 2, 3, 4, 5]; const List<int> pages = const <int>[0, 1, 2, 3, 4, 5];
int currentPage = null; int currentPage = null;
bool itemsWrap = false; bool itemsWrap = false;
Widget buildPage(int page) { Widget buildPage(BuildContext context, int page) {
return new Container( return new Container(
key: new ValueKey<int>(page), key: new ValueKey<int>(page),
width: pageSize.width, width: pageSize.width,
...@@ -20,14 +20,14 @@ Widget buildPage(int page) { ...@@ -20,14 +20,14 @@ Widget buildPage(int page) {
Widget buildFrame() { Widget buildFrame() {
// The test framework forces the frame (and so the PageableList) // The test framework forces the frame (and so the PageableList)
// to be 800x600. The pageSize constant reflects as much. // to be 800x600. The pageSize constant reflects this.
return new PageableList<int>( return new PageableList<int>(
items: pages, items: pages,
itemBuilder: buildPage, itemBuilder: buildPage,
itemsWrap: itemsWrap, itemsWrap: itemsWrap,
itemExtent: pageSize.width, itemExtent: pageSize.width,
scrollDirection: ScrollDirection.horizontal, scrollDirection: ScrollDirection.horizontal,
pageChanged: (int page) { currentPage = page; } onPageChanged: (int page) { currentPage = page; }
); );
} }
...@@ -36,8 +36,8 @@ void page(WidgetTester tester, Offset offset) { ...@@ -36,8 +36,8 @@ void page(WidgetTester tester, Offset offset) {
new FakeAsync().run((async) { new FakeAsync().run((async) {
tester.scroll(tester.findText(itemText), offset); tester.scroll(tester.findText(itemText), offset);
// One frame to start the animation, a second to complete it. // One frame to start the animation, a second to complete it.
tester.pumpFrame(buildFrame); tester.pumpFrameWithoutChange();
tester.pumpFrame(buildFrame, 1000.0); tester.pumpFrameWithoutChange(1000.0);
async.elapse(new Duration(seconds: 1)); async.elapse(new Duration(seconds: 1));
}); });
} }
...@@ -57,42 +57,55 @@ void main() { ...@@ -57,42 +57,55 @@ void main() {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
currentPage = null; currentPage = null;
itemsWrap = false; itemsWrap = false;
tester.pumpFrame(buildFrame); tester.pumpFrame(buildFrame());
expect(currentPage, isNull); expect(currentPage, isNull);
pageLeft(tester); pageLeft(tester);
expect(currentPage, equals(1)); expect(currentPage, equals(1));
}); });
test('Underscroll (scroll right), return to page 0', () { test('Scroll right from page 1 to page 0', () {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
currentPage = null;
itemsWrap = false; itemsWrap = false;
tester.pumpFrame(buildFrame); tester.pumpFrame(buildFrame());
expect(currentPage, isNull); expect(currentPage, equals(1));
pageRight(tester); pageRight(tester);
expect(currentPage, equals(0)); expect(currentPage, equals(0));
}); });
// PageableList with itemsWrap: true test('Scroll right from page 0 does nothing (underscroll)', () {
WidgetTester tester = new WidgetTester();
itemsWrap = false;
tester.pumpFrame(buildFrame());
expect(currentPage, equals(0));
pageRight(tester);
expect(currentPage, equals(0));
});
itemsWrap = true; // PageableList with itemsWrap: true
test('Scroll left page 0 to page 1, itemsWrap: true', () { test('Scroll left page 0 to page 1, itemsWrap: true', () {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
tester.reset();
currentPage = null; currentPage = null;
itemsWrap = true; itemsWrap = true;
tester.pumpFrame(buildFrame); tester.pumpFrame(buildFrame());
expect(currentPage, isNull); expect(currentPage, isNull);
pageLeft(tester); pageLeft(tester);
expect(currentPage, equals(1)); expect(currentPage, equals(1));
}); });
test('Scroll right from page 0 to page 5, itemsWrap: true', () { test('Scroll right from page 1 to page 0, itemsWrap: true', () {
WidgetTester tester = new WidgetTester(); WidgetTester tester = new WidgetTester();
currentPage = null; tester.pumpFrame(buildFrame());
itemsWrap = true; expect(currentPage, equals(1));
tester.pumpFrame(buildFrame); pageRight(tester);
expect(currentPage, isNull); expect(currentPage, equals(0));
});
test('Scroll right from page 0 to page 5, itemsWrap: true (underscroll)', () {
WidgetTester tester = new WidgetTester();
tester.pumpFrame(buildFrame());
expect(currentPage, equals(0));
pageRight(tester); pageRight(tester);
expect(currentPage, equals(5)); expect(currentPage, equals(5));
}); });
......
import 'package:quiver/testing/async.dart';
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
import '../fn3/widget_tester.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
List<int> tapped = <int>[];
Widget buildFrame() {
return ;
}
void main() {
double t = 0.0;
WidgetTester tester = new WidgetTester();
test('Tap item after scroll - horizontal', () {
tester.pumpFrame(new Center(
child: new Container(
height: 50.0,
child: new ScrollableList<int>(
key: new GlobalKey(),
items: items,
itemBuilder: (BuildContext context, int item) {
return new Container(
key: new ValueKey<int>(item),
child: new GestureDetector(
onTap: () { tapped.add(item); },
child: new Text('$item')
)
);
},
itemExtent: 290.0,
scrollDirection: ScrollDirection.horizontal
)
)
), t);
tester.scroll(tester.findText('2'), const Offset(-280.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -280..10 = 0
// 10..300 = 1
// 300..590 = 2
// 590..880 = 3
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
expect(tapped, equals([]));
tester.tap(tester.findText('2'));
expect(tapped, equals([2]));
});
test('Tap item after scroll - vertical', () {
tester.pumpFrame(new Center(
child: new Container(
width: 50.0,
child: new ScrollableList<int>(
key: new GlobalKey(),
items: items,
itemBuilder: (BuildContext context, int item) {
return new Container(
key: new ValueKey<int>(item),
child: new GestureDetector(
onTap: () { tapped.add(item); },
child: new Text('$item')
)
);
},
itemExtent: 290.0,
scrollDirection: ScrollDirection.vertical
)
)
), t);
tester.scroll(tester.findText('1'), const Offset(0.0, -280.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 600px tall, and has the following items:
// -280..10 = 0
// 10..300 = 1
// 300..590 = 2
// 590..880 = 3
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
expect(tapped, equals([2]));
tester.tap(tester.findText('1'));
expect(tapped, equals([2, 1]));
tester.tap(tester.findText('3'));
expect(tapped, equals([2, 1])); // the center of the third item is off-screen so it shouldn't get hit
});
}
import 'package:quiver/testing/async.dart';
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
import '../fn3/widget_tester.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
Widget buildFrame() {
return new Center(
child: new Container(
height: 50.0,
child: new ScrollableList<int>(
items: items,
itemBuilder: (BuildContext context, int item) {
return new Container(
key: new ValueKey<int>(item),
child: new Text('$item')
);
},
itemExtent: 290.0,
scrollDirection: ScrollDirection.horizontal
)
)
);
}
void main() {
double t = 0.0;
WidgetTester tester = new WidgetTester();
tester.pumpFrame(buildFrame());
test('Drag to the left using item 1', () {
tester.pumpFrameWithoutChange(t += 1000.0);
tester.scroll(tester.findText('1'), const Offset(-300.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -10..280 = 1
// 280..570 = 2
// 570..860 = 3
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
});
test('Drag to the left using item 3', () {
// the center of item 3 is visible, so this works;
// if item 3 was a bit wider, such that it's center was past the 800px mark, this would fail,
// because it wouldn't be hit tested when scrolling from its center, as scroll() does.
tester.pumpFrameWithoutChange(t += 1000.0);
tester.scroll(tester.findText('3'), const Offset(-290.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -10..280 = 2
// 280..570 = 3
// 570..860 = 4
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
});
test('Drag up using item 3', () {
tester.pumpFrameWithoutChange(t += 1000.0);
tester.scroll(tester.findText('3'), const Offset(0.0, -290.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// unchanged
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
});
test('Drag to the left using item 3 again', () {
tester.pumpFrameWithoutChange(t += 1000.0);
tester.scroll(tester.findText('3'), const Offset(-290.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -10..280 = 3
// 280..570 = 4
// 570..860 = 5
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
});
test('Drag to the left using item 3 again again (past the end of the list)', () {
tester.pumpFrameWithoutChange(t += 1000.0);
// at this point we can drag 60 pixels further before we hit the friction zone
// then, every pixel we drag is equivalent to half a pixel of movement
// to move item 3 entirely off screen therefore takes:
// 60 + (290-60)*2 = 520 pixels
// plus a couple more to be sure
tester.scroll(tester.findText('3'), const Offset(-522.0, 0.0));
tester.pumpFrameWithoutChange(t += 0.0); // just after release
// screen is 800px wide, and has the following items:
// -11..279 = 4
// 279..569 = 5
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
tester.pumpFrameWithoutChange(t += 1000.0); // a second after release
// screen is 800px wide, and has the following items:
// -70..220 = 3
// 220..510 = 4
// 510..800 = 5
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNotNull);
});
test('Drag to the left using item 2 when the scroll offset is big', () {
tester.reset();
tester.pumpFrame(buildFrame(), t += 1000.0);
tester.scroll(tester.findText('2'), const Offset(-280.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -280..10 = 0
// 10..300 = 1
// 300..590 = 2
// 590..880 = 3
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
tester.pumpFrameWithoutChange(t += 1000.0);
tester.scroll(tester.findText('2'), const Offset(-290.0, 0.0));
tester.pumpFrameWithoutChange(t += 1000.0);
// screen is 800px wide, and has the following items:
// -280..10 = 1
// 10..300 = 2
// 300..590 = 3
// 590..880 = 4
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
});
}
import 'package:quiver/testing/async.dart';
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
import '../fn3/widget_tester.dart';
const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
Widget buildFrame() {
return new ScrollableList<int>(
items: items,
itemBuilder: (BuildContext context, int item) {
return new Container(
key: new ValueKey<int>(item),
child: new Text('$item')
);
},
itemExtent: 290.0,
scrollDirection: ScrollDirection.vertical
);
}
void main() {
WidgetTester tester = new WidgetTester();
tester.pumpFrame(buildFrame());
test('Drag up using item 1', () {
tester.pumpFrameWithoutChange();
tester.scroll(tester.findText('1'), const Offset(0.0, -300.0));
tester.pumpFrameWithoutChange();
// screen is 600px high, and has the following items:
// -10..280 = 1
// 280..570 = 2
// 570..860 = 3
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNull);
expect(tester.findText('5'), isNull);
});
test('Drag up using item 2', () {
tester.pumpFrameWithoutChange();
tester.scroll(tester.findText('2'), const Offset(0.0, -290.0));
tester.pumpFrameWithoutChange();
// screen is 600px high, and has the following items:
// -10..280 = 2
// 280..570 = 3
// 570..860 = 4
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
});
test('Drag to the left using item 3', () {
tester.pumpFrameWithoutChange();
tester.scroll(tester.findText('3'), const Offset(-300.0, 0.0));
tester.pumpFrameWithoutChange();
// nothing should have changed
expect(tester.findText('0'), isNull);
expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNotNull);
expect(tester.findText('3'), isNotNull);
expect(tester.findText('4'), isNotNull);
expect(tester.findText('5'), isNull);
});
}
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