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<double> - 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
This commit is contained in:
parent
1ce3146df2
commit
f808055756
@ -699,10 +699,10 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateScrollBehavior() {
|
void _updateScrollBehavior() {
|
||||||
scrollBehavior.updateExtents(
|
scrollTo(scrollBehavior.updateExtents(
|
||||||
containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
|
containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
|
||||||
contentExtent: _tabWidths.reduce((double sum, double width) => sum + width)
|
contentExtent: _tabWidths.reduce((double sum, double width) => sum + width)
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
|
void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
|
||||||
@ -713,11 +713,16 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleViewportSizeChanged(Size newSize) {
|
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
|
||||||
_viewportSize = newSize;
|
// 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();
|
_updateScrollBehavior();
|
||||||
if (config.isScrollable)
|
if (config.isScrollable)
|
||||||
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
|
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
|
||||||
|
return scrollOffsetToPixelDelta(scrollOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildContent(BuildContext context) {
|
Widget buildContent(BuildContext context) {
|
||||||
@ -772,13 +777,11 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (config.isScrollable) {
|
if (config.isScrollable) {
|
||||||
contents = new SizeObserver(
|
|
||||||
onSizeChanged: _handleViewportSizeChanged,
|
|
||||||
child: new Viewport(
|
child: new Viewport(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
|
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
|
||||||
|
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
|
||||||
child: contents
|
child: contents
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,20 @@ class ViewportDimensions {
|
|||||||
return paintOffset + (containerSize - contentSize);
|
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 {
|
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.
|
/// A render object that's bigger on the inside.
|
||||||
///
|
///
|
||||||
/// The child of a viewport can layout to a larger size than the viewport
|
/// 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,
|
Offset paintOffset: Offset.zero,
|
||||||
Axis scrollDirection: Axis.vertical,
|
Axis scrollDirection: Axis.vertical,
|
||||||
ViewportAnchor scrollAnchor: ViewportAnchor.start,
|
ViewportAnchor scrollAnchor: ViewportAnchor.start,
|
||||||
Painter overlayPainter
|
Painter overlayPainter,
|
||||||
|
this.onPaintOffsetUpdateNeeded
|
||||||
}) : super(paintOffset, scrollDirection, scrollAnchor, overlayPainter) {
|
}) : super(paintOffset, scrollDirection, scrollAnchor, overlayPainter) {
|
||||||
this.child = child;
|
this.child = child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called during [layout] to report the dimensions of the viewport
|
||||||
|
/// and its child.
|
||||||
|
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
|
||||||
|
|
||||||
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
|
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
|
||||||
BoxConstraints innerConstraints;
|
BoxConstraints innerConstraints;
|
||||||
switch (scrollDirection) {
|
switch (scrollDirection) {
|
||||||
@ -228,6 +249,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
|
|||||||
// parent was baseline-aligned, which makes no sense.
|
// parent was baseline-aligned, which makes no sense.
|
||||||
|
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
|
ViewportDimensions oldDimensions = dimensions;
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
|
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
|
||||||
size = constraints.constrain(child.size);
|
size = constraints.constrain(child.size);
|
||||||
@ -238,6 +260,9 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
|
|||||||
performResize();
|
performResize();
|
||||||
dimensions = new ViewportDimensions(containerSize: size);
|
dimensions = new ViewportDimensions(containerSize: size);
|
||||||
}
|
}
|
||||||
|
if (onPaintOffsetUpdateNeeded != null && dimensions != oldDimensions)
|
||||||
|
paintOffset = onPaintOffsetUpdateNeeded(dimensions);
|
||||||
|
assert(paintOffset != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldClipAtPaintOffset(Offset paintOffset) {
|
bool _shouldClipAtPaintOffset(Offset paintOffset) {
|
||||||
|
@ -37,6 +37,8 @@ abstract class BindingBase {
|
|||||||
void initInstances() {
|
void initInstances() {
|
||||||
assert(() { _debugInitialized = true; return true; });
|
assert(() { _debugInitialized = true; return true; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String toString() => '<$runtimeType>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// A replacement for shell.connectToService. Implementations should return true
|
// A replacement for shell.connectToService. Implementations should return true
|
||||||
|
@ -43,7 +43,9 @@ export 'package:flutter/rendering.dart' show
|
|||||||
RelativeRect,
|
RelativeRect,
|
||||||
ShaderCallback,
|
ShaderCallback,
|
||||||
ValueChanged,
|
ValueChanged,
|
||||||
ViewportAnchor;
|
ViewportAnchor,
|
||||||
|
ViewportDimensions,
|
||||||
|
ViewportDimensionsChangeCallback;
|
||||||
|
|
||||||
// PAINTING NODES
|
// PAINTING NODES
|
||||||
|
|
||||||
@ -777,6 +779,7 @@ class Viewport extends OneChildRenderObjectWidget {
|
|||||||
this.scrollDirection: Axis.vertical,
|
this.scrollDirection: Axis.vertical,
|
||||||
this.scrollAnchor: ViewportAnchor.start,
|
this.scrollAnchor: ViewportAnchor.start,
|
||||||
this.overlayPainter,
|
this.overlayPainter,
|
||||||
|
this.onPaintOffsetUpdateNeeded,
|
||||||
Widget child
|
Widget child
|
||||||
}) : super(key: key, child: child) {
|
}) : super(key: key, child: child) {
|
||||||
assert(scrollDirection != null);
|
assert(scrollDirection != null);
|
||||||
@ -802,11 +805,14 @@ class Viewport extends OneChildRenderObjectWidget {
|
|||||||
/// Often used to paint scroll bars.
|
/// Often used to paint scroll bars.
|
||||||
final Painter overlayPainter;
|
final Painter overlayPainter;
|
||||||
|
|
||||||
|
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
|
||||||
|
|
||||||
RenderViewport createRenderObject() {
|
RenderViewport createRenderObject() {
|
||||||
return new RenderViewport(
|
return new RenderViewport(
|
||||||
paintOffset: paintOffset,
|
paintOffset: paintOffset,
|
||||||
scrollDirection: scrollDirection,
|
scrollDirection: scrollDirection,
|
||||||
scrollAnchor: scrollAnchor,
|
scrollAnchor: scrollAnchor,
|
||||||
|
onPaintOffsetUpdateNeeded: onPaintOffsetUpdateNeeded,
|
||||||
overlayPainter: overlayPainter
|
overlayPainter: overlayPainter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -817,6 +823,7 @@ class Viewport extends OneChildRenderObjectWidget {
|
|||||||
..scrollDirection = scrollDirection
|
..scrollDirection = scrollDirection
|
||||||
..scrollAnchor = scrollAnchor
|
..scrollAnchor = scrollAnchor
|
||||||
..paintOffset = paintOffset
|
..paintOffset = paintOffset
|
||||||
|
..onPaintOffsetUpdateNeeded = onPaintOffsetUpdateNeeded
|
||||||
..overlayPainter = overlayPainter;
|
..overlayPainter = overlayPainter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -280,29 +280,29 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
|
|||||||
void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
|
void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
|
||||||
assert(() {
|
assert(() {
|
||||||
RenderObject renderObject = context.findRenderObject();
|
RenderObject renderObject = context.findRenderObject();
|
||||||
assert(renderObject is RenderPointerListener);
|
|
||||||
RenderPointerListener listener = renderObject;
|
|
||||||
RenderBox descendant = listener.child;
|
|
||||||
if (!config.excludeFromSemantics) {
|
if (!config.excludeFromSemantics) {
|
||||||
assert(descendant is RenderSemanticsGestureHandler);
|
assert(renderObject is RenderSemanticsGestureHandler);
|
||||||
RenderSemanticsGestureHandler semanticsGestureHandler = descendant;
|
RenderSemanticsGestureHandler semanticsGestureHandler = renderObject;
|
||||||
descendant = semanticsGestureHandler.child;
|
renderObject = semanticsGestureHandler.child;
|
||||||
}
|
}
|
||||||
assert(descendant != null);
|
assert(renderObject is RenderPointerListener);
|
||||||
if (!descendant.debugDoingThisLayout) {
|
RenderPointerListener pointerListener = renderObject;
|
||||||
|
renderObject = pointerListener.child;
|
||||||
|
if (!renderObject.debugDoingThisLayout) {
|
||||||
throw new WidgetError(
|
throw new WidgetError(
|
||||||
'replaceGestureRecognizers() can only be called during the layout phase of the GestureDetector\'s nearest descendant RenderObjectWidget.\n'
|
'replaceGestureRecognizers() can only be called during the layout phase of the GestureDetector\'s nearest descendant RenderObjectWidget.\n'
|
||||||
'In this particular case, that is:\n'
|
'In this particular case, that is:\n'
|
||||||
' $descendant'
|
' $renderObject'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
_syncAll(gestures);
|
_syncAll(gestures);
|
||||||
if (!config.excludeFromSemantics) {
|
if (!config.excludeFromSemantics) {
|
||||||
RenderPointerListener listener = context.findRenderObject();
|
RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
|
||||||
RenderSemanticsGestureHandler semanticsGestureHandler = listener.child;
|
context.visitChildElements((RenderObjectElement element) {
|
||||||
context.visitChildElements((RenderObjectElement element) => element.widget.updateRenderObject(semanticsGestureHandler, null));
|
element.widget.updateRenderObject(semanticsGestureHandler, null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import 'framework.dart';
|
|||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
|
|
||||||
typedef Widget IndexedBuilder(BuildContext context, int index); // return null if index is greater than index of last entry
|
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 InvalidatorCallback(Iterable<int> indices);
|
||||||
typedef void InvalidatorAvailableCallback(InvalidatorCallback invalidator);
|
typedef void InvalidatorAvailableCallback(InvalidatorCallback invalidator);
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ class MixedViewport extends RenderObjectWidget {
|
|||||||
this.direction: Axis.vertical,
|
this.direction: Axis.vertical,
|
||||||
this.builder,
|
this.builder,
|
||||||
this.token,
|
this.token,
|
||||||
this.onExtentsUpdate,
|
this.onExtentChanged,
|
||||||
this.onInvalidatorAvailable
|
this.onInvalidatorAvailable
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ class MixedViewport extends RenderObjectWidget {
|
|||||||
final Axis direction;
|
final Axis direction;
|
||||||
final IndexedBuilder builder;
|
final IndexedBuilder builder;
|
||||||
final Object token; // change this if the list changed (i.e. there are added, removed, or resorted items)
|
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
|
final InvalidatorAvailableCallback onInvalidatorAvailable; // call the callback this gives to invalidate sizes
|
||||||
|
|
||||||
_MixedViewportElement createElement() => new _MixedViewportElement(this);
|
_MixedViewportElement createElement() => new _MixedViewportElement(this);
|
||||||
@ -108,8 +107,8 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
|
|||||||
/// The constraints for which the current offsets are valid.
|
/// The constraints for which the current offsets are valid.
|
||||||
BoxConstraints _lastLayoutConstraints;
|
BoxConstraints _lastLayoutConstraints;
|
||||||
|
|
||||||
/// The last value that was sent to onExtentsUpdate.
|
/// The last value that was sent to onExtentChanged.
|
||||||
double _lastReportedExtents;
|
double _lastReportedExtent;
|
||||||
|
|
||||||
RenderBlockViewport get renderObject => super.renderObject;
|
RenderBlockViewport get renderObject => super.renderObject;
|
||||||
|
|
||||||
@ -227,11 +226,11 @@ class _MixedViewportElement extends RenderObjectElement<MixedViewport> {
|
|||||||
BuildableElement.lockState(() {
|
BuildableElement.lockState(() {
|
||||||
_doLayout(constraints);
|
_doLayout(constraints);
|
||||||
}, building: true);
|
}, building: true);
|
||||||
if (widget.onExtentsUpdate != null) {
|
if (widget.onExtentChanged != null) {
|
||||||
final double newExtents = _didReachLastChild ? _childOffsets.last : null;
|
final double newExtent = _didReachLastChild ? _childOffsets.last : null;
|
||||||
if (newExtents != _lastReportedExtents) {
|
if (newExtent != _lastReportedExtent) {
|
||||||
_lastReportedExtents = newExtents;
|
_lastReportedExtent = newExtent;
|
||||||
widget.onExtentsUpdate(_lastReportedExtents);
|
widget.onExtentChanged(_lastReportedExtent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,15 @@ abstract class ScrollBehavior<T, U> {
|
|||||||
|
|
||||||
/// Whether this scroll behavior currently permits scrolling
|
/// Whether this scroll behavior currently permits scrolling
|
||||||
bool get isScrollable => true;
|
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.
|
/// 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.
|
/// The maximum value the scroll offset can obtain.
|
||||||
double get maxScrollOffset;
|
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.
|
/// 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;
|
return _scrollBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
GestureDragStartCallback _getDragStartHandler(Axis direction) {
|
Map<Type, GestureRecognizerFactory> buildGestureDetectors() {
|
||||||
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
|
if (scrollBehavior.isScrollable) {
|
||||||
return null;
|
switch (config.scrollDirection) {
|
||||||
return _handleDragStart;
|
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) {
|
final GlobalKey _gestureDetectorKey = new GlobalKey();
|
||||||
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
|
|
||||||
return null;
|
|
||||||
return _handleDragUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
GestureDragEndCallback _getDragEndHandler(Axis direction) {
|
void updateGestureDetector() {
|
||||||
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
|
_gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
|
||||||
return null;
|
|
||||||
return _handleDragEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new GestureDetector(
|
return new RawGestureDetector(
|
||||||
onVerticalDragStart: _getDragStartHandler(Axis.vertical),
|
key: _gestureDetectorKey,
|
||||||
onVerticalDragUpdate: _getDragUpdateHandler(Axis.vertical),
|
gestures: buildGestureDetectors(),
|
||||||
onVerticalDragEnd: _getDragEndHandler(Axis.vertical),
|
|
||||||
onHorizontalDragStart: _getDragStartHandler(Axis.horizontal),
|
|
||||||
onHorizontalDragUpdate: _getDragUpdateHandler(Axis.horizontal),
|
|
||||||
onHorizontalDragEnd: _getDragEndHandler(Axis.horizontal),
|
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: new Listener(
|
child: new Listener(
|
||||||
child: buildContent(context),
|
child: buildContent(context),
|
||||||
@ -321,7 +331,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
|||||||
if (endScrollOffset.isNaN)
|
if (endScrollOffset.isNaN)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
final double snappedScrollOffset = snapScrollOffset(endScrollOffset);
|
final double snappedScrollOffset = snapScrollOffset(endScrollOffset); // invokes the config.snapOffsetCallback callback
|
||||||
if (!_scrollOffsetIsInBounds(snappedScrollOffset))
|
if (!_scrollOffsetIsInBounds(snappedScrollOffset))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -443,7 +453,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragStart(_) {
|
void _handleDragStart(_) {
|
||||||
scheduleMicrotask(dispatchOnScrollStart);
|
dispatchOnScrollStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragUpdate(double delta) {
|
void _handleDragUpdate(double delta) {
|
||||||
@ -503,18 +513,19 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
|
|||||||
|
|
||||||
double _viewportSize = 0.0;
|
double _viewportSize = 0.0;
|
||||||
double _childSize = 0.0;
|
double _childSize = 0.0;
|
||||||
void _handleViewportSizeChanged(Size newSize) {
|
|
||||||
_viewportSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
|
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
|
||||||
setState(() {
|
// 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();
|
_updateScrollBehavior();
|
||||||
});
|
updateGestureDetector();
|
||||||
}
|
return scrollOffsetToPixelDelta(scrollOffset);
|
||||||
void _handleChildSizeChanged(Size newSize) {
|
|
||||||
_childSize = config.scrollDirection == Axis.vertical ? newSize.height : newSize.width;
|
|
||||||
setState(() {
|
|
||||||
_updateScrollBehavior();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateScrollBehavior() {
|
void _updateScrollBehavior() {
|
||||||
// if you don't call this from build(), you must call it from setState().
|
// if you don't call this from build(), you must call it from setState().
|
||||||
scrollTo(scrollBehavior.updateExtents(
|
scrollTo(scrollBehavior.updateExtents(
|
||||||
@ -525,17 +536,12 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildContent(BuildContext context) {
|
Widget buildContent(BuildContext context) {
|
||||||
return new SizeObserver(
|
return new Viewport(
|
||||||
onSizeChanged: _handleViewportSizeChanged,
|
|
||||||
child: new Viewport(
|
|
||||||
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
|
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
|
||||||
scrollDirection: config.scrollDirection,
|
scrollDirection: config.scrollDirection,
|
||||||
scrollAnchor: config.scrollAnchor,
|
scrollAnchor: config.scrollAnchor,
|
||||||
child: new SizeObserver(
|
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
|
||||||
onSizeChanged: _handleChildSizeChanged,
|
|
||||||
child: config.child
|
child: config.child
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -690,11 +696,11 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleExtentsUpdate(double newExtents) {
|
void _handleExtentChanged(double newExtent) {
|
||||||
double newScrollOffset;
|
double newScrollOffset;
|
||||||
setState(() {
|
setState(() {
|
||||||
newScrollOffset = scrollBehavior.updateExtents(
|
newScrollOffset = scrollBehavior.updateExtents(
|
||||||
contentExtent: newExtents ?? double.INFINITY,
|
contentExtent: newExtent ?? double.INFINITY,
|
||||||
scrollOffset: scrollOffset
|
scrollOffset: scrollOffset
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -712,7 +718,7 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
|
|||||||
builder: config.builder,
|
builder: config.builder,
|
||||||
token: config.token,
|
token: config.token,
|
||||||
onInvalidatorAvailable: config.onInvalidatorAvailable,
|
onInvalidatorAvailable: config.onInvalidatorAvailable,
|
||||||
onExtentsUpdate: _handleExtentsUpdate
|
onExtentChanged: _handleExtentChanged
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,16 +50,18 @@ void main() {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
tester.pump(); // for SizeObservers
|
|
||||||
|
|
||||||
Point middleOfContainer = tester.getCenter(tester.findText('Hello'));
|
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));
|
Point target = tester.getCenter(tester.findElementByKey(blockKey));
|
||||||
TestGesture gesture = tester.startGesture(target);
|
TestGesture gesture = tester.startGesture(target);
|
||||||
gesture.moveBy(const Offset(0.0, -10.0));
|
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();
|
gesture.up();
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,8 @@ class Instrumentation {
|
|||||||
|
|
||||||
final WidgetFlutterBinding binding;
|
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!
|
// TODO(ianh): This should not be O(N) hidden behind a getter!
|
||||||
List<Layer> _layers(Layer layer) {
|
List<Layer> _layers(Layer layer) {
|
||||||
List<Layer> result = <Layer>[layer];
|
List<Layer> result = <Layer>[layer];
|
||||||
@ -32,9 +34,9 @@ class Instrumentation {
|
|||||||
}
|
}
|
||||||
return result;
|
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 walkElements(ElementVisitor visitor) {
|
||||||
void walk(Element element) {
|
void walk(Element element) {
|
||||||
visitor(element);
|
visitor(element);
|
||||||
@ -43,6 +45,9 @@ class Instrumentation {
|
|||||||
binding.renderViewElement.visitChildren(walk);
|
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)) {
|
Element findElement(bool predicate(Element element)) {
|
||||||
try {
|
try {
|
||||||
walkElements((Element element) {
|
walkElements((Element element) {
|
||||||
@ -55,16 +60,24 @@ class Instrumentation {
|
|||||||
return null;
|
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) {
|
Element findElementByKey(Key key) {
|
||||||
return findElement((Element element) => element.widget.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) {
|
Element findText(String text) {
|
||||||
return findElement((Element element) {
|
return findElement((Element element) {
|
||||||
return element.widget is Text && element.widget.data == text;
|
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) {
|
State findStateOfType(Type type) {
|
||||||
StatefulComponentElement element = findElement((Element element) {
|
StatefulComponentElement element = findElement((Element element) {
|
||||||
return element is StatefulComponentElement && element.state.runtimeType == type;
|
return element is StatefulComponentElement && element.state.runtimeType == type;
|
||||||
@ -72,6 +85,10 @@ class Instrumentation {
|
|||||||
return element?.state;
|
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) {
|
State findStateByConfig(Widget config) {
|
||||||
StatefulComponentElement element = findElement((Element element) {
|
StatefulComponentElement element = findElement((Element element) {
|
||||||
return element is StatefulComponentElement && element.state.config == config;
|
return element is StatefulComponentElement && element.state.config == config;
|
||||||
@ -79,26 +96,36 @@ class Instrumentation {
|
|||||||
return element?.state;
|
return element?.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the point at the center of the given element.
|
||||||
Point getCenter(Element element) {
|
Point getCenter(Element element) {
|
||||||
return _getElementPoint(element, (Size size) => size.center(Point.origin));
|
return _getElementPoint(element, (Size size) => size.center(Point.origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the point at the top left of the given element.
|
||||||
Point getTopLeft(Element element) {
|
Point getTopLeft(Element element) {
|
||||||
return _getElementPoint(element, (_) => Point.origin);
|
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) {
|
Point getTopRight(Element element) {
|
||||||
return _getElementPoint(element, (Size size) => size.topRight(Point.origin));
|
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) {
|
Point getBottomLeft(Element element) {
|
||||||
return _getElementPoint(element, (Size size) => size.bottomLeft(Point.origin));
|
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) {
|
Point getBottomRight(Element element) {
|
||||||
return _getElementPoint(element, (Size size) => size.bottomRight(Point.origin));
|
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) {
|
Size getSize(Element element) {
|
||||||
assert(element != null);
|
assert(element != null);
|
||||||
RenderBox box = element.renderObject as RenderBox;
|
RenderBox box = element.renderObject as RenderBox;
|
||||||
@ -113,22 +140,34 @@ class Instrumentation {
|
|||||||
return box.localToGlobal(sizeToPoint(box.size));
|
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 }) {
|
void tap(Element element, { int pointer: 1 }) {
|
||||||
tapAt(getCenter(element), pointer: pointer);
|
tapAt(getCenter(element), pointer: pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatch a pointer down / pointer up sequence at the given
|
||||||
|
/// location.
|
||||||
void tapAt(Point location, { int pointer: 1 }) {
|
void tapAt(Point location, { int pointer: 1 }) {
|
||||||
HitTestResult result = _hitTest(location);
|
HitTestResult result = _hitTest(location);
|
||||||
TestPointer p = new TestPointer(pointer);
|
TestPointer p = new TestPointer(pointer);
|
||||||
_dispatchEvent(p.down(location), result);
|
dispatchEvent(p.down(location), result);
|
||||||
_dispatchEvent(p.up(), 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 }) {
|
void fling(Element element, Offset offset, double velocity, { int pointer: 1 }) {
|
||||||
flingFrom(getCenter(element), offset, velocity, pointer: pointer);
|
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 }) {
|
void flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) {
|
||||||
assert(offset.distance > 0.0);
|
assert(offset.distance > 0.0);
|
||||||
assert(velocity != 0.0); // velocity is pixels/second
|
assert(velocity != 0.0); // velocity is pixels/second
|
||||||
@ -137,53 +176,65 @@ class Instrumentation {
|
|||||||
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
||||||
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
|
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
|
||||||
double timeStamp = 0.0;
|
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++) {
|
for(int i = 0; i < kMoveCount; i++) {
|
||||||
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
|
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;
|
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 }) {
|
void scroll(Element element, Offset offset, { int pointer: 1 }) {
|
||||||
scrollAt(getCenter(element), offset, pointer: pointer);
|
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 }) {
|
void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) {
|
||||||
Point endLocation = startLocation + offset;
|
Point endLocation = startLocation + offset;
|
||||||
TestPointer p = new TestPointer(pointer);
|
TestPointer p = new TestPointer(pointer);
|
||||||
// Events for the entire press-drag-release gesture are dispatched
|
// Events for the entire press-drag-release gesture are dispatched
|
||||||
// to the widgets "hit" by the pointer down event.
|
// to the widgets "hit" by the pointer down event.
|
||||||
HitTestResult result = _hitTest(startLocation);
|
HitTestResult result = _hitTest(startLocation);
|
||||||
_dispatchEvent(p.down(startLocation), result);
|
dispatchEvent(p.down(startLocation), result);
|
||||||
_dispatchEvent(p.move(endLocation), result);
|
dispatchEvent(p.move(endLocation), result);
|
||||||
_dispatchEvent(p.up(), 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 }) {
|
TestGesture startGesture(Point downLocation, { int pointer: 1 }) {
|
||||||
TestPointer p = new TestPointer(pointer);
|
TestPointer p = new TestPointer(pointer);
|
||||||
HitTestResult result = _hitTest(downLocation);
|
HitTestResult result = _hitTest(downLocation);
|
||||||
_dispatchEvent(p.down(downLocation), result);
|
dispatchEvent(p.down(downLocation), result);
|
||||||
return new TestGesture._(this, result, p);
|
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 _hitTest(Point location) {
|
||||||
HitTestResult result = new HitTestResult();
|
HitTestResult result = new HitTestResult();
|
||||||
binding.hitTest(result, location);
|
binding.hitTest(result, location);
|
||||||
return result;
|
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);
|
binding.dispatchEvent(event, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class for performing gestures in tests. To create a
|
||||||
|
/// [TestGesture], call [WidgetTester.startGesture].
|
||||||
class TestGesture {
|
class TestGesture {
|
||||||
TestGesture._(this._target, this._result, this.pointer);
|
TestGesture._(this._target, this._result, this.pointer);
|
||||||
|
|
||||||
@ -192,25 +243,31 @@ class TestGesture {
|
|||||||
final TestPointer pointer;
|
final TestPointer pointer;
|
||||||
bool _isDown = true;
|
bool _isDown = true;
|
||||||
|
|
||||||
|
/// Send a move event moving the pointer to the given location.
|
||||||
void moveTo(Point location) {
|
void moveTo(Point location) {
|
||||||
assert(_isDown);
|
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) {
|
void moveBy(Offset offset) {
|
||||||
assert(_isDown);
|
assert(_isDown);
|
||||||
moveTo(pointer.location + offset);
|
moveTo(pointer.location + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// End the gesture by releasing the pointer.
|
||||||
void up() {
|
void up() {
|
||||||
assert(_isDown);
|
assert(_isDown);
|
||||||
_isDown = false;
|
_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() {
|
void cancel() {
|
||||||
assert(_isDown);
|
assert(_isDown);
|
||||||
_isDown = false;
|
_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/testing/async.dart';
|
||||||
import 'package:quiver/time.dart';
|
import 'package:quiver/time.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@ -57,6 +58,11 @@ class WidgetTester extends Instrumentation {
|
|||||||
);
|
);
|
||||||
async.flushMicrotasks();
|
async.flushMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void dispatchEvent(PointerEvent event, HitTestResult result) {
|
||||||
|
super.dispatchEvent(event, result);
|
||||||
|
async.flushMicrotasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWidgets(callback(WidgetTester tester)) {
|
void testWidgets(callback(WidgetTester tester)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user