From 8047f1e28932a30bac641c42e1d1cc071c1368d0 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 27 Aug 2015 18:05:16 -0700 Subject: [PATCH] Add tap, show press, and long press gestures and use tap in IconButton These gestures use the new gesture detection system. --- sky/packages/sky/lib/gestures/README.md | 9 ++ sky/packages/sky/lib/gestures/arena.dart | 24 ++- sky/packages/sky/lib/gestures/constants.dart | 23 +++ sky/packages/sky/lib/gestures/long_press.dart | 29 ++++ sky/packages/sky/lib/gestures/recognizer.dart | 148 ++++++++++++++++++ sky/packages/sky/lib/gestures/show_press.dart | 31 ++++ sky/packages/sky/lib/gestures/tap.dart | 25 +++ .../sky/lib/theme/view_configuration.dart | 16 -- .../sky/lib/widgets/gesture_detector.dart | 121 ++++++++++++++ sky/packages/sky/lib/widgets/icon_button.dart | 13 +- sky/packages/sky/lib/widgets/scrollable.dart | 6 +- sky/unit/test/base/pointer_router_test.dart | 6 +- sky/unit/test/engine/mock_events.dart | 81 ++++++++++ sky/unit/test/gestures/long_press_test.dart | 106 +++++++++++++ sky/unit/test/gestures/show_press_test.dart | 68 ++++++++ sky/unit/test/gestures/tap_test.dart | 41 +++++ sky/unit/test/widget/pageable_list_test.dart | 2 +- sky/unit/test/widget/widget_tester.dart | 83 +--------- 18 files changed, 718 insertions(+), 114 deletions(-) create mode 100644 sky/packages/sky/lib/gestures/README.md create mode 100644 sky/packages/sky/lib/gestures/constants.dart create mode 100644 sky/packages/sky/lib/gestures/long_press.dart create mode 100644 sky/packages/sky/lib/gestures/recognizer.dart create mode 100644 sky/packages/sky/lib/gestures/show_press.dart create mode 100644 sky/packages/sky/lib/gestures/tap.dart create mode 100644 sky/packages/sky/lib/widgets/gesture_detector.dart create mode 100644 sky/unit/test/engine/mock_events.dart create mode 100644 sky/unit/test/gestures/long_press_test.dart create mode 100644 sky/unit/test/gestures/show_press_test.dart create mode 100644 sky/unit/test/gestures/tap_test.dart diff --git a/sky/packages/sky/lib/gestures/README.md b/sky/packages/sky/lib/gestures/README.md new file mode 100644 index 00000000000..27ef1d0bcc9 --- /dev/null +++ b/sky/packages/sky/lib/gestures/README.md @@ -0,0 +1,9 @@ +Sky Gestures +============ + +Code for recognizing and representing user interface gestures. + +Dependencies +------------ + + * [`package:sky/base`](../base) diff --git a/sky/packages/sky/lib/gestures/arena.dart b/sky/packages/sky/lib/gestures/arena.dart index 360177d6802..c074b3b810b 100644 --- a/sky/packages/sky/lib/gestures/arena.dart +++ b/sky/packages/sky/lib/gestures/arena.dart @@ -7,14 +7,26 @@ enum GestureDisposition { rejected } +/// Represents an object participating in an arena. +/// +/// Receives callbacks from the GestureArena to notify the object when it wins +/// or loses a gesture negotiation. Exactly one of [acceptGesture] or +/// [rejectGesture] will be called for each arena key this member was added to, +/// regardless of what caused the arena to be resolved. For example, if a +/// member resolves the arena itself, that member still receives an +/// [acceptGesture] callback. abstract class GestureArenaMember { - /// Called when this member wins the arena. + /// Called when this member wins the arena for the given key. void acceptGesture(Object key); - /// Called when this member loses the arena. + /// Called when this member loses the arena for the given key. void rejectGesture(Object key); } +/// An interface to information to an arena +/// +/// A given [GestureArenaMember] can have multiple entries in multiple arenas +/// with different keys. class GestureArenaEntry { GestureArenaEntry._(this._arena, this._key, this._member); @@ -23,6 +35,8 @@ class GestureArenaEntry { final GestureArenaMember _member; /// Call this member to claim victory (with accepted) or admit defeat (with rejected). + /// + /// It's fine to attempt to resolve an arena that is already resolved. void resolve(GestureDisposition disposition) { _arena._resolve(_key, _member, disposition); } @@ -32,6 +46,8 @@ class GestureArenaEntry { class GestureArena { final Map> _arenas = new Map>(); + static final GestureArena instance = new GestureArena(); + GestureArenaEntry add(Object key, GestureArenaMember member) { List members = _arenas.putIfAbsent(key, () => new List()); members.add(member); @@ -40,10 +56,13 @@ class GestureArena { void _resolve(Object key, GestureArenaMember member, GestureDisposition disposition) { List members = _arenas[key]; + if (members == null) + return; // This arena has already resolved. assert(members != null); assert(members.contains(member)); if (disposition == GestureDisposition.rejected) { members.remove(member); + member.rejectGesture(key); if (members.length == 1) { _arenas.remove(key); members.first.acceptGesture(key); @@ -52,7 +71,6 @@ class GestureArena { } } else { assert(disposition == GestureDisposition.accepted); - List members = _arenas[key]; _arenas.remove(key); for (GestureArenaMember rejectedMember in members) { if (rejectedMember != member) diff --git a/sky/packages/sky/lib/gestures/constants.dart b/sky/packages/sky/lib/gestures/constants.dart new file mode 100644 index 00000000000..f379c547fec --- /dev/null +++ b/sky/packages/sky/lib/gestures/constants.dart @@ -0,0 +1,23 @@ +// 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. + +// Modeled after Android's ViewConfiguration: +// https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java + +const Duration kLongPressTimeout = const Duration(milliseconds: 500); +const Duration kTapTimeout = const Duration(milliseconds: 100); +const Duration kJumpTapTimeout = const Duration(milliseconds: 500); +const Duration kDoubleTapTimeout = const Duration(milliseconds: 300); +const Duration kDoubleTapMinTime = const Duration(milliseconds: 40); +const Duration kHoverTapTimeout = const Duration(milliseconds: 150); +const Duration kZoomControlsTimeout = const Duration(milliseconds: 3000); +const double kHoverTapSlop = 20.0; // Logical pixels +const double kEdgeSlop = 12.0; // Logical pixels +const double kTouchSlop = 8.0; // Logical pixels +const double kDoubleTapTouchSlop = kTouchSlop; // Logical pixels +const double kPagingTouchSlop = kTouchSlop * 2.0; // Logical pixels +const double kDoubleTapSlop = 100.0; // Logical pixels +const double kWindowTouchSlop = 16.0; // Logical pixels +const double kMinFlingVelocity = 50.0; // TODO(abarth): Which units is this in? +const double kMaxFlingVelocity = 8000.0; // TODO(abarth): Which units is this in? diff --git a/sky/packages/sky/lib/gestures/long_press.dart b/sky/packages/sky/lib/gestures/long_press.dart new file mode 100644 index 00000000000..d450ee0b60f --- /dev/null +++ b/sky/packages/sky/lib/gestures/long_press.dart @@ -0,0 +1,29 @@ +// 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:sky' as sky; + +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; +import 'package:sky/gestures/constants.dart'; +import 'package:sky/gestures/recognizer.dart'; + +typedef void GestureLongPressListener(); + +class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { + LongPressGestureRecognizer({ PointerRouter router, this.onLongPress }) + : super(router: router, deadline: kTapTimeout + kLongPressTimeout); + + GestureLongPressListener onLongPress; + + void didExceedDeadline() { + resolve(GestureDisposition.accepted); + onLongPress(); + } + + void handlePrimaryPointer(sky.PointerEvent event) { + if (event.type == 'pointerup') + resolve(GestureDisposition.rejected); + } +} diff --git a/sky/packages/sky/lib/gestures/recognizer.dart b/sky/packages/sky/lib/gestures/recognizer.dart new file mode 100644 index 00000000000..451511dad26 --- /dev/null +++ b/sky/packages/sky/lib/gestures/recognizer.dart @@ -0,0 +1,148 @@ +// 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:sky' as sky; + +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; +import 'package:sky/gestures/constants.dart'; + +abstract class GestureRecognizer extends GestureArenaMember { + GestureRecognizer({ PointerRouter router }) : _router = router; + + PointerRouter _router; + + final List _entries = new List(); + final Set _trackedPointers = new Set(); + + /// The primary entry point for users of this class. + void addPointer(sky.PointerEvent event); + + void handleEvent(sky.PointerEvent event); + void acceptGesture(int key); + void rejectGesture(int key); + void didStopTrackingLastPointer(); + + void resolve(GestureDisposition disposition) { + List localEntries = new List.from(_entries); + _entries.clear(); + for (GestureArenaEntry entry in localEntries) + entry.resolve(disposition); + } + + void dispose() { + resolve(GestureDisposition.rejected); + for (int pointer in _trackedPointers) + _router.removeRoute(pointer, handleEvent); + _trackedPointers.clear(); + assert(_entries.isEmpty); + _router = null; + } + + void startTrackingPointer(int pointer) { + _router.addRoute(pointer, handleEvent); + _trackedPointers.add(pointer); + _entries.add(GestureArena.instance.add(pointer, this)); + } + + void stopTrackingPointer(int pointer) { + _router.removeRoute(pointer, handleEvent); + _trackedPointers.remove(pointer); + if (_trackedPointers.isEmpty) + didStopTrackingLastPointer(); + } + + void stopTrackingIfPointerNoLongerDown(sky.PointerEvent event) { + if (event.type == 'pointerup' || event.type == 'pointercancel') + stopTrackingPointer(event.pointer); + } + +} + +enum GestureRecognizerState { + ready, + possible, + defunct +} + +sky.Point _getPoint(sky.PointerEvent event) { + return new sky.Point(event.x, event.y); +} + +abstract class PrimaryPointerGestureRecognizer extends GestureRecognizer { + PrimaryPointerGestureRecognizer({ PointerRouter router, this.deadline }) + : super(router: router); + + final Duration deadline; + + GestureRecognizerState state = GestureRecognizerState.ready; + int primaryPointer; + sky.Point initialPosition; + Timer _timer; + + void addPointer(sky.PointerEvent event) { + startTrackingPointer(event.pointer); + if (state == GestureRecognizerState.ready) { + state = GestureRecognizerState.possible; + primaryPointer = event.pointer; + initialPosition = _getPoint(event); + if (deadline != null) + _timer = new Timer(deadline, didExceedDeadline); + } + } + + void handleEvent(sky.PointerEvent event) { + assert(state != GestureRecognizerState.ready); + if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) { + // TODO(abarth): Maybe factor the slop handling out into a separate class? + if (event.type == 'pointermove' && _getDistance(event) > kTouchSlop) + resolve(GestureDisposition.rejected); + else + handlePrimaryPointer(event); + } + stopTrackingIfPointerNoLongerDown(event); + } + + /// Override to provide behavior for the primary pointer when the gesture is still possible. + void handlePrimaryPointer(sky.PointerEvent event); + + /// Override to be notified with [deadline] is exceeded. + /// + /// You must override this function if you supply a [deadline]. + void didExceedDeadline() { + assert(deadline == null); + } + + void acceptGesture(int pointer) { + } + + void rejectGesture(int pointer) { + _stopTimer(); + if (pointer == primaryPointer) + state = GestureRecognizerState.defunct; + } + + void didStopTrackingLastPointer() { + state = GestureRecognizerState.ready; + } + + void dispose() { + _stopTimer(); + super.dispose(); + } + + void _stopTimer() { + if (_timer != null) { + _timer.cancel(); + _timer = null; + } + } + + double _getDistance(sky.PointerEvent event) { + sky.Offset offset = _getPoint(event) - initialPosition; + return offset.distance; + } + +} diff --git a/sky/packages/sky/lib/gestures/show_press.dart b/sky/packages/sky/lib/gestures/show_press.dart new file mode 100644 index 00000000000..be9f4b3d572 --- /dev/null +++ b/sky/packages/sky/lib/gestures/show_press.dart @@ -0,0 +1,31 @@ +// 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:sky' as sky; + +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; +import 'package:sky/gestures/constants.dart'; +import 'package:sky/gestures/recognizer.dart'; + +typedef void GestureShowPressListener(); + +class ShowPressGestureRecognizer extends PrimaryPointerGestureRecognizer { + ShowPressGestureRecognizer({ PointerRouter router, this.onShowPress }) + : super(router: router, deadline: kTapTimeout); + + GestureShowPressListener onShowPress; + + void didExceedDeadline() { + // Show press isn't an exclusive gesture. We can recognize a show press + // as well as another gesture, like a long press. + resolve(GestureDisposition.rejected); + onShowPress(); + } + + void handlePrimaryPointer(sky.PointerEvent event) { + if (event.type == 'pointerup') + resolve(GestureDisposition.rejected); + } +} diff --git a/sky/packages/sky/lib/gestures/tap.dart b/sky/packages/sky/lib/gestures/tap.dart new file mode 100644 index 00000000000..df1aee0de16 --- /dev/null +++ b/sky/packages/sky/lib/gestures/tap.dart @@ -0,0 +1,25 @@ +// 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:sky' as sky; + +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; +import 'package:sky/gestures/recognizer.dart'; + +typedef void GestureTapListener(); + +class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { + TapGestureRecognizer({ PointerRouter router, this.onTap }) + : super(router: router); + + GestureTapListener onTap; + + void handlePrimaryPointer(sky.PointerEvent event) { + if (event.type == 'pointerup') { + resolve(GestureDisposition.accepted); + onTap(); + } + } +} diff --git a/sky/packages/sky/lib/theme/view_configuration.dart b/sky/packages/sky/lib/theme/view_configuration.dart index bebad2949f5..b106845c052 100644 --- a/sky/packages/sky/lib/theme/view_configuration.dart +++ b/sky/packages/sky/lib/theme/view_configuration.dart @@ -21,19 +21,3 @@ const double kScrollbarFadeDuration = 250.0; const double kScrollbarFadeDelay = 300.0; const double kFadingEdgeLength = 12.0; const double kPressedStateDuration = 64.0; -const double kDefaultLongPressTimeout = 500.0; -const double kTapTimeout = 100.0; -const double kJumpTapTimeout = 500.0; -const double kDoubleTapTimeout = 300.0; -const double kDoubleTapMinTime = 40.0; -const double kHoverTapTimeout = 150.0; -const double kHoverTapSlop = 20.0; -const double kZoomControlsTimeout = 3000.0; -const double kEdgeSlop = 12.0; -const double kTouchSlop = 8.0; -const double kDoubleTapTouchSlop = kTouchSlop; -const double kPagingTouchSlop = kTouchSlop * 2.0; -const double kDoubleTapSlop = 100.0; -const double kWindowTouchSlop = 16.0; -const double kMinFlingVelocity = 50.0; -const double kMaxFlingVelocity = 8000.0; diff --git a/sky/packages/sky/lib/widgets/gesture_detector.dart b/sky/packages/sky/lib/widgets/gesture_detector.dart new file mode 100644 index 00000000000..ee08b54f7e5 --- /dev/null +++ b/sky/packages/sky/lib/widgets/gesture_detector.dart @@ -0,0 +1,121 @@ +// 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:sky' as sky; + +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/long_press.dart'; +import 'package:sky/gestures/recognizer.dart'; +import 'package:sky/gestures/show_press.dart'; +import 'package:sky/gestures/tap.dart'; +import 'package:sky/rendering/sky_binding.dart'; +import 'package:sky/widgets/framework.dart'; + +class GestureDetector extends StatefulComponent { + GestureDetector({ + Key key, + this.child, + this.onTap, + this.onShowPress, + this.onLongPress + }) : super(key: key); + + Widget child; + GestureTapListener onTap; + GestureShowPressListener onShowPress; + GestureLongPressListener onLongPress; + + void syncConstructorArguments(GestureDetector source) { + child = source.child; + onTap = source.onTap; + onShowPress = source.onShowPress; + onLongPress = source.onLongPress; + _syncGestureListeners(); + } + + final PointerRouter _router = SkyBinding.instance.pointerRouter; + + TapGestureRecognizer _tap; + TapGestureRecognizer _ensureTap() { + if (_tap == null) + _tap = new TapGestureRecognizer(router: _router); + return _tap; + } + + ShowPressGestureRecognizer _showPress; + ShowPressGestureRecognizer _ensureShowPress() { + if (_showPress == null) + _showPress = new ShowPressGestureRecognizer(router: _router); + return _showPress; + } + + LongPressGestureRecognizer _longPress; + LongPressGestureRecognizer _ensureLongPress() { + if (_longPress == null) + _longPress = new LongPressGestureRecognizer(router: _router); + return _longPress; + } + + void didMount() { + super.didMount(); + _syncGestureListeners(); + } + + void didUnmount() { + super.didUnmount(); + _tap = _ensureDisposed(_tap); + _showPress = _ensureDisposed(_showPress); + _longPress = _ensureDisposed(_longPress); + } + + void _syncGestureListeners() { + _syncTap(); + _syncShowPress(); + _syncLongPress(); + } + + void _syncTap() { + if (onTap == null) + _tap = _ensureDisposed(_tap); + else + _ensureTap().onTap = onTap; + } + + void _syncShowPress() { + if (onShowPress == null) + _showPress = _ensureDisposed(_showPress); + else + _ensureShowPress().onShowPress = onShowPress; + } + + void _syncLongPress() { + if (onLongPress == null) + _longPress = _ensureDisposed(_longPress); + else + _ensureLongPress().onLongPress = onLongPress; + } + + GestureRecognizer _ensureDisposed(GestureRecognizer recognizer) { + if (recognizer != null) + recognizer.dispose(); + return null; + } + + EventDisposition _handlePointerDown(sky.PointerEvent event) { + if (_tap != null) + _tap.addPointer(event); + if (_showPress != null) + _showPress.addPointer(event); + if (_longPress != null) + _longPress.addPointer(event); + return EventDisposition.processed; + } + + Widget build() { + return new Listener( + onPointerDown: _handlePointerDown, + child: child + ); + } +} diff --git a/sky/packages/sky/lib/widgets/icon_button.dart b/sky/packages/sky/lib/widgets/icon_button.dart index fc54846d6ae..a66458bbd9a 100644 --- a/sky/packages/sky/lib/widgets/icon_button.dart +++ b/sky/packages/sky/lib/widgets/icon_button.dart @@ -7,6 +7,7 @@ import 'dart:sky' as sky; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/icon.dart'; import 'package:sky/widgets/framework.dart'; +import 'package:sky/widgets/gesture_detector.dart'; class IconButton extends Component { @@ -25,14 +26,14 @@ class IconButton extends Component { child: child ); } - return new Listener( - child: new Padding( - child: child, - padding: const EdgeDims.all(8.0)), - onGestureTap: (_) { + return new GestureDetector( + onTap: () { if (onPressed != null) onPressed(); - } + }, + child: new Padding( + child: child, + padding: const EdgeDims.all(8.0)) ); } diff --git a/sky/packages/sky/lib/widgets/scrollable.dart b/sky/packages/sky/lib/widgets/scrollable.dart index e823bd9166e..1571a15d4c9 100644 --- a/sky/packages/sky/lib/widgets/scrollable.dart +++ b/sky/packages/sky/lib/widgets/scrollable.dart @@ -12,9 +12,9 @@ import 'package:sky/animation/animation_performance.dart'; import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/curves.dart'; import 'package:sky/animation/scroll_behavior.dart'; +import 'package:sky/gestures/constants.dart'; import 'package:sky/rendering/box.dart'; import 'package:sky/rendering/viewport.dart'; -import 'package:sky/theme/view_configuration.dart' as config; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/framework.dart'; import 'package:sky/widgets/mixed_viewport.dart'; @@ -24,8 +24,8 @@ export 'package:sky/widgets/mixed_viewport.dart' show MixedViewportLayoutState; // The GestureEvent velocity properties are pixels/second, config min,max limits are pixels/ms const double _kMillisecondsPerSecond = 1000.0; -const double _kMinFlingVelocity = -config.kMaxFlingVelocity * _kMillisecondsPerSecond; -const double _kMaxFlingVelocity = config.kMaxFlingVelocity * _kMillisecondsPerSecond; +const double _kMinFlingVelocity = -kMaxFlingVelocity * _kMillisecondsPerSecond; +const double _kMaxFlingVelocity = kMaxFlingVelocity * _kMillisecondsPerSecond; typedef void ScrollListener(); diff --git a/sky/unit/test/base/pointer_router_test.dart b/sky/unit/test/base/pointer_router_test.dart index 1fb5c87f57a..835b95088a4 100644 --- a/sky/unit/test/base/pointer_router_test.dart +++ b/sky/unit/test/base/pointer_router_test.dart @@ -4,11 +4,7 @@ import 'package:sky/base/hit_test.dart'; import 'package:sky/base/pointer_router.dart'; import 'package:test/test.dart'; -class TestPointerEvent extends sky.PointerEvent { - TestPointerEvent({ this.pointer }); - - final int pointer; -} +import '../engine/mock_events.dart'; void main() { test('Should route pointers', () { diff --git a/sky/unit/test/engine/mock_events.dart b/sky/unit/test/engine/mock_events.dart new file mode 100644 index 00000000000..a596ae1b7ce --- /dev/null +++ b/sky/unit/test/engine/mock_events.dart @@ -0,0 +1,81 @@ +import 'dart:sky' as sky; + +class TestPointerEvent extends sky.PointerEvent { + TestPointerEvent({ + this.type, + this.pointer, + this.kind, + this.x, + this.y, + this.dx, + this.dy, + this.velocityX, + this.velocityY, + this.buttons, + this.down, + this.primary, + this.obscured, + this.pressure, + this.pressureMin, + this.pressureMax, + this.distance, + this.distanceMin, + this.distanceMax, + this.radiusMajor, + this.radiusMinor, + this.radiusMin, + this.radiusMax, + this.orientation, + this.tilt + }); + + // These are all of the PointerEvent members, but not all of Event. + String type; + int pointer; + String kind; + double x; + double y; + double dx; + double dy; + double velocityX; + double velocityY; + int buttons; + bool down; + bool primary; + bool obscured; + double pressure; + double pressureMin; + double pressureMax; + double distance; + double distanceMin; + double distanceMax; + double radiusMajor; + double radiusMinor; + double radiusMin; + double radiusMax; + double orientation; + double tilt; +} + +class TestGestureEvent extends sky.GestureEvent { + TestGestureEvent({ + this.type, + this.primaryPointer, + this.x, + this.y, + this.dx, + this.dy, + this.velocityX, + this.velocityY + }); + + // These are all of the GestureEvent members, but not all of Event. + String type; + int primaryPointer; + double x; + double y; + double dx; + double dy; + double velocityX; + double velocityY; +} diff --git a/sky/unit/test/gestures/long_press_test.dart b/sky/unit/test/gestures/long_press_test.dart new file mode 100644 index 00000000000..e38e5a5db0f --- /dev/null +++ b/sky/unit/test/gestures/long_press_test.dart @@ -0,0 +1,106 @@ +import 'package:quiver/testing/async.dart'; +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/long_press.dart'; +import 'package:sky/gestures/show_press.dart'; +import 'package:test/test.dart'; + +import '../engine/mock_events.dart'; + +final TestPointerEvent down = new TestPointerEvent( + pointer: 5, + type: 'pointerdown', + x: 10.0, + y: 10.0 +); + +final TestPointerEvent up = new TestPointerEvent( + pointer: 5, + type: 'pointerup', + x: 11.0, + y: 9.0 +); + +void main() { + test('Should recognize long press', () { + PointerRouter router = new PointerRouter(); + LongPressGestureRecognizer longPress = new LongPressGestureRecognizer(router: router); + + bool longPressRecognized = false; + longPress.onLongPress = () { + longPressRecognized = true; + }; + + new FakeAsync().run((async) { + longPress.addPointer(down); + expect(longPressRecognized, isFalse); + router.handleEvent(down, null); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 300)); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 700)); + expect(longPressRecognized, isTrue); + }); + + longPress.dispose(); + }); + + test('Up cancels long press', () { + PointerRouter router = new PointerRouter(); + LongPressGestureRecognizer longPress = new LongPressGestureRecognizer(router: router); + + bool longPressRecognized = false; + longPress.onLongPress = () { + longPressRecognized = true; + }; + + new FakeAsync().run((async) { + longPress.addPointer(down); + expect(longPressRecognized, isFalse); + router.handleEvent(down, null); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 300)); + expect(longPressRecognized, isFalse); + router.handleEvent(up, null); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(seconds: 1)); + expect(longPressRecognized, isFalse); + }); + + longPress.dispose(); + }); + + test('Should recognize both show press and long press', () { + PointerRouter router = new PointerRouter(); + ShowPressGestureRecognizer showPress = new ShowPressGestureRecognizer(router: router); + LongPressGestureRecognizer longPress = new LongPressGestureRecognizer(router: router); + + bool showPressRecognized = false; + showPress.onShowPress = () { + showPressRecognized = true; + }; + + bool longPressRecognized = false; + longPress.onLongPress = () { + longPressRecognized = true; + }; + + new FakeAsync().run((async) { + showPress.addPointer(down); + longPress.addPointer(down); + expect(showPressRecognized, isFalse); + expect(longPressRecognized, isFalse); + router.handleEvent(down, null); + expect(showPressRecognized, isFalse); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 300)); + expect(showPressRecognized, isTrue); + expect(longPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 700)); + expect(showPressRecognized, isTrue); + expect(longPressRecognized, isTrue); + }); + + showPress.dispose(); + longPress.dispose(); + }); +} diff --git a/sky/unit/test/gestures/show_press_test.dart b/sky/unit/test/gestures/show_press_test.dart new file mode 100644 index 00000000000..8e6053624b7 --- /dev/null +++ b/sky/unit/test/gestures/show_press_test.dart @@ -0,0 +1,68 @@ +import 'package:quiver/testing/async.dart'; +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/show_press.dart'; +import 'package:test/test.dart'; + +import '../engine/mock_events.dart'; + +final TestPointerEvent down = new TestPointerEvent( + pointer: 5, + type: 'pointerdown', + x: 10.0, + y: 10.0 +); + +final TestPointerEvent up = new TestPointerEvent( + pointer: 5, + type: 'pointerup', + x: 11.0, + y: 9.0 +); + +void main() { + test('Should recognize show press', () { + PointerRouter router = new PointerRouter(); + ShowPressGestureRecognizer showPress = new ShowPressGestureRecognizer(router: router); + + bool showPressRecognized = false; + showPress.onShowPress = () { + showPressRecognized = true; + }; + + new FakeAsync().run((async) { + showPress.addPointer(down); + expect(showPressRecognized, isFalse); + router.handleEvent(down, null); + expect(showPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 300)); + expect(showPressRecognized, isTrue); + }); + + showPress.dispose(); + }); + + test('Up cancels show press', () { + PointerRouter router = new PointerRouter(); + ShowPressGestureRecognizer showPress = new ShowPressGestureRecognizer(router: router); + + bool showPressRecognized = false; + showPress.onShowPress = () { + showPressRecognized = true; + }; + + new FakeAsync().run((async) { + showPress.addPointer(down); + expect(showPressRecognized, isFalse); + router.handleEvent(down, null); + expect(showPressRecognized, isFalse); + async.elapse(new Duration(milliseconds: 50)); + expect(showPressRecognized, isFalse); + router.handleEvent(up, null); + expect(showPressRecognized, isFalse); + async.elapse(new Duration(seconds: 1)); + expect(showPressRecognized, isFalse); + }); + + showPress.dispose(); + }); +} diff --git a/sky/unit/test/gestures/tap_test.dart b/sky/unit/test/gestures/tap_test.dart new file mode 100644 index 00000000000..8935055b24b --- /dev/null +++ b/sky/unit/test/gestures/tap_test.dart @@ -0,0 +1,41 @@ +import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/tap.dart'; +import 'package:test/test.dart'; + +import '../engine/mock_events.dart'; + +void main() { + test('Should recognize tap', () { + PointerRouter router = new PointerRouter(); + TapGestureRecognizer tap = new TapGestureRecognizer(router: router); + + bool tapRecognized = false; + tap.onTap = () { + tapRecognized = true; + }; + + TestPointerEvent down = new TestPointerEvent( + pointer: 5, + type: 'pointerdown', + x: 10.0, + y: 10.0 + ); + + tap.addPointer(down); + expect(tapRecognized, isFalse); + router.handleEvent(down, null); + expect(tapRecognized, isFalse); + + TestPointerEvent up = new TestPointerEvent( + pointer: 5, + type: 'pointerup', + x: 11.0, + y: 9.0 + ); + + router.handleEvent(up, null); + expect(tapRecognized, isTrue); + + tap.dispose(); + }); +} diff --git a/sky/unit/test/widget/pageable_list_test.dart b/sky/unit/test/widget/pageable_list_test.dart index 915f1f49cbd..f560eefa8e3 100644 --- a/sky/unit/test/widget/pageable_list_test.dart +++ b/sky/unit/test/widget/pageable_list_test.dart @@ -1,6 +1,6 @@ +import 'package:quiver/testing/async.dart'; import 'package:sky/widgets.dart'; import 'package:test/test.dart'; -import 'package:quiver/testing/async.dart'; import 'widget_tester.dart'; diff --git a/sky/unit/test/widget/widget_tester.dart b/sky/unit/test/widget/widget_tester.dart index 4676e04eb7b..94e73be4952 100644 --- a/sky/unit/test/widget/widget_tester.dart +++ b/sky/unit/test/widget/widget_tester.dart @@ -1,8 +1,11 @@ import 'dart:sky' as sky; + import 'package:sky/rendering.dart'; import 'package:sky/widgets.dart'; import 'package:sky/base/scheduler.dart' as scheduler; +import '../engine/mock_events.dart'; + typedef Widget WidgetBuilder(); class TestApp extends App { @@ -22,86 +25,6 @@ class TestApp extends App { } } -class TestPointerEvent extends sky.PointerEvent { - TestPointerEvent({ - this.type, - this.pointer, - this.kind, - this.x, - this.y, - this.dx, - this.dy, - this.velocityX, - this.velocityY, - this.buttons, - this.down, - this.primary, - this.obscured, - this.pressure, - this.pressureMin, - this.pressureMax, - this.distance, - this.distanceMin, - this.distanceMax, - this.radiusMajor, - this.radiusMinor, - this.radiusMin, - this.radiusMax, - this.orientation, - this.tilt - }); - - // These are all of the PointerEvent members, but not all of Event. - String type; - int pointer; - String kind; - double x; - double y; - double dx; - double dy; - double velocityX; - double velocityY; - int buttons; - bool down; - bool primary; - bool obscured; - double pressure; - double pressureMin; - double pressureMax; - double distance; - double distanceMin; - double distanceMax; - double radiusMajor; - double radiusMinor; - double radiusMin; - double radiusMax; - double orientation; - double tilt; -} - -class TestGestureEvent extends sky.GestureEvent { - TestGestureEvent({ - this.type, - this.primaryPointer, - this.x, - this.y, - this.dx, - this.dy, - this.velocityX, - this.velocityY - }); - - // These are all of the GestureEvent members, but not all of Event. - String type; - int primaryPointer; - double x; - double y; - double dx; - double dy; - double velocityX; - double velocityY; -} - class WidgetTester { WidgetTester() { _app = new TestApp();