From f808055756d3b7946be90a4aec74e59eb40eddba Mon Sep 17 00:00:00 2001 From: Hixie Date: Wed, 24 Feb 2016 09:17:21 -0800 Subject: [PATCH] Remove size observers from scrollables. Also: - add operator==/hashCode/toString to ViewportDimensions - add toString to BindingBase - add toString and debugFillDescription to ScrollBehavior - fix a bug in the RawGestureDetectorState's replaceGestureRecognizers - rename MixedViewport's onExtentsUpdate to onExtentChanged - replace ExtentsUpdateCallback with ValueChanged - remove a microtask for dispatching scroll start, since it did not appear to have any purpose - added dartdocs to Instrumentation until I understood it - made all event dispatch in Instrumentation drain microtasks --- packages/flutter/lib/src/material/tabs.dart | 25 +++-- .../flutter/lib/src/rendering/viewport.dart | 27 ++++- .../flutter/lib/src/services/binding.dart | 2 + packages/flutter/lib/src/widgets/basic.dart | 9 +- .../lib/src/widgets/gesture_detector.dart | 24 ++--- .../lib/src/widgets/mixed_viewport.dart | 19 ++-- .../lib/src/widgets/scroll_behavior.dart | 16 +++ .../flutter/lib/src/widgets/scrollable.dart | 100 ++++++++++-------- packages/flutter/test/widget/block_test.dart | 8 +- .../flutter_test/lib/src/instrumentation.dart | 99 +++++++++++++---- .../flutter_test/lib/src/widget_tester.dart | 6 ++ 11 files changed, 229 insertions(+), 106 deletions(-) diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 7faf8179a43..b734bb744de 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -699,10 +699,10 @@ class _TabBarState extends ScrollableState> 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 tabWidths) { @@ -713,11 +713,16 @@ class _TabBarState extends ScrollableState> 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 extends ScrollableState> 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 ); } diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index e1fee2ae61f..1f3319400d6 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -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) { diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 2f11163fecc..51f71aa141a 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -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 diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index cbd126a4ee5..1e44f7c4a3c 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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; } } diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 6f91f6a3bee..756d4858cb5 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -280,29 +280,29 @@ class RawGestureDetectorState extends State { void replaceGestureRecognizers(Map 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); + }); } } diff --git a/packages/flutter/lib/src/widgets/mixed_viewport.dart b/packages/flutter/lib/src/widgets/mixed_viewport.dart index b855d3a9cd3..89f74ceda19 100644 --- a/packages/flutter/lib/src/widgets/mixed_viewport.dart +++ b/packages/flutter/lib/src/widgets/mixed_viewport.dart @@ -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 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 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 { /// 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 { 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); } } } diff --git a/packages/flutter/lib/src/widgets/scroll_behavior.dart b/packages/flutter/lib/src/widgets/scroll_behavior.dart index cd42bcb89bc..7a599dd33c8 100644 --- a/packages/flutter/lib/src/widgets/scroll_behavior.dart +++ b/packages/flutter/lib/src/widgets/scroll_behavior.dart @@ -35,6 +35,15 @@ abstract class ScrollBehavior { /// Whether this scroll behavior currently permits scrolling bool get isScrollable => true; + + String toString() { + List description = []; + debugFillDescription(description); + return '$runtimeType(${description.join("; ")})'; + } + void debugFillDescription(List 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 { /// The maximum value the scroll offset can obtain. double get maxScrollOffset; + + void debugFillDescription(List 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. diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 585a8580f1a..ab5764e6987 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -237,32 +237,42 @@ abstract class ScrollableState extends State { return _scrollBehavior; } - GestureDragStartCallback _getDragStartHandler(Axis direction) { - if (config.scrollDirection != direction || !scrollBehavior.isScrollable) - return null; - return _handleDragStart; + Map buildGestureDetectors() { + if (scrollBehavior.isScrollable) { + switch (config.scrollDirection) { + case Axis.vertical: + return { + VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) { + return (recognizer ??= new VerticalDragGestureRecognizer()) + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd; + } + }; + case Axis.horizontal: + return { + HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) { + return (recognizer ??= new HorizontalDragGestureRecognizer()) + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd; + } + }; + } + } + return const {}; } - 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 extends State { 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 extends State { } void _handleDragStart(_) { - scheduleMicrotask(dispatchOnScrollStart); + dispatchOnScrollStart(); } void _handleDragUpdate(double delta) { @@ -503,18 +513,19 @@ class _ScrollableViewportState extends ScrollableState { 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 { } 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 get layers => _layers(binding.renderView.layer); // TODO(ianh): This should not be O(N) hidden behind a getter! List _layers(Layer layer) { List result = [layer]; @@ -32,9 +34,9 @@ class Instrumentation { } return result; } - List 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); } } diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index ba68ac9e8cd..de59ac3a1e6 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -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)) {