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() {
|
||||
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)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user