Commit 8e279f32 authored by Hixie's avatar Hixie

Provide a Draggable that starts on drag

This lets it cooperate with other gestures like tap.

The way I implemented this was to refactor the entire Draggable gesture
logic to use a new kind of gesture detector called
MultiDragGestureRecognizer. It works a bit like
MultiTapGestureRecognizer but for drags.

Also some tweaks to the velocity estimator.
parent 88cefe12
......@@ -39,20 +39,29 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
}
}
class Dot extends StatelessComponent {
Dot({ Key key, this.color, this.size, this.child }) : super(key: key);
class Dot extends StatefulComponent {
Dot({ Key key, this.color, this.size, this.child, this.tappable: false }) : super(key: key);
final Color color;
final double size;
final Widget child;
final bool tappable;
DotState createState() => new DotState();
}
class DotState extends State<Dot> {
int taps = 0;
Widget build(BuildContext context) {
return new Container(
width: size,
height: size,
decoration: new BoxDecoration(
backgroundColor: color,
shape: BoxShape.circle
),
child: child
return new GestureDetector(
onTap: config.tappable ? () { setState(() { taps += 1; }); } : null,
child: new Container(
width: config.size,
height: config.size,
decoration: new BoxDecoration(
backgroundColor: config.color,
border: new Border.all(color: const Color(0xFF000000), width: taps.toDouble()),
shape: BoxShape.circle
),
child: config.child
)
);
}
}
......@@ -155,10 +164,14 @@ class DashOutlineCirclePainter extends CustomPainter {
class MovableBall extends StatelessComponent {
MovableBall(this.position, this.ballPosition, this.callback);
final int position;
final int ballPosition;
final ValueChanged<int> callback;
static final GlobalKey kBallKey = new GlobalKey();
static const double kBallSize = 50.0;
Widget build(BuildContext context) {
Widget ball = new DefaultTextStyle(
style: Theme.of(context).text.body1.copyWith(
......@@ -166,8 +179,10 @@ class MovableBall extends StatelessComponent {
color: Colors.white
),
child: new Dot(
key: kBallKey,
color: Colors.blue[700],
size: kBallSize,
tappable: true,
child: new Center(child: new Text('BALL'))
)
);
......
......@@ -14,6 +14,7 @@ export 'src/gestures/events.dart';
export 'src/gestures/hit_test.dart';
export 'src/gestures/long_press.dart';
export 'src/gestures/lsq_solver.dart';
export 'src/gestures/multidrag.dart';
export 'src/gestures/multitap.dart';
export 'src/gestures/pointer_router.dart';
export 'src/gestures/recognizer.dart';
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' show Point, Offset;
import 'arena.dart';
import 'constants.dart';
import 'events.dart';
import 'pointer_router.dart';
import 'recognizer.dart';
import 'velocity_tracker.dart';
typedef Drag GestureMultiDragStartCallback(Point position);
class Drag {
void move(Offset offset) { }
void end(Offset velocity) { }
void cancel() { }
}
abstract class MultiDragPointerState {
MultiDragPointerState(this.initialPosition);
final Point initialPosition;
final VelocityTracker _velocityTracker = new VelocityTracker();
Drag _client;
Offset get pendingDelta => _pendingDelta;
Offset _pendingDelta = Offset.zero;
GestureArenaEntry _arenaEntry;
void _setArenaEntry(GestureArenaEntry entry) {
assert(_arenaEntry == null);
assert(pendingDelta != null);
assert(_client == null);
_arenaEntry = entry;
}
void resolve(GestureDisposition disposition) {
_arenaEntry.resolve(disposition);
}
void _move(PointerMoveEvent event) {
assert(_arenaEntry != null);
_velocityTracker.addPosition(event.timeStamp, event.position);
if (_client != null) {
assert(pendingDelta == null);
_client.move(event.delta);
} else {
assert(pendingDelta != null);
_pendingDelta += event.delta;
checkForResolutionAfterMove();
}
return null;
}
/// Override this to call resolve() if the drag should be accepted or rejected.
/// This is called when a pointer movement is received, but only if the gesture
/// has not yet been resolved.
void checkForResolutionAfterMove() { }
/// Called when the gesture was accepted.
void accepted(Drag client) {
assert(_arenaEntry != null);
assert(_client == null);
_client = client;
_client.move(pendingDelta);
_pendingDelta = null;
}
/// Called when the gesture was rejected.
void rejected() {
assert(_arenaEntry != null);
assert(_client == null);
assert(pendingDelta != null);
_pendingDelta = null;
_arenaEntry = null;
}
void _up() {
assert(_arenaEntry != null);
if (_client != null) {
assert(pendingDelta == null);
_client.end(_velocityTracker.getVelocity());
_client = null;
} else {
assert(pendingDelta != null);
_pendingDelta = null;
}
_arenaEntry = null;
}
void _cancel() {
assert(_arenaEntry != null);
if (_client != null) {
assert(pendingDelta == null);
_client.cancel();
_client = null;
} else {
assert(pendingDelta != null);
_pendingDelta = null;
}
_arenaEntry = null;
}
void dispose() { }
}
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
MultiDragGestureRecognizer({
PointerRouter pointerRouter,
GestureArena gestureArena,
this.onStart
}) : _pointerRouter = pointerRouter, _gestureArena = gestureArena {
assert(pointerRouter != null);
assert(gestureArena != null);
}
final PointerRouter _pointerRouter;
final GestureArena _gestureArena;
GestureMultiDragStartCallback onStart;
Map<int, T> _pointers = <int, T>{};
void addPointer(PointerDownEvent event) {
assert(_pointers != null);
assert(event.pointer != null);
assert(event.position != null);
assert(!_pointers.containsKey(event.pointer));
T state = createNewPointerState(event);
_pointers[event.pointer] = state;
_pointerRouter.addRoute(event.pointer, handleEvent);
state._setArenaEntry(_gestureArena.add(event.pointer, this));
}
T createNewPointerState(PointerDownEvent event);
void handleEvent(PointerEvent event) {
assert(_pointers != null);
assert(event.pointer != null);
assert(event.timeStamp != null);
assert(event.position != null);
assert(_pointers.containsKey(event.pointer));
T state = _pointers[event.pointer];
if (event is PointerMoveEvent) {
state._move(event);
} else if (event is PointerUpEvent) {
assert(event.delta == Offset.zero);
state._up();
_removeState(event.pointer);
} else if (event is PointerCancelEvent) {
assert(event.delta == Offset.zero);
state._cancel();
_removeState(event.pointer);
} else if (event is! PointerDownEvent) {
// we get the PointerDownEvent that resulted in our addPointer gettig called since we
// add ourselves to the pointer router then (before the pointer router has heard of
// the event).
assert(false);
}
}
void acceptGesture(int pointer) {
assert(_pointers != null);
T state = _pointers[pointer];
assert(state != null);
Drag drag;
if (onStart != null)
drag = onStart(state.initialPosition);
if (drag != null) {
state.accepted(drag);
} else {
_removeState(pointer);
}
}
void rejectGesture(int pointer) {
assert(_pointers != null);
if (_pointers.containsKey(pointer)) {
T state = _pointers[pointer];
assert(state != null);
state.rejected();
_removeState(pointer);
} // else we already preemptively forgot about it (e.g. we got an up event)
}
void _removeState(int pointer) {
assert(_pointers != null);
assert(_pointers.containsKey(pointer));
_pointerRouter.removeRoute(pointer, handleEvent);
_pointers[pointer].dispose();
_pointers.remove(pointer);
}
void dispose() {
for (int pointer in _pointers.keys)
_removeState(pointer);
_pointers = null;
super.dispose();
}
}
class _ImmediatePointerState extends MultiDragPointerState {
_ImmediatePointerState(Point initialPosition) : super(initialPosition);
void checkForResolutionAfterMove() {
assert(pendingDelta != null);
if (pendingDelta.distance > kTouchSlop)
resolve(GestureDisposition.accepted);
}
}
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
ImmediateMultiDragGestureRecognizer({
PointerRouter pointerRouter,
GestureArena gestureArena,
GestureMultiDragStartCallback onStart
}) : super(pointerRouter: pointerRouter, gestureArena: gestureArena, onStart: onStart);
_ImmediatePointerState createNewPointerState(PointerDownEvent event) {
return new _ImmediatePointerState(event.position);
}
}
class _DelayedPointerState extends MultiDragPointerState {
_DelayedPointerState(Point initialPosition, Duration delay) : super(initialPosition) {
assert(delay != null);
_timer = new Timer(delay, _delayPassed);
}
Timer _timer;
void _delayPassed() {
assert(_timer != null);
assert(pendingDelta != null);
assert(pendingDelta.distance <= kTouchSlop);
resolve(GestureDisposition.accepted);
_timer = null;
}
void accepted(Drag client) {
_timer?.cancel();
_timer = null;
super.accepted(client);
}
void checkForResolutionAfterMove() {
assert(_timer != null);
assert(pendingDelta != null);
if (pendingDelta.distance > kTouchSlop)
resolve(GestureDisposition.rejected);
}
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
}
}
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
DelayedMultiDragGestureRecognizer({
PointerRouter pointerRouter,
GestureArena gestureArena,
GestureMultiDragStartCallback onStart,
Duration delay: kLongPressTimeout
}) : _delay = delay,
super(pointerRouter: pointerRouter, gestureArena: gestureArena, onStart: onStart) {
assert(delay != null);
}
Duration get delay => _delay;
Duration _delay;
void set delay(Duration value) {
assert(value != null);
_delay = value;
}
_DelayedPointerState createNewPointerState(PointerDownEvent event) {
return new _DelayedPointerState(event.position, _delay);
}
}
......@@ -290,7 +290,7 @@ class _TapGesture extends _TapTracker {
if (_wonArena)
reject();
else
entry.resolve(GestureDisposition.rejected);
entry.resolve(GestureDisposition.rejected); // eventually calls reject()
}
void _check() {
......
......@@ -13,23 +13,45 @@ import 'pointer_router.dart';
export 'pointer_router.dart' show PointerRouter;
/// The base class that all GestureRecognizers should inherit from.
///
/// Provides a basic API that can be used by classes that work with
/// gesture recognizers but don't care about the specific details of
/// the gestures recognizers themselves.
abstract class GestureRecognizer extends GestureArenaMember {
/// Calls this with the pointerdown event of each pointer that should be
/// considered for this gesture.
/// Registers a new pointer that might be relevant to this gesture
/// detector.
///
/// It's the GestureRecognizer's responsibility to then add itself to the
/// global pointer router to receive subsequent events for this pointer.
/// The owner of this gesture recognizer calls addPointer() with the
/// PointerDownEvent of each pointer that should be considered for
/// this gesture.
///
/// It's the GestureRecognizer's responsibility to then add itself
/// to the global pointer router (see [PointerRouter]) to receive
/// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArena]) to track
/// that pointer.
void addPointer(PointerDownEvent event);
/// Releases any resources used by the object.
///
/// This method is called when the object is no longer needed (e.g. a gesture
/// recogniser is being unregistered from a [GestureDetector]).
/// This method is called by the owner of this gesture recognizer
/// when the object is no longer needed (e.g. when a gesture
/// recogniser is being unregistered from a [GestureDetector], the
/// GestureDetector widget calls this method).
void dispose() { }
}
/// Base class for gesture recognizers that can only recognize one
/// gesture at a time. For example, a single [TapGestureRecognizer]
/// can never recognize two taps happening simultaneously, even if
/// multiple pointers are placed on the same widget.
///
/// This is in contrast to, for instance, [MultiTapGestureRecognizer],
/// which manages each pointer independently and can consider multiple
/// simultaneous touches to each result in a separate tap.
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
OneSequenceGestureRecognizer({
PointerRouter router,
......
......@@ -8,25 +8,27 @@ import 'lsq_solver.dart';
export 'dart:ui' show Point, Offset;
class _Estimator {
int degree;
Duration time;
List<double> xCoefficients;
List<double> yCoefficients;
double confidence;
class _Estimate {
const _Estimate({ this.xCoefficients, this.yCoefficients, this.time, this.degree, this.confidence });
final List<double> xCoefficients;
final List<double> yCoefficients;
final Duration time;
final int degree;
final double confidence;
String toString() {
return 'Estimator(degree: $degree, '
'time: $time, '
'confidence: $confidence, '
'xCoefficients: $xCoefficients, '
'yCoefficients: $yCoefficients)';
return 'Estimate(xCoefficients: $xCoefficients, '
'yCoefficients: $yCoefficients, '
'time: $time, '
'degree: $degree, '
'confidence: $confidence)';
}
}
abstract class _VelocityTrackerStrategy {
void addMovement(Duration timeStamp, Point position);
bool getEstimator(_Estimator estimator);
_Estimate getEstimate();
void clear();
}
......@@ -80,7 +82,7 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
movement.position = position;
}
bool getEstimator(_Estimator estimator) {
_Estimate getEstimate() {
// Iterate over movement samples in reverse time order and collect samples.
List<double> x = new List<double>();
List<double> y = new List<double>();
......@@ -107,7 +109,7 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
} while (m < kHistorySize);
if (m == 0) // because we broke out of the loop above after age > kHorizonMilliseconds
return false; // no data
return null; // no data
// Calculate a least squares polynomial fit.
int n = degree;
......@@ -121,24 +123,26 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
LeastSquaresSolver ySolver = new LeastSquaresSolver(time, y, w);
PolynomialFit yFit = ySolver.solve(n);
if (yFit != null) {
estimator.xCoefficients = xFit.coefficients;
estimator.yCoefficients = yFit.coefficients;
estimator.time = newestMovement.eventTime;
estimator.degree = n;
estimator.confidence = xFit.confidence * yFit.confidence;
return true;
return new _Estimate(
xCoefficients: xFit.coefficients,
yCoefficients: yFit.coefficients,
time: newestMovement.eventTime,
degree: n,
confidence: xFit.confidence * yFit.confidence
);
}
}
}
// No velocity data available for this pointer, but we do have its current
// position.
estimator.xCoefficients = <double>[ x[0] ];
estimator.yCoefficients = <double>[ y[0] ];
estimator.time = newestMovement.eventTime;
estimator.degree = 0;
estimator.confidence = 1.0;
return true;
return new _Estimate(
xCoefficients: <double>[ x[0] ],
yCoefficients: <double>[ y[0] ],
time: newestMovement.eventTime,
degree: 0,
confidence: 1.0
);
}
void clear() {
......@@ -204,27 +208,52 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
}
/// Computes a pointer velocity based on data from PointerMove events.
///
/// The input data is provided by calling addPosition(). Adding data
/// is cheap.
///
/// To obtain a velocity, call getVelocity(). This will compute the
/// velocity based on the data added so far. Only call this when you
/// need to use the velocity, as it is comparatively expensive.
///
/// The quality of the velocity estimation will be better if more data
/// points have been received.
class VelocityTracker {
static const int kAssumePointerMoveStoppedTimeMs = 40;
/// The maximum length of time between two move events to allow
/// before assuming the pointer stopped.
static const Duration kAssumePointerMoveStoppedTime = const Duration(milliseconds: 40);
VelocityTracker() : _strategy = _createStrategy();
Duration _lastTimeStamp = const Duration();
_VelocityTrackerStrategy _strategy;
/// Add a given position corresponding to a specific time.
///
/// If [kAssumePointerMoveStoppedTime] has elapsed since the last
/// call, then earlier data will be discarded.
void addPosition(Duration timeStamp, Point position) {
if ((timeStamp - _lastTimeStamp).inMilliseconds >= kAssumePointerMoveStoppedTimeMs)
if (timeStamp - _lastTimeStamp >= kAssumePointerMoveStoppedTime)
_strategy.clear();
_lastTimeStamp = timeStamp;
_strategy.addMovement(timeStamp, position);
}
/// Computes the velocity of the pointer at the time of the last
/// provided data point.
///
/// This can be expensive. Only call this when you need the velocity.
///
/// getVelocity() will return null if no estimate is available or if
/// the velocity is zero.
Offset getVelocity() {
_Estimator estimator = new _Estimator();
if (_strategy.getEstimator(estimator) && estimator.degree >= 1) {
_Estimate estimate = _strategy.getEstimate();
if (estimate != null && estimate.degree >= 1) {
return new Offset( // convert from pixels/ms to pixels/s
estimator.xCoefficients[1] * 1000,
estimator.yCoefficients[1] * 1000
estimate.xCoefficients[1] * 1000,
estimate.yCoefficients[1] * 1000
);
}
return null;
......
......@@ -16,7 +16,6 @@ import 'overlay.dart';
typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
typedef void DragStartCallback(Point position, int pointer);
/// Where the [Draggable] should be anchored during a drag.
enum DragAnchor {
......@@ -81,11 +80,9 @@ abstract class DraggableBase<T> extends StatefulComponent {
/// dragged at a time.
final int maxSimultaneousDrags;
/// Should return a GestureRecognizer instance that is configured to call the starter
/// argument when the drag is to begin. The arena for the pointer must not yet have
/// resolved at the time that the callback is invoked, because the draggable itself
/// is going to attempt to win the pointer's arena in that case.
GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter);
/// Should return a new MultiDragGestureRecognizer instance
/// constructed with the given arguments.
MultiDragGestureRecognizer createRecognizer(PointerRouter router, GestureArena arena, GestureMultiDragStartCallback starter);
_DraggableState<T> createState() => new _DraggableState<T>();
}
......@@ -112,11 +109,11 @@ class Draggable<T> extends DraggableBase<T> {
maxSimultaneousDrags: maxSimultaneousDrags
);
GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter) {
return new MultiTapGestureRecognizer(
router: router,
gestureArena: Gesturer.instance.gestureArena,
onTapDown: starter
MultiDragGestureRecognizer createRecognizer(PointerRouter router, GestureArena arena, GestureMultiDragStartCallback starter) {
return new ImmediateMultiDragGestureRecognizer(
pointerRouter: router,
gestureArena: arena,
onStart: starter
);
}
}
......@@ -143,50 +140,44 @@ class LongPressDraggable<T> extends DraggableBase<T> {
maxSimultaneousDrags: maxSimultaneousDrags
);
GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter) {
return new MultiTapGestureRecognizer(
router: router,
gestureArena: Gesturer.instance.gestureArena,
longTapDelay: kLongPressTimeout,
onLongTapDown: (Point position, int pointer) {
userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
starter(position, pointer);
MultiDragGestureRecognizer createRecognizer(PointerRouter router, GestureArena arena, GestureMultiDragStartCallback starter) {
return new DelayedMultiDragGestureRecognizer(
pointerRouter: router,
gestureArena: arena,
delay: kLongPressTimeout,
onStart: (Point position) {
Drag result = starter(position);
if (result != null)
userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
return result;
}
);
}
}
class _DraggableState<T> extends State<DraggableBase<T>> implements GestureArenaMember {
PointerRouter get router => Gesturer.instance.pointerRouter;
class _DraggableState<T> extends State<DraggableBase<T>> {
void initState() {
super.initState();
_recognizer = config.createRecognizer(router, _startDrag);
_recognizer = config.createRecognizer(
Gesturer.instance.pointerRouter,
Gesturer.instance.gestureArena,
_startDrag
);
}
GestureRecognizer _recognizer;
Map<int, GestureArenaEntry> _activePointers = <int, GestureArenaEntry>{};
int _activeCount = 0;
void _routePointer(PointerEvent event) {
_activePointers[event.pointer] = Gesturer.instance.gestureArena.add(event.pointer, this);
if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
return;
_recognizer.addPointer(event);
}
void acceptGesture(int pointer) {
_activePointers.remove(pointer);
}
void rejectGesture(int pointer) {
_activePointers.remove(pointer);
}
void _startDrag(Point position, int pointer) {
_DragAvatar _startDrag(Point position) {
if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
return;
assert(_activePointers.containsKey(pointer));
_activePointers[pointer].resolve(GestureDisposition.accepted);
return null;
Point dragStartPoint;
switch (config.dragAnchor) {
case DragAnchor.child:
......@@ -200,9 +191,7 @@ class _DraggableState<T> extends State<DraggableBase<T>> implements GestureArena
setState(() {
_activeCount += 1;
});
new _DragAvatar<T>(
pointer: pointer,
router: router,
return new _DragAvatar<T>(
overlay: Overlay.of(context),
data: config.data,
initialPosition: position,
......@@ -305,10 +294,8 @@ enum _DragEndKind { dropped, canceled }
// overlay goes away, or maybe even if the Draggable that created goes away.
// This will probably need to be changed once we have more experience with using
// this widget.
class _DragAvatar<T> {
class _DragAvatar<T> extends Drag {
_DragAvatar({
this.pointer,
this.router,
OverlayState overlay,
this.data,
Point initialPosition,
......@@ -317,19 +304,15 @@ class _DragAvatar<T> {
this.feedbackOffset: Offset.zero,
this.onDragEnd
}) {
assert(pointer != null);
assert(router != null);
assert(overlay != null);
assert(dragStartPoint != null);
assert(feedbackOffset != null);
router.addRoute(pointer, handleEvent);
_entry = new OverlayEntry(builder: _build);
overlay.insert(_entry);
_position = initialPosition;
update(initialPosition);
}
final int pointer;
final PointerRouter router;
final T data;
final Point dragStartPoint;
final Widget feedback;
......@@ -338,18 +321,20 @@ class _DragAvatar<T> {
_DragTargetState _activeTarget;
bool _activeTargetWillAcceptDrop = false;
Point _position;
Offset _lastOffset;
OverlayEntry _entry;
void handleEvent(PointerEvent event) {
if (event is PointerUpEvent) {
update(event.position);
finish(_DragEndKind.dropped);
} else if (event is PointerCancelEvent) {
finish(_DragEndKind.canceled);
} else if (event is PointerMoveEvent) {
update(event.position);
}
// Drag API
void move(Offset offset) {
_position += offset;
update(_position);
}
void end(Offset velocity) {
finish(_DragEndKind.dropped);
}
void cancel() {
finish(_DragEndKind.canceled);
}
void update(Point globalPosition) {
......@@ -390,7 +375,6 @@ class _DragAvatar<T> {
_activeTargetWillAcceptDrop = false;
_entry.remove();
_entry = null;
router.removeRoute(pointer, handleEvent);
if (onDragEnd != null)
onDragEnd();
}
......
......@@ -168,7 +168,68 @@ void main() {
tester.pump();
expect(events, equals(<String>['tap', 'tap', 'drop']));
events.clear();
});
testWidgets((WidgetTester tester) {
TestPointer pointer = new TestPointer(7);
List<String> events = <String>[];
Point firstLocation, secondLocation;
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) { return new Column(
children: <Widget>[
new Draggable(
data: 1,
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
events.add('tap');
},
child: new Container(
child: new Text('Button')
)
),
feedback: new Text('Dragging')
),
new DragTarget(
builder: (context, data, rejects) {
return new Text('Target');
},
onAccept: (data) {
events.add('drop');
}
),
]);
},
}
));
expect(events, isEmpty);
expect(tester.findText('Button'), isNotNull);
expect(tester.findText('Target'), isNotNull);
expect(events, isEmpty);
tester.tap(tester.findText('Button'));
expect(events, equals(<String>['tap']));
events.clear();
firstLocation = tester.getCenter(tester.findText('Button'));
tester.dispatchEvent(pointer.down(firstLocation), firstLocation);
tester.pump();
secondLocation = tester.getCenter(tester.findText('Target'));
tester.dispatchEvent(pointer.move(secondLocation), firstLocation);
tester.pump();
expect(events, isEmpty);
tester.dispatchEvent(pointer.up(), firstLocation);
tester.pump();
expect(events, equals(<String>['drop']));
events.clear();
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment