Commit 135a38d6 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #2165 from Hixie/size-obs-3

SizeObserver crusade: ScrollableViewport and tabs
parents 1ce3146d f8080557
......@@ -699,10 +699,10 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
}
void _updateScrollBehavior() {
scrollBehavior.updateExtents(
scrollTo(scrollBehavior.updateExtents(
containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
contentExtent: _tabWidths.reduce((double sum, double width) => sum + width)
);
));
}
void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
......@@ -713,11 +713,16 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
});
}
void _handleViewportSizeChanged(Size newSize) {
_viewportSize = newSize;
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_viewportSize = dimensions.containerSize;
_updateScrollBehavior();
if (config.isScrollable)
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
return scrollOffsetToPixelDelta(scrollOffset);
}
Widget buildContent(BuildContext context) {
......@@ -772,13 +777,11 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
);
if (config.isScrollable) {
contents = new SizeObserver(
onSizeChanged: _handleViewportSizeChanged,
child: new Viewport(
scrollDirection: Axis.horizontal,
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
child: contents
)
child: new Viewport(
scrollDirection: Axis.horizontal,
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
child: contents
);
}
......
......@@ -39,6 +39,20 @@ class ViewportDimensions {
return paintOffset + (containerSize - contentSize);
}
}
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! ViewportDimensions)
return false;
final ViewportDimensions typedOther = other;
return contentSize == typedOther.contentSize &&
containerSize == typedOther.containerSize;
}
int get hashCode => hashValues(contentSize, containerSize);
String toString() => 'ViewportDimensions(container: $containerSize, content: $contentSize)';
}
abstract class HasScrollDirection {
......@@ -163,6 +177,8 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection {
}
typedef Offset ViewportDimensionsChangeCallback(ViewportDimensions dimensions);
/// A render object that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size than the viewport
......@@ -176,11 +192,16 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
Painter overlayPainter
Painter overlayPainter,
this.onPaintOffsetUpdateNeeded
}) : super(paintOffset, scrollDirection, scrollAnchor, overlayPainter) {
this.child = child;
}
/// Called during [layout] to report the dimensions of the viewport
/// and its child.
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
BoxConstraints innerConstraints;
switch (scrollDirection) {
......@@ -228,6 +249,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
// parent was baseline-aligned, which makes no sense.
void performLayout() {
ViewportDimensions oldDimensions = dimensions;
if (child != null) {
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child.size);
......@@ -238,6 +260,9 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
performResize();
dimensions = new ViewportDimensions(containerSize: size);
}
if (onPaintOffsetUpdateNeeded != null && dimensions != oldDimensions)
paintOffset = onPaintOffsetUpdateNeeded(dimensions);
assert(paintOffset != null);
}
bool _shouldClipAtPaintOffset(Offset paintOffset) {
......
......@@ -37,6 +37,8 @@ abstract class BindingBase {
void initInstances() {
assert(() { _debugInitialized = true; return true; });
}
String toString() => '<$runtimeType>';
}
// A replacement for shell.connectToService. Implementations should return true
......
......@@ -43,7 +43,9 @@ export 'package:flutter/rendering.dart' show
RelativeRect,
ShaderCallback,
ValueChanged,
ViewportAnchor;
ViewportAnchor,
ViewportDimensions,
ViewportDimensionsChangeCallback;
// PAINTING NODES
......@@ -777,6 +779,7 @@ class Viewport extends OneChildRenderObjectWidget {
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.overlayPainter,
this.onPaintOffsetUpdateNeeded,
Widget child
}) : super(key: key, child: child) {
assert(scrollDirection != null);
......@@ -802,11 +805,14 @@ class Viewport extends OneChildRenderObjectWidget {
/// Often used to paint scroll bars.
final Painter overlayPainter;
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
RenderViewport createRenderObject() {
return new RenderViewport(
paintOffset: paintOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onPaintOffsetUpdateNeeded: onPaintOffsetUpdateNeeded,
overlayPainter: overlayPainter
);
}
......@@ -817,6 +823,7 @@ class Viewport extends OneChildRenderObjectWidget {
..scrollDirection = scrollDirection
..scrollAnchor = scrollAnchor
..paintOffset = paintOffset
..onPaintOffsetUpdateNeeded = onPaintOffsetUpdateNeeded
..overlayPainter = overlayPainter;
}
}
......
......@@ -280,29 +280,29 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
assert(() {
RenderObject renderObject = context.findRenderObject();
assert(renderObject is RenderPointerListener);
RenderPointerListener listener = renderObject;
RenderBox descendant = listener.child;
if (!config.excludeFromSemantics) {
assert(descendant is RenderSemanticsGestureHandler);
RenderSemanticsGestureHandler semanticsGestureHandler = descendant;
descendant = semanticsGestureHandler.child;
assert(renderObject is RenderSemanticsGestureHandler);
RenderSemanticsGestureHandler semanticsGestureHandler = renderObject;
renderObject = semanticsGestureHandler.child;
}
assert(descendant != null);
if (!descendant.debugDoingThisLayout) {
assert(renderObject is RenderPointerListener);
RenderPointerListener pointerListener = renderObject;
renderObject = pointerListener.child;
if (!renderObject.debugDoingThisLayout) {
throw new WidgetError(
'replaceGestureRecognizers() can only be called during the layout phase of the GestureDetector\'s nearest descendant RenderObjectWidget.\n'
'In this particular case, that is:\n'
' $descendant'
' $renderObject'
);
}
return true;
});
_syncAll(gestures);
if (!config.excludeFromSemantics) {
RenderPointerListener listener = context.findRenderObject();
RenderSemanticsGestureHandler semanticsGestureHandler = listener.child;
context.visitChildElements((RenderObjectElement element) => element.widget.updateRenderObject(semanticsGestureHandler, null));
RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
context.visitChildElements((RenderObjectElement element) {
element.widget.updateRenderObject(semanticsGestureHandler, null);
});
}
}
......
......@@ -10,7 +10,6 @@ import 'framework.dart';
import 'basic.dart';
typedef Widget IndexedBuilder(BuildContext context, int index); // return null if index is greater than index of last entry
typedef void ExtentsUpdateCallback(double newExtents);
typedef void InvalidatorCallback(Iterable<int> indices);
typedef void InvalidatorAvailableCallback(InvalidatorCallback invalidator);
......@@ -23,7 +22,7 @@ class MixedViewport extends RenderObjectWidget {
this.direction: Axis.vertical,
this.builder,
this.token,
this.onExtentsUpdate,
this.onExtentChanged,
this.onInvalidatorAvailable
}) : super(key: key);
......@@ -31,7 +30,7 @@ class MixedViewport extends RenderObjectWidget {
final Axis direction;
final IndexedBuilder builder;
final Object token; // change this if the list changed (i.e. there are added, removed, or resorted items)
final ExtentsUpdateCallback onExtentsUpdate;
final ValueChanged<double> onExtentChanged;
final InvalidatorAvailableCallback onInvalidatorAvailable; // call the callback this gives to invalidate sizes
_MixedViewportElement createElement() => new _MixedViewportElement(this);
......@@ -108,8 +107,8 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
/// The constraints for which the current offsets are valid.
BoxConstraints _lastLayoutConstraints;
/// The last value that was sent to onExtentsUpdate.
double _lastReportedExtents;
/// The last value that was sent to onExtentChanged.
double _lastReportedExtent;
RenderBlockViewport get renderObject => super.renderObject;
......@@ -227,11 +226,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
BuildableElement.lockState(() {
_doLayout(constraints);
}, building: true);
if (widget.onExtentsUpdate != null) {
final double newExtents = _didReachLastChild ? _childOffsets.last : null;
if (newExtents != _lastReportedExtents) {
_lastReportedExtents = newExtents;
widget.onExtentsUpdate(_lastReportedExtents);
if (widget.onExtentChanged != null) {
final double newExtent = _didReachLastChild ? _childOffsets.last : null;
if (newExtent != _lastReportedExtent) {
_lastReportedExtent = newExtent;
widget.onExtentChanged(_lastReportedExtent);
}
}
}
......
......@@ -35,6 +35,15 @@ abstract class ScrollBehavior<T, U> {
/// Whether this scroll behavior currently permits scrolling
bool get isScrollable => true;
String toString() {
List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType(${description.join("; ")})';
}
void debugFillDescription(List<String> description) {
description.add(isScrollable ? 'scrollable' : 'not scrollable');
}
}
/// A scroll behavior for a scrollable widget with linear extent (i.e.
......@@ -74,6 +83,13 @@ abstract class ExtentScrollBehavior extends ScrollBehavior<double, double> {
/// The maximum value the scroll offset can obtain.
double get maxScrollOffset;
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('content: ${contentExtent.toStringAsFixed(1)}');
description.add('container: ${contentExtent.toStringAsFixed(1)}');
description.add('range: ${minScrollOffset?.toStringAsFixed(1)} .. ${maxScrollOffset?.toStringAsFixed(1)}');
}
}
/// A scroll behavior that prevents the user from exceeding scroll bounds.
......
......@@ -237,32 +237,42 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return _scrollBehavior;
}
GestureDragStartCallback _getDragStartHandler(Axis direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragStart;
Map<Type, GestureRecognizerFactory> buildGestureDetectors() {
if (scrollBehavior.isScrollable) {
switch (config.scrollDirection) {
case Axis.vertical:
return <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) {
return (recognizer ??= new VerticalDragGestureRecognizer())
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
};
case Axis.horizontal:
return <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) {
return (recognizer ??= new HorizontalDragGestureRecognizer())
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
};
}
}
return const <Type, GestureRecognizerFactory>{};
}
GestureDragUpdateCallback _getDragUpdateHandler(Axis direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragUpdate;
}
final GlobalKey _gestureDetectorKey = new GlobalKey();
GestureDragEndCallback _getDragEndHandler(Axis direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragEnd;
void updateGestureDetector() {
_gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
}
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragStart: _getDragStartHandler(Axis.vertical),
onVerticalDragUpdate: _getDragUpdateHandler(Axis.vertical),
onVerticalDragEnd: _getDragEndHandler(Axis.vertical),
onHorizontalDragStart: _getDragStartHandler(Axis.horizontal),
onHorizontalDragUpdate: _getDragUpdateHandler(Axis.horizontal),
onHorizontalDragEnd: _getDragEndHandler(Axis.horizontal),
return new RawGestureDetector(
key: _gestureDetectorKey,
gestures: buildGestureDetectors(),
behavior: HitTestBehavior.opaque,
child: new Listener(
child: buildContent(context),
......@@ -321,7 +331,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
if (endScrollOffset.isNaN)
return null;
final double snappedScrollOffset = snapScrollOffset(endScrollOffset);
final double snappedScrollOffset = snapScrollOffset(endScrollOffset); // invokes the config.snapOffsetCallback callback
if (!_scrollOffsetIsInBounds(snappedScrollOffset))
return null;
......@@ -443,7 +453,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
}
void _handleDragStart(_) {
scheduleMicrotask(dispatchOnScrollStart);
dispatchOnScrollStart();
}
void _handleDragUpdate(double delta) {
......@@ -503,18 +513,19 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
double _viewportSize = 0.0;
double _childSize = 0.0;
void _handleViewportSizeChanged(Size newSize) {
_viewportSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
setState(() {
_updateScrollBehavior();
});
}
void _handleChildSizeChanged(Size newSize) {
_childSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
setState(() {
_updateScrollBehavior();
});
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
_childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
_updateScrollBehavior();
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
scrollTo(scrollBehavior.updateExtents(
......@@ -525,17 +536,12 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
}
Widget buildContent(BuildContext context) {
return new SizeObserver(
onSizeChanged: _handleViewportSizeChanged,
child: new Viewport(
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
child: new SizeObserver(
onSizeChanged: _handleChildSizeChanged,
child: config.child
)
)
return new Viewport(
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
child: config.child
);
}
}
......@@ -690,11 +696,11 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
}
}
void _handleExtentsUpdate(double newExtents) {
void _handleExtentChanged(double newExtent) {
double newScrollOffset;
setState(() {
newScrollOffset = scrollBehavior.updateExtents(
contentExtent: newExtents ?? double.INFINITY,
contentExtent: newExtent ?? double.INFINITY,
scrollOffset: scrollOffset
);
});
......@@ -712,7 +718,7 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
builder: config.builder,
token: config.token,
onInvalidatorAvailable: config.onInvalidatorAvailable,
onExtentsUpdate: _handleExtentsUpdate
onExtentChanged: _handleExtentChanged
)
);
}
......
......@@ -50,16 +50,18 @@ void main() {
]
)
);
tester.pump(); // for SizeObservers
Point middleOfContainer = tester.getCenter(tester.findText('Hello'));
expect(middleOfContainer.x, equals(400.0));
expect(middleOfContainer.y, equals(1000.0));
Point target = tester.getCenter(tester.findElementByKey(blockKey));
TestGesture gesture = tester.startGesture(target);
gesture.moveBy(const Offset(0.0, -10.0));
tester.pump(const Duration(milliseconds: 1));
tester.pump(); // redo layout
expect(tester.getCenter(tester.findText('Hello')) == middleOfContainer, isFalse);
expect(tester.getCenter(tester.findText('Hello')), isNot(equals(middleOfContainer)));
gesture.up();
});
......
......@@ -19,6 +19,8 @@ class Instrumentation {
final WidgetFlutterBinding binding;
/// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _layers(binding.renderView.layer);
// TODO(ianh): This should not be O(N) hidden behind a getter!
List<Layer> _layers(Layer layer) {
List<Layer> result = <Layer>[layer];
......@@ -32,9 +34,9 @@ class Instrumentation {
}
return result;
}
List<Layer> get layers => _layers(binding.renderView.layer);
/// Walks all the elements in the tree, in depth-first pre-order,
/// calling the given function for each one.
void walkElements(ElementVisitor visitor) {
void walk(Element element) {
visitor(element);
......@@ -43,6 +45,9 @@ class Instrumentation {
binding.renderViewElement.visitChildren(walk);
}
/// Returns the first element that for which the given predicate
/// function returns true, if any, or null if the predicate function
/// never returns true.
Element findElement(bool predicate(Element element)) {
try {
walkElements((Element element) {
......@@ -55,16 +60,24 @@ class Instrumentation {
return null;
}
/// Returns the first element that corresponds to a widget with the
/// given [Key], or null if there is no such element.
Element findElementByKey(Key key) {
return findElement((Element element) => element.widget.key == key);
}
/// Returns the first element that corresponds to a [Text] widget
/// whose data is the given string, or null if there is no such
/// element.
Element findText(String text) {
return findElement((Element element) {
return element.widget is Text && element.widget.data == text;
});
}
/// Returns the [State] object of the first element whose state has
/// the given [runtimeType], if any. Returns null if there is no
/// matching element.
State findStateOfType(Type type) {
StatefulComponentElement element = findElement((Element element) {
return element is StatefulComponentElement && element.state.runtimeType == type;
......@@ -72,6 +85,10 @@ class Instrumentation {
return element?.state;
}
/// Returns the [State] object of the first element whose
/// configuration is the given widget, if any. Returns null if the
/// given configuration is not that of a stateful widget or if there
/// is no matching element.
State findStateByConfig(Widget config) {
StatefulComponentElement element = findElement((Element element) {
return element is StatefulComponentElement && element.state.config == config;
......@@ -79,26 +96,36 @@ class Instrumentation {
return element?.state;
}
/// Returns the point at the center of the given element.
Point getCenter(Element element) {
return _getElementPoint(element, (Size size) => size.center(Point.origin));
}
/// Returns the point at the top left of the given element.
Point getTopLeft(Element element) {
return _getElementPoint(element, (_) => Point.origin);
}
/// Returns the point at the top right of the given element. This
/// point is not inside the object's hit test area.
Point getTopRight(Element element) {
return _getElementPoint(element, (Size size) => size.topRight(Point.origin));
}
/// Returns the point at the bottom left of the given element. This
/// point is not inside the object's hit test area.
Point getBottomLeft(Element element) {
return _getElementPoint(element, (Size size) => size.bottomLeft(Point.origin));
}
/// Returns the point at the bottom right of the given element. This
/// point is not inside the object's hit test area.
Point getBottomRight(Element element) {
return _getElementPoint(element, (Size size) => size.bottomRight(Point.origin));
}
/// Returns the size of the given element. This is only valid once
/// the element's render object has been laid out at least once.
Size getSize(Element element) {
assert(element != null);
RenderBox box = element.renderObject as RenderBox;
......@@ -113,22 +140,34 @@ class Instrumentation {
return box.localToGlobal(sizeToPoint(box.size));
}
/// Dispatch a pointer down / pointer up sequence at the center of
/// the given element, assuming it is exposed. If the center of the
/// element is not exposed, this might send events to another
/// object.
void tap(Element element, { int pointer: 1 }) {
tapAt(getCenter(element), pointer: pointer);
}
/// Dispatch a pointer down / pointer up sequence at the given
/// location.
void tapAt(Point location, { int pointer: 1 }) {
HitTestResult result = _hitTest(location);
TestPointer p = new TestPointer(pointer);
_dispatchEvent(p.down(location), result);
_dispatchEvent(p.up(), result);
dispatchEvent(p.down(location), result);
dispatchEvent(p.up(), result);
}
/// Attempts a fling gesture starting from the center of the given
/// element, moving the given distance, reaching the given velocity.
///
/// If the middle of the element is not exposed, this might send
/// events to another object.
void fling(Element element, Offset offset, double velocity, { int pointer: 1 }) {
flingFrom(getCenter(element), offset, velocity, pointer: pointer);
}
/// Attempts a fling gesture starting from the given location,
/// moving the given distance, reaching the given velocity.
void flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) {
assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second
......@@ -137,53 +176,65 @@ class Instrumentation {
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0;
_dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
for(int i = 0; i < kMoveCount; i++) {
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
_dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta;
}
_dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
}
/// Attempts to drag the given element by the given offset, by
/// starting a drag in the middle of the element.
///
/// If the middle of the element is not exposed, this might send
/// events to another object.
void scroll(Element element, Offset offset, { int pointer: 1 }) {
scrollAt(getCenter(element), offset, pointer: pointer);
}
/// Attempts a drag gesture consisting of a pointer down, a move by
/// the given offset, and a pointer up.
void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) {
Point endLocation = startLocation + offset;
TestPointer p = new TestPointer(pointer);
// Events for the entire press-drag-release gesture are dispatched
// to the widgets "hit" by the pointer down event.
HitTestResult result = _hitTest(startLocation);
_dispatchEvent(p.down(startLocation), result);
_dispatchEvent(p.move(endLocation), result);
_dispatchEvent(p.up(), result);
dispatchEvent(p.down(startLocation), result);
dispatchEvent(p.move(endLocation), result);
dispatchEvent(p.up(), result);
}
/// Begins a gesture at a particular point, and returns the
/// [TestGesture] object which you can use to continue the gesture.
TestGesture startGesture(Point downLocation, { int pointer: 1 }) {
TestPointer p = new TestPointer(pointer);
HitTestResult result = _hitTest(downLocation);
_dispatchEvent(p.down(downLocation), result);
dispatchEvent(p.down(downLocation), result);
return new TestGesture._(this, result, p);
}
@Deprecated('soon. Use startGesture instead.')
void dispatchEvent(PointerEvent event, Point location) {
_dispatchEvent(event, _hitTest(location));
}
HitTestResult _hitTest(Point location) {
HitTestResult result = new HitTestResult();
binding.hitTest(result, location);
return result;
}
void _dispatchEvent(PointerEvent event, HitTestResult result) {
/// Sends a [PointerEvent] at a particular [HitTestResult].
///
/// Generally speaking, it is preferred to use one of the more
/// semantically meaningful ways to dispatch events in tests, in
/// particular: [tap], [tapAt], [fling], [flingFrom], [scroll],
/// [scrollAt], or [startGesture].
void dispatchEvent(PointerEvent event, HitTestResult result) {
binding.dispatchEvent(event, result);
}
}
/// A class for performing gestures in tests. To create a
/// [TestGesture], call [WidgetTester.startGesture].
class TestGesture {
TestGesture._(this._target, this._result, this.pointer);
......@@ -192,25 +243,31 @@ class TestGesture {
final TestPointer pointer;
bool _isDown = true;
/// Send a move event moving the pointer to the given location.
void moveTo(Point location) {
assert(_isDown);
_target._dispatchEvent(pointer.move(location), _result);
_target.dispatchEvent(pointer.move(location), _result);
}
/// Send a move event moving the pointer by the given offset.
void moveBy(Offset offset) {
assert(_isDown);
moveTo(pointer.location + offset);
}
/// End the gesture by releasing the pointer.
void up() {
assert(_isDown);
_isDown = false;
_target._dispatchEvent(pointer.up(), _result);
_target.dispatchEvent(pointer.up(), _result);
}
/// End the gesture by canceling the pointer (as would happen if the
/// system showed a modal dialog on top of the Flutter application,
/// for instance).
void cancel() {
assert(_isDown);
_isDown = false;
_target._dispatchEvent(pointer.cancel(), _result);
_target.dispatchEvent(pointer.cancel(), _result);
}
}
......@@ -6,6 +6,7 @@ import 'dart:ui' as ui show window;
import 'package:quiver/testing/async.dart';
import 'package:quiver/time.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
......@@ -57,6 +58,11 @@ class WidgetTester extends Instrumentation {
);
async.flushMicrotasks();
}
void dispatchEvent(PointerEvent event, HitTestResult result) {
super.dispatchEvent(event, result);
async.flushMicrotasks();
}
}
void testWidgets(callback(WidgetTester tester)) {
......
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