From cea4aa9b7b843af10c24a538e1a84f9f28f57f5a Mon Sep 17 00:00:00 2001 From: jslavitz Date: Wed, 9 Jan 2019 10:53:47 -0800 Subject: [PATCH] Teach drag start behaviors to DragGestureRecognizer (#26246) * the onStart callback will report the location of the pointer where it wins the gesture arena by default instead of the pointer down location. Fixes all tests related to changing this default value. --- .../lib/demo/material/drawer_demo.dart | 5 + .../demo/material/text_form_field_demo.dart | 4 + .../flutter_gallery/lib/gallery/home.dart | 2 + examples/stocks/lib/stock_home.dart | 4 + .../flutter/lib/src/cupertino/switch.dart | 45 ++++- .../flutter/lib/src/gestures/monodrag.dart | 65 ++++++- .../flutter/lib/src/gestures/recognizer.dart | 18 ++ .../flutter/lib/src/material/date_picker.dart | 33 ++++ packages/flutter/lib/src/material/drawer.dart | 25 +++ .../src/material/paginated_data_table.dart | 8 + .../flutter/lib/src/material/scaffold.dart | 11 +- packages/flutter/lib/src/material/switch.dart | 24 ++- packages/flutter/lib/src/material/tabs.dart | 16 +- .../flutter/lib/src/material/text_field.dart | 7 + .../flutter/lib/src/widgets/dismissible.dart | 29 ++- .../lib/src/widgets/editable_text.dart | 15 +- .../lib/src/widgets/gesture_detector.dart | 41 ++++- .../lib/src/widgets/nested_scroll_view.dart | 10 +- .../flutter/lib/src/widgets/page_view.dart | 8 + .../flutter/lib/src/widgets/scroll_view.dart | 21 +++ .../flutter/lib/src/widgets/scrollable.dart | 27 ++- .../src/widgets/single_child_scroll_view.dart | 7 + .../lib/src/widgets/text_selection.dart | 28 ++- .../flutter/test/cupertino/switch_test.dart | 86 +++++++++ packages/flutter/test/gestures/drag_test.dart | 173 +++++++++++++++--- .../test/material/date_picker_test.dart | 5 +- .../flutter/test/material/ink_well_test.dart | 2 + .../material/paginated_data_table_test.dart | 38 ++-- .../flutter/test/material/scaffold_test.dart | 3 + .../flutter/test/material/switch_test.dart | 97 +++++++++- .../test/material/text_field_test.dart | 18 +- .../widgets/automatic_keep_alive_test.dart | 2 + .../test/widgets/dismissible_test.dart | 4 + .../flutter/test/widgets/draggable_test.dart | 2 + .../flutter/test/widgets/drawer_test.dart | 3 + .../test/widgets/gesture_detector_test.dart | 1 + .../flutter/test/widgets/grid_view_test.dart | 6 + .../flutter/test/widgets/heroes_test.dart | 7 +- .../widgets/listview_end_append_test.dart | 3 + .../test/widgets/nested_scroll_view_test.dart | 11 ++ .../flutter/test/widgets/page_view_test.dart | 3 + .../widgets/scroll_notification_test.dart | 14 +- .../test/widgets/scroll_view_test.dart | 6 + .../test/widgets/scrollable_fling_test.dart | 6 +- .../scrollable_list_hit_testing_test.dart | 5 + .../widgets/scrollable_semantics_test.dart | 2 + .../test/widgets/widget_inspector_test.dart | 10 +- 47 files changed, 863 insertions(+), 97 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart index 3f08bdea83..a97819910f 100644 --- a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../../gallery/demo.dart'; @@ -79,6 +80,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, key: _scaffoldKey, appBar: AppBar( leading: IconButton( @@ -106,6 +108,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { ), otherAccountsPictures: [ GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { _onOtherAccountsTap(context); }, @@ -120,6 +123,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { ), ), GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { _onOtherAccountsTap(context); }, @@ -149,6 +153,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { removeTop: true, child: Expanded( child: ListView( + dragStartBehavior: DragStartBehavior.down, padding: const EdgeInsets.only(top: 8.0), children: [ Stack( diff --git a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart index ddbea4bc54..4deab75ab4 100644 --- a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../../gallery/demo.dart'; @@ -67,6 +68,7 @@ class _PasswordFieldState extends State { labelText: widget.labelText, helperText: widget.helperText, suffixIcon: GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { setState(() { _obscureText = !_obscureText; @@ -167,6 +169,7 @@ class TextFormFieldDemoState extends State { @override Widget build(BuildContext context) { return Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, key: _scaffoldKey, appBar: AppBar( title: const Text('Text fields'), @@ -180,6 +183,7 @@ class TextFormFieldDemoState extends State { autovalidate: _autovalidate, onWillPop: _warnUserAboutInvalidData, child: SingleChildScrollView( + dragStartBehavior: DragStartBehavior.down, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 13bc8bb078..762656dc1f 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'backdrop.dart'; import 'demos.dart'; @@ -256,6 +257,7 @@ class _DemosPage extends StatelessWidget { label: category.name, explicitChildNodes: true, child: ListView( + dragStartBehavior: DragStartBehavior.down, key: PageStorageKey(category.name), padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding), children: kGalleryCategoryToDemos[category].map((GalleryDemo demo) { diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index c77134ea0a..f13b51b570 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree, DebugSemanticsDumpOrder; import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'stock_data.dart'; import 'stock_list.dart'; import 'stock_strings.dart'; @@ -110,6 +111,7 @@ class StockHomeState extends State { Widget _buildDrawer(BuildContext context) { return Drawer( child: ListView( + dragStartBehavior: DragStartBehavior.down, children: [ const DrawerHeader(child: Center(child: Text('Stocks'))), const ListTile( @@ -317,11 +319,13 @@ class StockHomeState extends State { return DefaultTabController( length: 2, child: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, key: _scaffoldKey, appBar: _isSearching ? buildSearchBar() : buildAppBar(), floatingActionButton: buildFloatingActionButton(), drawer: _buildDrawer(context), body: TabBarView( + dragStartBehavior: DragStartBehavior.down, children: [ _buildStockTab(context, StockHomeTab.market, widget.stocks.allSymbols), _buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols), diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart index bbb1d7a42f..f32b2d8ada 100644 --- a/packages/flutter/lib/src/cupertino/switch.dart +++ b/packages/flutter/lib/src/cupertino/switch.dart @@ -52,12 +52,16 @@ import 'thumb_painter.dart'; /// * class CupertinoSwitch extends StatefulWidget { /// Creates an iOS-style switch. + /// + /// [dragStartBehavior] must not be null. const CupertinoSwitch({ Key key, @required this.value, @required this.onChanged, this.activeColor, - }) : super(key: key); + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(dragStartBehavior != null), + super(key: key); /// Whether this switch is on or off. final bool value; @@ -92,6 +96,26 @@ class CupertinoSwitch extends StatefulWidget { /// [CupertinoTheme] in accordance to native iOS behavior. final Color activeColor; + /// {@template flutter.cupertino.switch.dragStartBehavior} + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag behavior used to move the + /// switch from on to off will begin upon the detection of a drag gesture. If + /// set to [DragStartBehavior.down] it will begin when a down event is first + /// detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + /// {@endtemplate} + final DragStartBehavior dragStartBehavior; + @override _CupertinoSwitchState createState() => _CupertinoSwitchState(); @@ -111,6 +135,7 @@ class _CupertinoSwitchState extends State with TickerProviderSt activeColor: widget.activeColor ?? CupertinoColors.activeGreen, onChanged: widget.onChanged, vsync: this, + dragStartBehavior: widget.dragStartBehavior, ); } } @@ -122,12 +147,14 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { this.activeColor, this.onChanged, this.vsync, + this.dragStartBehavior = DragStartBehavior.start, }) : super(key: key); final bool value; final Color activeColor; final ValueChanged onChanged; final TickerProvider vsync; + final DragStartBehavior dragStartBehavior; @override _RenderCupertinoSwitch createRenderObject(BuildContext context) { @@ -137,6 +164,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { onChanged: onChanged, textDirection: Directionality.of(context), vsync: vsync, + dragStartBehavior: dragStartBehavior ); } @@ -147,7 +175,8 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ..activeColor = activeColor ..onChanged = onChanged ..textDirection = Directionality.of(context) - ..vsync = vsync; + ..vsync = vsync + ..dragStartBehavior = dragStartBehavior; } } @@ -171,6 +200,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ValueChanged onChanged, @required TextDirection textDirection, @required TickerProvider vsync, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : assert(value != null), assert(activeColor != null), assert(vsync != null), @@ -188,7 +218,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { _drag = HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd; + ..onEnd = _handleDragEnd + ..dragStartBehavior = dragStartBehavior; _positionController = AnimationController( duration: _kToggleDuration, value: value ? 1.0 : 0.0, @@ -276,6 +307,14 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { markNeedsPaint(); } + DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior; + set dragStartBehavior(DragStartBehavior value) { + assert(value != null); + if (_drag.dragStartBehavior == value) + return; + _drag.dragStartBehavior = value; + } + bool get isInteractive => onChanged != null; TapGestureRecognizer _tap; diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 4c25e6857a..c8233113b2 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; + import 'arena.dart'; import 'constants.dart'; import 'drag_details.dart'; @@ -48,7 +50,36 @@ typedef GestureDragCancelCallback = void Function(); /// * [PanGestureRecognizer], for drags that are not locked to a single axis. abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// Initialize the object. - DragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); + /// + /// [dragStartBehavior] must not be null. + DragGestureRecognizer({ + Object debugOwner, + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(dragStartBehavior != null), + super(debugOwner: debugOwner); + + /// Configure the behavior of offsets sent to [onStart]. + /// + /// If set to [DragStartBehavior.start], the [onStart] callback will be called at the time and + /// position when the gesture detector wins the arena. If [DragStartBehavior.down], + /// [onStart] will be called at the time and position when a down event was + /// first detected. + /// + /// For more information about the gesture arena: + /// https://flutter.io/docs/development/ui/advanced/gestures#gesture-disambiguation + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// ## Example: + /// + /// A finger presses down on the screen with offset (500.0, 500.0), + /// and then moves to position (510.0, 500.0) before winning the arena. + /// With [dragStartBehavior] set to [DragStartBehavior.down], the [onStart] + /// callback will be called at the time corresponding to the touch's position + /// at (500.0, 500.0). If it is instead set to [DragStartBehavior.start], + /// [onStart] will be called at the time corresponding to the touch's position + /// at (510.0, 500.0). + DragStartBehavior dragStartBehavior; /// A pointer has contacted the screen and might begin to move. /// @@ -60,6 +91,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [DragStartDetails] object. + /// + /// Depending on the value of [dragStartBehavior], this function will be + /// called on the initial touch down, if set to [DragStartBehavior.down] or + /// when the drag gesture is first detected, if set to + /// [DragStartBehavior.start]. GestureDragStartCallback onStart; /// A pointer that is in contact with the screen and moving has moved again. @@ -163,6 +199,16 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { _state = _DragState.accepted; final Offset delta = _pendingDragOffset; final Duration timestamp = _lastPendingEventTimestamp; + Offset updateDelta; + switch (dragStartBehavior) { + case DragStartBehavior.start: + _initialPosition = _initialPosition + delta; + updateDelta = Offset.zero; + break; + case DragStartBehavior.down: + updateDelta = _getDeltaForDetails(delta); + break; + } _pendingDragOffset = Offset.zero; _lastPendingEventTimestamp = null; if (onStart != null) { @@ -171,13 +217,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { globalPosition: _initialPosition, ))); } - if (delta != Offset.zero && onUpdate != null) { - final Offset deltaForDetails = _getDeltaForDetails(delta); + if (updateDelta != Offset.zero && onUpdate != null) { invokeCallback('onUpdate', () => onUpdate(DragUpdateDetails( sourceTimeStamp: timestamp, - delta: deltaForDetails, - primaryDelta: _getPrimaryValueFromOffset(delta), - globalPosition: _initialPosition + deltaForDetails, + delta: updateDelta, + primaryDelta: _getPrimaryValueFromOffset(updateDelta), + globalPosition: _initialPosition + updateDelta, // Only adds delta for down behaviour ))); } } @@ -232,6 +277,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { _velocityTrackers.clear(); super.dispose(); } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('Start Behavior', dragStartBehavior)); + } } /// Recognizes movement in the vertical direction. @@ -280,7 +330,8 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { /// track each touch point independently. class HorizontalDragGestureRecognizer extends DragGestureRecognizer { /// Create a gesture recognizer for interactions in the horizontal axis. - HorizontalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); + HorizontalDragGestureRecognizer({ Object debugOwner }) : + super(debugOwner: debugOwner); @override bool _isFlingGesture(VelocityEstimate estimate) { diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index ca71a26b72..d5ddbd619e 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -24,6 +24,24 @@ export 'pointer_router.dart' show PointerRouter; /// anonymous functions that return objects of particular types. typedef RecognizerCallback = T Function(); +/// Configuration of offset passed to [DragStartDetails]. +/// +/// The settings determines when a drag formally starts when the user +/// initiates a drag. +/// +/// See also: +/// +/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. +enum DragStartBehavior { + /// Set the initial offset, at the position where the first down even was + /// detected. + down, + + /// Set the initial position at the position where the drag start event was + /// detected. + start, +} + /// The base class that all gesture recognizers inherit from. /// /// Provides a basic API that can be used by classes that work with diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index f4b77576a8..cdda1eabc7 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'button_bar.dart'; import 'button_theme.dart'; @@ -250,10 +251,12 @@ class DayPicker extends StatelessWidget { @required this.lastDate, @required this.displayedMonth, this.selectableDayPredicate, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(selectedDate != null), assert(currentDate != null), assert(onChanged != null), assert(displayedMonth != null), + assert(dragStartBehavior != null), assert(!firstDate.isAfter(lastDate)), assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)), super(key: key); @@ -281,6 +284,24 @@ class DayPicker extends StatelessWidget { /// Optional user supplied predicate function to customize selectable days. final SelectableDayPredicate selectableDayPredicate; + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag gesture used to scroll a + /// date picker wheel will begin upon the detection of a drag gesture. If set + /// to [DragStartBehavior.down] it will begin when a down event is first + /// detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + /// Builds widgets showing abbreviated days of week. The first widget in the /// returned list corresponds to the first day of week for the current locale. /// @@ -442,6 +463,7 @@ class DayPicker extends StatelessWidget { onChanged(dayToBuild); }, child: dayWidget, + dragStartBehavior: dragStartBehavior, ); } @@ -502,6 +524,7 @@ class MonthPicker extends StatefulWidget { @required this.firstDate, @required this.lastDate, this.selectableDayPredicate, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(selectedDate != null), assert(onChanged != null), assert(!firstDate.isAfter(lastDate)), @@ -525,6 +548,9 @@ class MonthPicker extends StatefulWidget { /// Optional user supplied predicate function to customize selectable days. final SelectableDayPredicate selectableDayPredicate; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override _MonthPickerState createState() => _MonthPickerState(); } @@ -609,6 +635,7 @@ class _MonthPickerState extends State with SingleTickerProviderStat lastDate: widget.lastDate, displayedMonth: month, selectableDayPredicate: widget.selectableDayPredicate, + dragStartBehavior: widget.dragStartBehavior, ); } @@ -669,6 +696,7 @@ class _MonthPickerState extends State with SingleTickerProviderStat return false; }, child: PageView.builder( + dragStartBehavior: widget.dragStartBehavior, key: ValueKey(widget.selectedDate), controller: _dayPickerController, scrollDirection: Axis.horizontal, @@ -759,6 +787,7 @@ class YearPicker extends StatefulWidget { @required this.onChanged, @required this.firstDate, @required this.lastDate, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(selectedDate != null), assert(onChanged != null), assert(!firstDate.isAfter(lastDate)), @@ -778,6 +807,9 @@ class YearPicker extends StatefulWidget { /// The latest date the user is permitted to pick. final DateTime lastDate; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override _YearPickerState createState() => _YearPickerState(); } @@ -801,6 +833,7 @@ class _YearPickerState extends State { final ThemeData themeData = Theme.of(context); final TextStyle style = themeData.textTheme.body1; return ListView.builder( + dragStartBehavior: widget.dragStartBehavior, controller: scrollController, itemExtent: _itemExtent, itemCount: widget.lastDate.year - widget.firstDate.year + 1, diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 89c4e22187..6a818e99d9 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'colors.dart'; import 'debug.dart'; @@ -179,7 +180,9 @@ class DrawerController extends StatefulWidget { @required this.child, @required this.alignment, this.drawerCallback, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(child != null), + assert(dragStartBehavior != null), assert(alignment != null), super(key: key); @@ -197,6 +200,26 @@ class DrawerController extends StatefulWidget { /// Optional callback that is called when a [Drawer] is opened or closed. final DrawerCallback drawerCallback; + /// {@template flutter.material.drawer.dragStartBehavior} + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag behavior used for opening + /// and closing a drawer will begin upon the detection of a drag gesture. If + /// set to [DragStartBehavior.down] it will begin when a down event is first + /// detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + /// {@endtemplate} + final DragStartBehavior dragStartBehavior; + @override DrawerControllerState createState() => DrawerControllerState(); } @@ -399,6 +422,7 @@ class DrawerControllerState extends State with SingleTickerPro onHorizontalDragEnd: _settle, behavior: HitTestBehavior.translucent, excludeFromSemantics: true, + dragStartBehavior: widget.dragStartBehavior, child: Container(width: dragAreaWidth), ), ); @@ -410,6 +434,7 @@ class DrawerControllerState extends State with SingleTickerPro onHorizontalDragEnd: _settle, onHorizontalDragCancel: _handleDragCancel, excludeFromSemantics: true, + dragStartBehavior: widget.dragStartBehavior, child: RepaintBoundary( child: Stack( children: [ diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index 792eec9e86..cfd0b4c07d 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'button_bar.dart'; import 'button_theme.dart'; @@ -74,9 +75,11 @@ class PaginatedDataTable extends StatefulWidget { this.rowsPerPage = defaultRowsPerPage, this.availableRowsPerPage = const [defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10], this.onRowsPerPageChanged, + this.dragStartBehavior = DragStartBehavior.start, @required this.source }) : assert(header != null), assert(columns != null), + assert(dragStartBehavior != null), assert(columns.isNotEmpty), assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), assert(sortAscending != null), @@ -170,6 +173,9 @@ class PaginatedDataTable extends StatefulWidget { /// [PaginatedDataTable] constructor is called. final DataTableSource source; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override PaginatedDataTableState createState() => PaginatedDataTableState(); } @@ -417,6 +423,7 @@ class PaginatedDataTableState extends State { ), SingleChildScrollView( scrollDirection: Axis.horizontal, + dragStartBehavior: widget.dragStartBehavior, child: DataTable( key: _tableKey, columns: widget.columns, @@ -435,6 +442,7 @@ class PaginatedDataTableState extends State { child: Container( height: 56.0, child: SingleChildScrollView( + dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.horizontal, reverse: true, child: Row( diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index baba3ac4e3..7969a86e07 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'app_bar.dart'; import 'bottom_sheet.dart'; @@ -732,7 +733,10 @@ class Scaffold extends StatefulWidget { this.backgroundColor, this.resizeToAvoidBottomPadding = true, this.primary = true, - }) : assert(primary != null), super(key: key); + this.drawerDragStartBehavior = DragStartBehavior.start, + }) : assert(primary != null), + assert(drawerDragStartBehavior != null), + super(key: key); /// An app bar to display at the top of the scaffold. final PreferredSizeWidget appBar; @@ -865,6 +869,9 @@ class Scaffold extends StatefulWidget { /// [AppBar.primary], is true. final bool primary; + /// {@macro flutter.material.drawer.dragStartBehavior} + final DragStartBehavior drawerDragStartBehavior; + /// The state from the closest instance of this class that encloses the given context. /// /// Typical usage is as follows: @@ -1500,6 +1507,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { alignment: DrawerAlignment.end, child: widget.endDrawer, drawerCallback: _endDrawerOpenedCallback, + dragStartBehavior: widget.drawerDragStartBehavior, ), _ScaffoldSlot.endDrawer, // remove the side padding from the side we're not touching @@ -1521,6 +1529,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { alignment: DrawerAlignment.start, child: widget.drawer, drawerCallback: _drawerOpenedCallback, + dragStartBehavior: widget.drawerDragStartBehavior, ), _ScaffoldSlot.drawer, // remove the side padding from the side we're not touching diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 6bc832c640..183d286a60 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -73,7 +73,9 @@ class Switch extends StatefulWidget { this.activeThumbImage, this.inactiveThumbImage, this.materialTapTargetSize, + this.dragStartBehavior = DragStartBehavior.start, }) : _switchType = _SwitchType.material, + assert(dragStartBehavior != null), super(key: key); /// Creates a [CupertinoSwitch] if the target platform is iOS, creates a @@ -95,6 +97,7 @@ class Switch extends StatefulWidget { this.activeThumbImage, this.inactiveThumbImage, this.materialTapTargetSize, + this.dragStartBehavior = DragStartBehavior.start, }) : _switchType = _SwitchType.adaptive, super(key: key); @@ -174,6 +177,9 @@ class Switch extends StatefulWidget { final _SwitchType _switchType; + /// {@macro flutter.cupertino.switch.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override _SwitchState createState() => _SwitchState(); @@ -219,6 +225,7 @@ class _SwitchState extends State with TickerProviderStateMixin { } return _SwitchRenderObjectWidget( + dragStartBehavior: widget.dragStartBehavior, value: widget.value, activeColor: activeThumbColor, inactiveColor: inactiveThumbColor, @@ -240,6 +247,7 @@ class _SwitchState extends State with TickerProviderStateMixin { height: size.height, alignment: Alignment.center, child: CupertinoSwitch( + dragStartBehavior: widget.dragStartBehavior, value: widget.value, onChanged: widget.onChanged, activeColor: widget.activeColor, @@ -284,6 +292,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { this.onChanged, this.vsync, this.additionalConstraints, + this.dragStartBehavior, }) : super(key: key); final bool value; @@ -297,10 +306,12 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { final ValueChanged onChanged; final TickerProvider vsync; final BoxConstraints additionalConstraints; + final DragStartBehavior dragStartBehavior; @override _RenderSwitch createRenderObject(BuildContext context) { return _RenderSwitch( + dragStartBehavior: dragStartBehavior, value: value, activeColor: activeColor, inactiveColor: inactiveColor, @@ -330,6 +341,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ..onChanged = onChanged ..textDirection = Directionality.of(context) ..additionalConstraints = additionalConstraints + ..dragStartBehavior = dragStartBehavior ..vsync = vsync; } } @@ -348,6 +360,7 @@ class _RenderSwitch extends RenderToggleable { @required TextDirection textDirection, ValueChanged onChanged, @required TickerProvider vsync, + DragStartBehavior dragStartBehavior, }) : assert(textDirection != null), _activeThumbImage = activeThumbImage, _inactiveThumbImage = inactiveThumbImage, @@ -367,7 +380,8 @@ class _RenderSwitch extends RenderToggleable { _drag = HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd; + ..onEnd = _handleDragEnd + ..dragStartBehavior = dragStartBehavior; } ImageProvider get activeThumbImage => _activeThumbImage; @@ -428,6 +442,14 @@ class _RenderSwitch extends RenderToggleable { markNeedsPaint(); } + DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior; + set dragStartBehavior(DragStartBehavior value) { + assert(value != null); + if(_drag.dragStartBehavior == value) + return; + _drag.dragStartBehavior = value; + } + @override void detach() { _cachedThumbPainter?.dispose(); diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index f83130e6cf..79405796ed 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'app_bar.dart'; import 'colors.dart'; @@ -549,9 +550,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, + this.dragStartBehavior = DragStartBehavior.start, this.onTap, }) : assert(tabs != null), assert(isScrollable != null), + assert(dragStartBehavior != null), assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)), assert(indicator != null || (indicatorPadding != null)), super(key: key); @@ -662,6 +665,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// is null then the text style of the theme's body2 definition is used. final TextStyle unselectedLabelStyle; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + /// An optional callback that's called when the [TabBar] is tapped. /// /// The callback is applied to the index of the tab where the tap occurred. @@ -1011,6 +1017,7 @@ class _TabBarState extends State { if (widget.isScrollable) { _scrollController ??= _TabBarScrollController(this); tabBar = SingleChildScrollView( + dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.horizontal, controller: _scrollController, child: tabBar, @@ -1035,7 +1042,10 @@ class TabBarView extends StatefulWidget { @required this.children, this.controller, this.physics, - }) : assert(children != null), super(key: key); + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(children != null), + assert(dragStartBehavior != null), + super(key: key); /// This widget's selection and animation state. /// @@ -1057,6 +1067,9 @@ class TabBarView extends StatefulWidget { /// Defaults to matching platform conventions. final ScrollPhysics physics; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override _TabBarViewState createState() => _TabBarViewState(); } @@ -1201,6 +1214,7 @@ class _TabBarViewState extends State { return NotificationListener( onNotification: _handleScrollNotification, child: PageView( + dragStartBehavior: widget.dragStartBehavior, controller: _pageController, physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics), children: _children, diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 4ca47e4378..d5e374f4e2 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart'; import 'debug.dart'; import 'feedback.dart'; @@ -127,6 +128,7 @@ class TextField extends StatefulWidget { this.cursorColor, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), + this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection, this.onTap, }) : assert(textAlign != null), @@ -135,6 +137,7 @@ class TextField extends StatefulWidget { assert(autocorrect != null), assert(maxLengthEnforced != null), assert(scrollPadding != null), + assert(dragStartBehavior != null), assert(maxLines == null || maxLines > 0), assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), @@ -346,6 +349,9 @@ class TextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + /// {@macro flutter.rendering.editable.selectionEnabled} bool get selectionEnabled { return enableInteractiveSelection ?? !obscureText; @@ -669,6 +675,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi scrollPadding: widget.scrollPadding, keyboardAppearance: keyboardAppearance, enableInteractiveSelection: widget.enableInteractiveSelection, + dragStartBehavior: widget.dragStartBehavior, ), ); diff --git a/packages/flutter/lib/src/widgets/dismissible.dart b/packages/flutter/lib/src/widgets/dismissible.dart index f945da48a5..98a94b7f45 100644 --- a/packages/flutter/lib/src/widgets/dismissible.dart +++ b/packages/flutter/lib/src/widgets/dismissible.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/gestures.dart'; + import 'automatic_keep_alive.dart'; import 'basic.dart'; import 'debug.dart'; @@ -82,8 +84,10 @@ class Dismissible extends StatefulWidget { this.dismissThresholds = const {}, this.movementDuration = const Duration(milliseconds: 200), this.crossAxisEndOffset = 0.0, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(key != null), assert(secondaryBackground != null ? background != null : true), + assert(dragStartBehavior != null), super(key: key); /// The widget below this widget in the tree. @@ -142,6 +146,23 @@ class Dismissible extends StatefulWidget { /// it is positive or negative. final double crossAxisEndOffset; + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a + /// dismissible will begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + @override _DismissibleState createState() => _DismissibleState(); } @@ -327,8 +348,8 @@ class _DismissibleState extends State with TickerProviderStateMixin Tween( begin: Offset.zero, end: _directionIsXAxis - ? Offset(end, widget.crossAxisEndOffset) - : Offset(widget.crossAxisEndOffset, end), + ? Offset(end, widget.crossAxisEndOffset) + : Offset(widget.crossAxisEndOffset, end), ), ); } @@ -514,7 +535,6 @@ class _DismissibleState extends State with TickerProviderStateMixin children.add(content); content = Stack(children: children); } - // We are not resizing but we may be being dragging in widget.direction. return GestureDetector( onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null, @@ -524,7 +544,8 @@ class _DismissibleState extends State with TickerProviderStateMixin onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate, onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd, behavior: HitTestBehavior.opaque, - child: content + child: content, + dragStartBehavior: widget.dragStartBehavior, ); } } diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index bd22473fe4..c8dc527481 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'automatic_keep_alive.dart'; import 'basic.dart'; @@ -184,7 +185,8 @@ class EditableText extends StatefulWidget { /// default to [TextInputType.multiline]. /// /// The [controller], [focusNode], [style], [cursorColor], [backgroundCursorColor], - /// [textAlign], and [rendererIgnoresPointer] arguments must not be null. + /// [textAlign], [dragStartBehavior] and [rendererIgnoresPointer] arguments + /// must not be null. EditableText({ Key key, @required this.controller, @@ -215,6 +217,7 @@ class EditableText extends StatefulWidget { this.cursorRadius, this.scrollPadding = const EdgeInsets.all(20.0), this.keyboardAppearance = Brightness.light, + this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection, }) : assert(controller != null), assert(focusNode != null), @@ -228,6 +231,7 @@ class EditableText extends StatefulWidget { assert(autofocus != null), assert(rendererIgnoresPointer != null), assert(scrollPadding != null), + assert(dragStartBehavior != null), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), inputFormatters = maxLines == 1 ? ( @@ -284,6 +288,10 @@ class EditableText extends StatefulWidget { /// its left. /// /// Defaults to the ambient [Directionality], if any. + /// + /// See also: + /// + /// * {@macro flutter.gestures.monodrag.dragStartExample} /// {@endtemplate} final TextDirection textDirection; @@ -494,6 +502,9 @@ class EditableText extends StatefulWidget { /// Defaults to false, resulting in a typical blinking cursor. static bool debugDeterministicCursor = false; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + /// {@macro flutter.rendering.editable.selectionEnabled} bool get selectionEnabled { return enableInteractiveSelection ?? !obscureText; @@ -840,6 +851,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien renderObject: renderObject, selectionControls: widget.selectionControls, selectionDelegate: this, + dragStartBehavior: widget.dragStartBehavior, ); final bool longPress = cause == SelectionChangedCause.longPress; if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress)) @@ -1061,6 +1073,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, controller: _scrollController, physics: const ClampingScrollPhysics(), + dragStartBehavior: widget.dragStartBehavior, viewportBuilder: (BuildContext context, ViewportOffset offset) { return CompositedTransformTarget( link: _layerLink, diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 158d308b83..119d846174 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -184,8 +184,10 @@ class GestureDetector extends StatelessWidget { this.onScaleUpdate, this.onScaleEnd, this.behavior, - this.excludeFromSemantics = false + this.excludeFromSemantics = false, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(excludeFromSemantics != null), + assert(dragStartBehavior != null), assert(() { final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null; final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null; @@ -370,6 +372,27 @@ class GestureDetector extends StatelessWidget { /// duplication of information. final bool excludeFromSemantics; + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], gesture drag behavior will + /// begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// Only the [onStart] callbacks for the [VerticalDragGestureRecognizer], + /// [HorizontalDragGestureRecognizer] and [PanGestureRecognizer] are affected + /// by this setting. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + @override Widget build(BuildContext context) { final Map gestures = {}; @@ -421,7 +444,8 @@ class GestureDetector extends StatelessWidget { ..onStart = onVerticalDragStart ..onUpdate = onVerticalDragUpdate ..onEnd = onVerticalDragEnd - ..onCancel = onVerticalDragCancel; + ..onCancel = onVerticalDragCancel + ..dragStartBehavior = dragStartBehavior; }, ); } @@ -439,7 +463,8 @@ class GestureDetector extends StatelessWidget { ..onStart = onHorizontalDragStart ..onUpdate = onHorizontalDragUpdate ..onEnd = onHorizontalDragEnd - ..onCancel = onHorizontalDragCancel; + ..onCancel = onHorizontalDragCancel + ..dragStartBehavior = dragStartBehavior; }, ); } @@ -457,7 +482,8 @@ class GestureDetector extends StatelessWidget { ..onStart = onPanStart ..onUpdate = onPanUpdate ..onEnd = onPanEnd - ..onCancel = onPanCancel; + ..onCancel = onPanCancel + ..dragStartBehavior = dragStartBehavior; }, ); } @@ -497,6 +523,11 @@ class GestureDetector extends StatelessWidget { child: child, ); } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('startBehavior', dragStartBehavior)); + } } /// A widget that detects gestures described by the given gesture @@ -550,7 +581,7 @@ class RawGestureDetector extends StatefulWidget { this.child, this.gestures = const {}, this.behavior, - this.excludeFromSemantics = false + this.excludeFromSemantics = false, }) : assert(gestures != null), assert(excludeFromSemantics != null), super(key: key); diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 8d410744a5..4369334f78 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/painting.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'basic.dart'; import 'framework.dart'; @@ -51,7 +52,7 @@ typedef NestedScrollViewHeaderSliversBuilder = List Function(BuildContex /// in the opposite direction (e.g. allowing the user to swipe horizontally /// between the pages represented by the tabs, while the list scrolls /// vertically), then any list inside that [TabBarView] would not interact with -/// the outer [ScrollView]. For example, flinging the inner list to scroll to +/// the outer [ScrollView]. For example, flinginsg the inner list to scroll to /// the top would not cause a collapsed [SliverAppBar] in the outer [ScrollView] /// to expand. /// @@ -188,6 +189,7 @@ class NestedScrollView extends StatefulWidget { this.physics, @required this.headerSliverBuilder, @required this.body, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(scrollDirection != null), assert(reverse != null), assert(headerSliverBuilder != null), @@ -252,6 +254,9 @@ class NestedScrollView extends StatefulWidget { /// the [PrimaryScrollController] provided by the [NestedScrollView]. final Widget body; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + /// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor /// [NestedScrollView]. /// @@ -338,6 +343,7 @@ class _NestedScrollViewState extends State { builder: (BuildContext context) { _lastHasScrolledBody = _coordinator.hasScrolledBody; return _NestedScrollViewCustomScrollView( + dragStartBehavior: widget.dragStartBehavior, scrollDirection: widget.scrollDirection, reverse: widget.reverse, physics: widget.physics != null @@ -365,12 +371,14 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView { @required ScrollController controller, @required List slivers, @required this.handle, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : super( scrollDirection: scrollDirection, reverse: reverse, physics: physics, controller: controller, slivers: slivers, + dragStartBehavior: dragStartBehavior, ); final SliverOverlapAbsorberHandle handle; diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 371cd58b8d..a46f22e4b7 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -7,6 +7,7 @@ import 'dart:math' as math; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'basic.dart'; import 'debug.dart'; @@ -423,6 +424,7 @@ class PageView extends StatefulWidget { this.pageSnapping = true, this.onPageChanged, List children = const [], + this.dragStartBehavior = DragStartBehavior.start, }) : controller = controller ?? _defaultPageController, childrenDelegate = SliverChildListDelegate(children), super(key: key); @@ -449,6 +451,7 @@ class PageView extends StatefulWidget { this.onPageChanged, @required IndexedWidgetBuilder itemBuilder, int itemCount, + this.dragStartBehavior = DragStartBehavior.start, }) : controller = controller ?? _defaultPageController, childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super(key: key); @@ -464,6 +467,7 @@ class PageView extends StatefulWidget { this.pageSnapping = true, this.onPageChanged, @required this.childrenDelegate, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(childrenDelegate != null), controller = controller ?? _defaultPageController, super(key: key); @@ -516,6 +520,9 @@ class PageView extends StatefulWidget { /// respectively. final SliverChildDelegate childrenDelegate; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + @override _PageViewState createState() => _PageViewState(); } @@ -562,6 +569,7 @@ class _PageViewState extends State { return false; }, child: Scrollable( + dragStartBehavior: widget.dragStartBehavior, axisDirection: axisDirection, controller: widget.controller, physics: physics, diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index f4575e629d..7e701a5448 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; +import 'package:flutter/gestures.dart'; import 'basic.dart'; import 'framework.dart'; @@ -60,8 +61,10 @@ abstract class ScrollView extends StatelessWidget { this.shrinkWrap = false, this.cacheExtent, this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(reverse != null), assert(shrinkWrap != null), + assert(dragStartBehavior != null), assert(!(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.' @@ -187,6 +190,9 @@ abstract class ScrollView extends StatelessWidget { /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. final int semanticChildCount; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + /// Returns the [AxisDirection] in which the scroll view scrolls. /// /// Combines the [scrollDirection] with the [reverse] boolean to obtain the @@ -246,6 +252,7 @@ abstract class ScrollView extends StatelessWidget { ? PrimaryScrollController.of(context) : controller; final Scrollable scrollable = Scrollable( + dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, physics: physics, @@ -397,6 +404,7 @@ class CustomScrollView extends ScrollView { double cacheExtent, this.slivers = const [], int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : super( key: key, scrollDirection: scrollDirection, @@ -407,6 +415,7 @@ class CustomScrollView extends ScrollView { shrinkWrap: shrinkWrap, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, ); /// The slivers to place inside the viewport. @@ -439,6 +448,7 @@ abstract class BoxScrollView extends ScrollView { this.padding, double cacheExtent, int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : super( key: key, scrollDirection: scrollDirection, @@ -449,6 +459,7 @@ abstract class BoxScrollView extends ScrollView { shrinkWrap: shrinkWrap, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, ); /// The amount of space by which to inset the children. @@ -739,6 +750,7 @@ class ListView extends BoxScrollView { double cacheExtent, List children = const [], int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : childrenDelegate = SliverChildListDelegate( children, addAutomaticKeepAlives: addAutomaticKeepAlives, @@ -755,6 +767,7 @@ class ListView extends BoxScrollView { padding: padding, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount ?? children.length, + dragStartBehavior: dragStartBehavior, ); /// Creates a scrollable, linear array of widgets that are created on demand. @@ -800,6 +813,7 @@ class ListView extends BoxScrollView { bool addSemanticIndexes = true, double cacheExtent, int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : childrenDelegate = SliverChildBuilderDelegate( itemBuilder, childCount: itemCount, @@ -817,6 +831,7 @@ class ListView extends BoxScrollView { padding: padding, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount ?? itemCount, + dragStartBehavior: dragStartBehavior, ); /// Creates a fixed-length scrollable linear array of list "items" separated @@ -1250,6 +1265,7 @@ class GridView extends BoxScrollView { @required this.childrenDelegate, double cacheExtent, int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : assert(gridDelegate != null), assert(childrenDelegate != null), super( @@ -1263,6 +1279,7 @@ class GridView extends BoxScrollView { padding: padding, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, ); /// Creates a scrollable, 2D array of widgets with a fixed number of tiles in @@ -1298,6 +1315,7 @@ class GridView extends BoxScrollView { double cacheExtent, List children = const [], int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: mainAxisSpacing, @@ -1320,6 +1338,7 @@ class GridView extends BoxScrollView { padding: padding, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount ?? children.length, + dragStartBehavior: dragStartBehavior, ); /// Creates a scrollable, 2D array of widgets with tiles that each have a @@ -1354,6 +1373,7 @@ class GridView extends BoxScrollView { bool addSemanticIndexes = true, List children = const [], int semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, mainAxisSpacing: mainAxisSpacing, @@ -1375,6 +1395,7 @@ class GridView extends BoxScrollView { shrinkWrap: shrinkWrap, padding: padding, semanticChildCount: semanticChildCount ?? children.length, + dragStartBehavior: dragStartBehavior, ); /// A delegate that controls the layout of the children within the [GridView]. diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index d6768c05a5..42b91a9747 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -81,7 +81,9 @@ class Scrollable extends StatefulWidget { @required this.viewportBuilder, this.excludeFromSemantics = false, this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(axisDirection != null), + assert(dragStartBehavior != null), assert(viewportBuilder != null), assert(excludeFromSemantics != null), super (key: key); @@ -180,6 +182,25 @@ class Scrollable extends StatefulWidget { /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. final int semanticChildCount; + /// {@template flutter.widgets.scrollable.dragStartBehavior} + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], scrolling drag behavior will + /// begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + /// {@endtemplate} + final DragStartBehavior dragStartBehavior; + /// The axis along which the scroll view scrolls. /// /// Determined by the [axisDirection]. @@ -391,7 +412,8 @@ class ScrollableState extends State with TickerProviderStateMixin ..onCancel = _handleDragCancel ..minFlingDistance = _physics?.minFlingDistance ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity; + ..maxFlingVelocity = _physics?.maxFlingVelocity + ..dragStartBehavior = widget.dragStartBehavior; }, ), }; @@ -409,7 +431,8 @@ class ScrollableState extends State with TickerProviderStateMixin ..onCancel = _handleDragCancel ..minFlingDistance = _physics?.minFlingDistance ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity; + ..maxFlingVelocity = _physics?.maxFlingVelocity + ..dragStartBehavior = widget.dragStartBehavior; }, ), }; diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index 2520283ec0..dbd039928a 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'basic.dart'; import 'framework.dart'; @@ -192,7 +193,9 @@ class SingleChildScrollView extends StatelessWidget { this.physics, this.controller, this.child, + this.dragStartBehavior = DragStartBehavior.start, }) : assert(scrollDirection != null), + assert(dragStartBehavior != null), assert(!(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.' @@ -259,6 +262,9 @@ class SingleChildScrollView extends StatelessWidget { /// {@macro flutter.widgets.child} final Widget child; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); } @@ -273,6 +279,7 @@ class SingleChildScrollView extends StatelessWidget { ? PrimaryScrollController.of(context) : controller; final Scrollable scrollable = Scrollable( + dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, physics: physics, diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 7af804fda1..5d8110e927 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -8,6 +8,7 @@ import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'basic.dart'; import 'container.dart'; @@ -229,6 +230,7 @@ class TextSelectionOverlay { @required this.renderObject, this.selectionControls, this.selectionDelegate, + this.dragStartBehavior = DragStartBehavior.start, }): assert(value != null), assert(context != null), _value = value { @@ -263,6 +265,23 @@ class TextSelectionOverlay { /// text field. final TextSelectionDelegate selectionDelegate; + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], handle drag behavior will + /// begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + /// Controls the fade-in animations. static const Duration _fadeDuration = Duration(milliseconds: 150); AnimationController _handleController; @@ -365,9 +384,8 @@ class TextSelectionOverlay { Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) { if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) || - selectionControls == null) + selectionControls == null) return Container(); // hide the second handle when collapsed - return FadeTransition( opacity: _handleOpacity, child: _TextSelectionHandleOverlay( @@ -378,6 +396,7 @@ class TextSelectionOverlay { selection: _selection, selectionControls: selectionControls, position: position, + dragStartBehavior: dragStartBehavior, ) ); } @@ -447,7 +466,8 @@ class _TextSelectionHandleOverlay extends StatefulWidget { @required this.renderObject, @required this.onSelectionHandleChanged, @required this.onSelectionHandleTapped, - @required this.selectionControls + @required this.selectionControls, + this.dragStartBehavior = DragStartBehavior.start, }) : super(key: key); final TextSelection selection; @@ -457,6 +477,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { final ValueChanged onSelectionHandleChanged; final VoidCallback onSelectionHandleTapped; final TextSelectionControls selectionControls; + final DragStartBehavior dragStartBehavior; @override _TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState(); @@ -528,6 +549,7 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay link: widget.layerLink, showWhenUnlinked: false, child: GestureDetector( + dragStartBehavior: widget.dragStartBehavior, onPanStart: _handleDragStart, onPanUpdate: _handleDragUpdate, onTap: _handleTap, diff --git a/packages/flutter/test/cupertino/switch_test.dart b/packages/flutter/test/cupertino/switch_test.dart index c08ea4ab49..08f4a787d6 100644 --- a/packages/flutter/test/cupertino/switch_test.dart +++ b/packages/flutter/test/cupertino/switch_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart'; void main() { testWidgets('Switch can toggle on tap', (WidgetTester tester) async { @@ -18,6 +19,7 @@ void main() { child: CupertinoSwitch( key: switchKey, value: value, + dragStartBehavior: DragStartBehavior.down, onChanged: (bool newValue) { setState(() { value = newValue; @@ -46,6 +48,7 @@ void main() { return Center( child: CupertinoSwitch( value: value, + dragStartBehavior: DragStartBehavior.down, onChanged: (bool newValue) { setState(() { value = newValue; @@ -79,6 +82,88 @@ void main() { expect(value, isFalse); }); + testWidgets('Switch can drag with dragStartBehavior', (WidgetTester tester) async { + bool value = false; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Center( + child: CupertinoSwitch( + value: value, + dragStartBehavior: DragStartBehavior.down, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + }, + ), + ); + }, + ), + ), + ); + + expect(value, isFalse); + await tester.drag(find.byType(CupertinoSwitch), const Offset(-30.0, 0.0)); + expect(value, isFalse); + + await tester.drag(find.byType(CupertinoSwitch), const Offset(30.0, 0.0)); + expect(value, isTrue); + await tester.pump(); + await tester.drag(find.byType(CupertinoSwitch), const Offset(30.0, 0.0)); + expect(value, isTrue); + await tester.pump(); + await tester.drag(find.byType(CupertinoSwitch), const Offset(-30.0, 0.0)); + expect(value, isFalse); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Center( + child: CupertinoSwitch( + value: value, + dragStartBehavior: DragStartBehavior.start, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + }, + ), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + final Rect switchRect = tester.getRect(find.byType(CupertinoSwitch)); + + TestGesture gesture = await tester.startGesture(switchRect.center); + // We have to execute the drag in two frames because the first update will + // just set the start position. + await gesture.moveBy(const Offset(20.0, 0.0)); + await gesture.moveBy(const Offset(20.0, 0.0)); + expect(value, isTrue); + await gesture.up(); + await tester.pump(); + + gesture = await tester.startGesture(switchRect.center); + await gesture.moveBy(const Offset(20.0, 0.0)); + await gesture.moveBy(const Offset(20.0, 0.0)); + expect(value, isTrue); + await gesture.up(); + await tester.pump(); + + gesture = await tester.startGesture(switchRect.center); + await gesture.moveBy(const Offset(-20.0, 0.0)); + await gesture.moveBy(const Offset(-20.0, 0.0)); + expect(value, isFalse); + }); + testWidgets('Switch can drag (RTL)', (WidgetTester tester) async { bool value = false; @@ -90,6 +175,7 @@ void main() { return Center( child: CupertinoSwitch( value: value, + dragStartBehavior: DragStartBehavior.down, onChanged: (bool newValue) { setState(() { value = newValue; diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index 4d9946669b..8349c0bcd7 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -58,8 +58,7 @@ void main() { tester.route(pointer.move(const Offset(20.0, 30.0))); // moved 10 horizontally and 20 vertically which is 22 total expect(didStartPan, isTrue); // 22 > 18 didStartPan = false; - expect(updatedScrollDelta, const Offset(10.0, 20.0)); - updatedScrollDelta = null; + expect(updatedScrollDelta, null); expect(didEndPan, isFalse); expect(didTap, isFalse); @@ -82,7 +81,7 @@ void main() { }); testGesture('Should recognize drag', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; bool didStartDrag = false; drag.onStart = (_) { @@ -135,7 +134,7 @@ void main() { }); testGesture('Should report original timestamps', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; Duration startTimestamp; drag.onStart = (DragStartDetails details) { @@ -165,9 +164,98 @@ void main() { drag.dispose(); }); - testGesture('Drag with multiple pointers', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag1 = HorizontalDragGestureRecognizer(); - final VerticalDragGestureRecognizer drag2 = VerticalDragGestureRecognizer(); + testGesture('Should report most recent point to onStart by default', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer(); + + Offset positionAtOnStart; + drag.onStart = (DragStartDetails details) { + positionAtOnStart = details.globalPosition; + }; + + final TestPointer pointer = TestPointer(5); + final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); + drag.addPointer(down); + competingDrag.addPointer(down); + tester.closeArena(5); + tester.route(down); + + tester.route(pointer.move(const Offset(30.0, 0.0))); + drag.dispose(); + competingDrag.dispose(); + + expect(positionAtOnStart, const Offset(30.0, 00.0)); + }); + + testGesture('Should report most recent point to onStart with a start configuration', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag = + HorizontalDragGestureRecognizer(); + final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer(); + + Offset positionAtOnStart; + drag.onStart = (DragStartDetails details) { + positionAtOnStart = details.globalPosition; + }; + Offset updateOffset; + drag.onUpdate = (DragUpdateDetails details) { + updateOffset = details.globalPosition; + }; + + final TestPointer pointer = TestPointer(5); + final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); + drag.addPointer(down); + competingDrag.addPointer(down); + tester.closeArena(5); + tester.route(down); + + tester.route(pointer.move(const Offset(30.0, 0.0))); + drag.dispose(); + competingDrag.dispose(); + + expect(positionAtOnStart, const Offset(30.0, 0.0)); + expect(updateOffset, null); + }); + + testGesture('Should report initial down point to onStart with a down configuration', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag = + HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; + final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; + + Offset positionAtOnStart; + drag.onStart = (DragStartDetails details) { + positionAtOnStart = details.globalPosition; + }; + Offset updateOffset; + Offset updateDelta; + drag.onUpdate = (DragUpdateDetails details) { + updateOffset = details.globalPosition; + updateDelta = details.delta; + }; + + final TestPointer pointer = TestPointer(5); + final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); + drag.addPointer(down); + competingDrag.addPointer(down); + tester.closeArena(5); + tester.route(down); + + tester.route(pointer.move(const Offset(30.0, 0.0))); + drag.dispose(); + competingDrag.dispose(); + + expect(positionAtOnStart, const Offset(10.0, 10.0)); + + // The drag is horizontal so we're going to ignore the vertical delta position + // when calculating the new global position. + expect(updateOffset, const Offset(30.0, 10.0)); + expect(updateDelta, const Offset(20.0, 0.0)); + }); + + testGesture('Drag with multiple pointers in down behavior', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag1 = + HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; + final VerticalDragGestureRecognizer drag2 = + VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; final List log = []; drag1.onDown = (_) { log.add('drag1-down'); }; @@ -235,7 +323,7 @@ void main() { }); testGesture('Clamp max velocity', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; Velocity velocity; double primaryVelocity; @@ -269,7 +357,7 @@ void main() { }); testGesture('Synthesized pointer events are ignored for velocity tracking', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; Velocity velocity; drag.onEnd = (DragEndDetails details) { @@ -303,7 +391,7 @@ void main() { /// Checks that quick flick gestures with 1 down, 2 move and 1 up pointer /// events still have a velocity testGesture('Quick flicks have velocity', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; Velocity velocity; drag.onEnd = (DragEndDetails details) { @@ -333,16 +421,18 @@ void main() { }); testGesture('Should recognize drag', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; bool didStartDrag = false; drag.onStart = (_) { didStartDrag = true; }; - double updatedDelta; + Offset updateDelta; + double updatePrimaryDelta; drag.onUpdate = (DragUpdateDetails details) { - updatedDelta = details.primaryDelta; + updateDelta = details.delta; + updatePrimaryDelta = details.primaryDelta; }; bool didEndDrag = false; @@ -354,31 +444,39 @@ void main() { final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); drag.addPointer(down); tester.closeArena(5); + expect(didStartDrag, isFalse); - expect(updatedDelta, isNull); + expect(updateDelta, isNull); + expect(updatePrimaryDelta, isNull); expect(didEndDrag, isFalse); tester.route(down); expect(didStartDrag, isTrue); - expect(updatedDelta, isNull); + expect(updateDelta, isNull); + expect(updatePrimaryDelta, isNull); expect(didEndDrag, isFalse); - - tester.route(pointer.move(const Offset(20.0, 25.0))); - expect(didStartDrag, isTrue); didStartDrag = false; - expect(updatedDelta, 10.0); - updatedDelta = null; - expect(didEndDrag, isFalse); tester.route(pointer.move(const Offset(20.0, 25.0))); expect(didStartDrag, isFalse); - expect(updatedDelta, 0.0); - updatedDelta = null; + expect(updateDelta, const Offset(10.0, 0.0)); + expect(updatePrimaryDelta, 10.0); expect(didEndDrag, isFalse); + updateDelta = null; + updatePrimaryDelta = null; + + tester.route(pointer.move(const Offset(20.0, 25.0))); + expect(didStartDrag, isFalse); + expect(updateDelta, const Offset(0.0, 0.0)); + expect(updatePrimaryDelta, 0.0); + expect(didEndDrag, isFalse); + updateDelta = null; + updatePrimaryDelta = null; tester.route(pointer.up()); expect(didStartDrag, isFalse); - expect(updatedDelta, isNull); + expect(updateDelta, isNull); + expect(updatePrimaryDelta, isNull); expect(didEndDrag, isTrue); didEndDrag = false; @@ -386,21 +484,36 @@ void main() { }); testGesture('Should recognize drag', (GestureTester tester) { - final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; - Offset newGlobalPosition; + Offset latestGlobalPosition; + drag.onStart = (DragStartDetails details) { + latestGlobalPosition = details.globalPosition; + }; + Offset latestDelta; drag.onUpdate = (DragUpdateDetails details) { - newGlobalPosition = details.globalPosition; + latestGlobalPosition = details.globalPosition; + latestDelta = details.delta; }; final TestPointer pointer = TestPointer(5); final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); drag.addPointer(down); - tester.route(pointer.move(const Offset(20.0, 25.0))); tester.closeArena(5); + tester.route(down); - expect(newGlobalPosition, const Offset(20.0, 10.0)); + expect(latestGlobalPosition, const Offset(10.0, 10.0)); + expect(latestDelta, isNull); + + tester.route(pointer.move(const Offset(20.0, 25.0))); + expect(latestGlobalPosition, const Offset(20.0, 25.0)); + expect(latestDelta, const Offset(10.0, 0.0)); + + tester.route(pointer.move(const Offset(0.0, 45.0))); + expect(latestGlobalPosition, const Offset(0.0, 45.0)); + expect(latestDelta, const Offset(-20.0, 0.0)); + tester.route(pointer.up()); drag.dispose(); }); -} +} \ No newline at end of file diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index c3baedbd46..34a8950b6b 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -46,8 +47,10 @@ void _tests() { return Container( width: 400.0, child: SingleChildScrollView( + dragStartBehavior: DragStartBehavior.down, child: Material( child: MonthPicker( + dragStartBehavior: DragStartBehavior.down, firstDate: DateTime(0), lastDate: DateTime(9999), key: _datePickerKey, @@ -63,7 +66,7 @@ void _tests() { ); }, ), - ) + ), ); expect(_selectedDate, equals(DateTime(2016, DateTime.july, 26))); diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index 0ae9410327..e89d1baf1f 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; @@ -164,6 +165,7 @@ void main() { link: LayerLink(), child: ListView( addAutomaticKeepAlives: keepAlive, + dragStartBehavior: DragStartBehavior.down, children: [ Container(height: 500.0, child: InkWell(onTap: () { }, child: const Placeholder())), Container(height: 500.0), diff --git a/packages/flutter/test/material/paginated_data_table_test.dart b/packages/flutter/test/material/paginated_data_table_test.dart index 9221a6be44..c1d833e20e 100644 --- a/packages/flutter/test/material/paginated_data_table_test.dart +++ b/packages/flutter/test/material/paginated_data_table_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'data_table_test_utils.dart'; @@ -248,26 +249,29 @@ void main() { testWidgets('PaginatedDataTable footer scrolls', (WidgetTester tester) async { final TestDataSource source = TestDataSource(); - await tester.pumpWidget(MaterialApp( - home: Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 100.0, - child: PaginatedDataTable( - header: const Text('HEADER'), - source: source, - rowsPerPage: 5, - availableRowsPerPage: const [ 5 ], - onRowsPerPageChanged: (int rowsPerPage) { }, - columns: const [ - DataColumn(label: Text('COL1')), - DataColumn(label: Text('COL2')), - DataColumn(label: Text('COL3')), - ], + await tester.pumpWidget( + MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 100.0, + child: PaginatedDataTable( + header: const Text('HEADER'), + source: source, + rowsPerPage: 5, + dragStartBehavior: DragStartBehavior.down, + availableRowsPerPage: const [ 5 ], + onRowsPerPageChanged: (int rowsPerPage) { }, + columns: const [ + DataColumn(label: Text('COL1')), + DataColumn(label: Text('COL2')), + DataColumn(label: Text('COL3')), + ], + ), ), ), ), - )); + ); expect(find.text('Rows per page:'), findsOneWidget); expect(tester.getTopLeft(find.text('Rows per page:')).dx, lessThan(0.0)); // off screen await tester.dragFrom( diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index f8d87d3840..d1046014f9 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../widgets/semantics_tester.dart'; @@ -201,6 +202,7 @@ void main() { drawer: Drawer( key: drawerKey, child: ListView( + dragStartBehavior: DragStartBehavior.down, controller: scrollOffset, children: List.generate(10, (int index) => SizedBox(height: 100.0, child: Text('D$index')) @@ -630,6 +632,7 @@ void main() { viewInsets: EdgeInsets.only(bottom: 200.0), ), child: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, appBar: PreferredSize( preferredSize: const Size(11.0, 13.0), child: Container( diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index 59a7c5f827..84b8bebc04 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; @@ -25,6 +25,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, key: switchKey, value: value, onChanged: (bool newValue) { @@ -54,6 +55,7 @@ void main() { child: Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (bool newValue) {}, ), @@ -73,6 +75,7 @@ void main() { child: Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (bool newValue) {}, ), @@ -96,6 +99,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { @@ -131,6 +135,93 @@ void main() { expect(value, isFalse); }); + testWidgets('Switch can drag with dragStartBehavior', (WidgetTester tester) async { + bool value = false; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Material( + child: Center( + child: Switch( + dragStartBehavior: DragStartBehavior.down, + value: value, + onChanged: (bool newValue) { + setState(() { + value = newValue; + } + ); + } + ), + ), + ); + }, + ), + ), + ); + + expect(value, isFalse); + await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); + expect(value, isFalse); + + await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); + expect(value, isTrue); + await tester.pump(); + await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); + expect(value, isTrue); + await tester.pump(); + await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); + expect(value, isFalse); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Material( + child: Center( + child: Switch( + dragStartBehavior: DragStartBehavior.start, + value: value, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + } + ), + ), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + final Rect switchRect = tester.getRect(find.byType(Switch)); + + TestGesture gesture = await tester.startGesture(switchRect.center); + // We have to execute the drag in two frames because the first update will + // just set the start position. + await gesture.moveBy(const Offset(20.0, 0.0)); + await gesture.moveBy(const Offset(20.0, 0.0)); + expect(value, isTrue); + await gesture.up(); + await tester.pump(); + + gesture = await tester.startGesture(switchRect.center); + await gesture.moveBy(const Offset(20.0, 0.0)); + await gesture.moveBy(const Offset(20.0, 0.0)); + expect(value, isTrue); + await gesture.up(); + await tester.pump(); + + gesture = await tester.startGesture(switchRect.center); + await gesture.moveBy(const Offset(-20.0, 0.0)); + await gesture.moveBy(const Offset(-20.0, 0.0)); + expect(value, isFalse); + }); + testWidgets('Switch can drag (RTL)', (WidgetTester tester) async { bool value = false; @@ -142,6 +233,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { @@ -185,6 +277,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { @@ -306,6 +399,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { @@ -365,6 +459,7 @@ void main() { return Material( child: Center( child: Switch( + dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 3e1327b326..59b90ca999 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -501,9 +502,12 @@ void main() { final TextEditingController controller = TextEditingController(); await tester.pumpWidget( - overlay( - child: TextField( - controller: controller, + MaterialApp( + home: Material( + child: TextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + ), ), ), ); @@ -542,7 +546,7 @@ void main() { await tester.pump(); expect(controller.selection.baseOffset, selection.baseOffset); - expect(controller.selection.extentOffset, selection.extentOffset+2); + expect(controller.selection.extentOffset, selection.extentOffset); // Drag the left handle 2 letters to the left. handlePos = endpoints[0].point + const Offset(-1.0, 1.0); @@ -554,8 +558,8 @@ void main() { await gesture.up(); await tester.pump(); - expect(controller.selection.baseOffset, selection.baseOffset-2); - expect(controller.selection.extentOffset, selection.extentOffset+2); + expect(controller.selection.baseOffset, selection.baseOffset); + expect(controller.selection.extentOffset, selection.extentOffset); }); testWidgets('Can use selection toolbar', (WidgetTester tester) async { @@ -826,6 +830,7 @@ void main() { await tester.pumpWidget( overlay( child: TextField( + dragStartBehavior: DragStartBehavior.down, controller: controller, style: const TextStyle(color: Colors.black, fontSize: 34.0), maxLines: 3, @@ -909,6 +914,7 @@ void main() { await tester.pumpWidget( overlay( child: TextField( + dragStartBehavior: DragStartBehavior.down, key: textFieldKey, controller: controller, style: const TextStyle(color: Colors.black, fontSize: 34.0), diff --git a/packages/flutter/test/widgets/automatic_keep_alive_test.dart b/packages/flutter/test/widgets/automatic_keep_alive_test.dart index 404e633992..68bfe86466 100644 --- a/packages/flutter/test/widgets/automatic_keep_alive_test.dart +++ b/packages/flutter/test/widgets/automatic_keep_alive_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; class Leaf extends StatefulWidget { const Leaf({ Key key, this.child }) : super(key: key); @@ -476,6 +477,7 @@ void main() { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( + dragStartBehavior: DragStartBehavior.down, addSemanticIndexes: false, itemCount: 50, itemBuilder: (BuildContext context, int index){ diff --git a/packages/flutter/test/widgets/dismissible_test.dart b/packages/flutter/test/widgets/dismissible_test.dart index 420fdcc0ea..9623121b93 100644 --- a/packages/flutter/test/widgets/dismissible_test.dart +++ b/packages/flutter/test/widgets/dismissible_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; const double itemExtent = 100.0; Axis scrollDirection = Axis.vertical; @@ -21,6 +22,7 @@ Widget buildTest({ double startToEndThreshold, TextDirection textDirection = Tex builder: (BuildContext context, StateSetter setState) { Widget buildDismissibleItem(int item) { return Dismissible( + dragStartBehavior: DragStartBehavior.down, key: ValueKey(item), direction: dismissDirection, onDismissed: (DismissDirection direction) { @@ -49,6 +51,7 @@ Widget buildTest({ double startToEndThreshold, TextDirection textDirection = Tex return Container( padding: const EdgeInsets.all(10.0), child: ListView( + dragStartBehavior: DragStartBehavior.down, scrollDirection: scrollDirection, itemExtent: itemExtent, children: [0, 1, 2, 3, 4] @@ -199,6 +202,7 @@ class Test1215DismissibleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Dismissible( + dragStartBehavior: DragStartBehavior.down, key: ObjectKey(text), child: AspectRatio( aspectRatio: 1.0, diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 97941fce20..ef8e964919 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -383,6 +383,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: ListView( + dragStartBehavior: DragStartBehavior.down, children: [ DragTarget( builder: (BuildContext context, List data, List rejects) { @@ -489,6 +490,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: ListView( + dragStartBehavior: DragStartBehavior.down, scrollDirection: Axis.horizontal, children: [ DragTarget( diff --git a/packages/flutter/test/widgets/drawer_test.dart b/packages/flutter/test/widgets/drawer_test.dart index 30868b09ba..5dc9a7f6f2 100644 --- a/packages/flutter/test/widgets/drawer_test.dart +++ b/packages/flutter/test/widgets/drawer_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'semantics_tester.dart'; @@ -82,6 +83,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, key: scaffoldKey, drawer: Drawer( child: ListView( @@ -134,6 +136,7 @@ void main() { home: Directionality( textDirection: TextDirection.rtl, child: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, key: scaffoldKey, drawer: Drawer( child: ListView( diff --git a/packages/flutter/test/widgets/gesture_detector_test.dart b/packages/flutter/test/widgets/gesture_detector_test.dart index f7acb45d5d..922126bf9a 100644 --- a/packages/flutter/test/widgets/gesture_detector_test.dart +++ b/packages/flutter/test/widgets/gesture_detector_test.dart @@ -64,6 +64,7 @@ void main() { const Offset upLocation = Offset(10.0, 50.0); // must be far enough to be more than kTouchSlop final Widget widget = GestureDetector( + dragStartBehavior: DragStartBehavior.down, onVerticalDragUpdate: (DragUpdateDetails details) { dragDistance += details.primaryDelta; }, onVerticalDragEnd: (DragEndDetails details) { gestureCount += 1; }, onHorizontalDragUpdate: (DragUpdateDetails details) { fail('gesture should not match'); }, diff --git a/packages/flutter/test/widgets/grid_view_test.dart b/packages/flutter/test/widgets/grid_view_test.dart index 38b478630f..3938ac14d4 100644 --- a/packages/flutter/test/widgets/grid_view_test.dart +++ b/packages/flutter/test/widgets/grid_view_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../rendering/mock_canvas.dart'; import 'states.dart'; @@ -14,6 +15,7 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: GridView.count( + dragStartBehavior: DragStartBehavior.down, crossAxisCount: 4, children: const [], ), @@ -28,9 +30,11 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: GridView.count( + dragStartBehavior: DragStartBehavior.down, crossAxisCount: 4, children: kStates.map((String state) { return GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, @@ -99,9 +103,11 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: GridView.extent( + dragStartBehavior: DragStartBehavior.down, maxCrossAxisExtent: 200.0, children: kStates.map((String state) { return GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index e33291b4bf..23e6460460 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -1373,8 +1373,11 @@ void main() { expect(find.byKey(secondKey), isOnstage); expect(find.byKey(secondKey), isInCard); - final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); - await gesture.moveBy(const Offset(200.0, 0.0)); + final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); + await gesture.moveBy(const Offset(20.0, 0.0)); + await gesture.moveBy(const Offset(180.0, 0.0)); + await gesture.up(); + await tester.pump(); await tester.pump(); diff --git a/packages/flutter/test/widgets/listview_end_append_test.dart b/packages/flutter/test/widgets/listview_end_append_test.dart index 0f9f1f4bee..e09969e9c2 100644 --- a/packages/flutter/test/widgets/listview_end_append_test.dart +++ b/packages/flutter/test/widgets/listview_end_append_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; void main() { testWidgets('ListView.builder() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { @@ -13,6 +14,7 @@ void main() { return Directionality( textDirection: TextDirection.ltr, child: ListView.builder( + dragStartBehavior: DragStartBehavior.down, itemExtent: 200.0, itemCount: itemCount, itemBuilder: (BuildContext context, int index) => Text('item $index'), @@ -40,6 +42,7 @@ void main() { return Directionality( textDirection: TextDirection.ltr, child: ListView.builder( + dragStartBehavior: DragStartBehavior.down, itemCount: itemCount, itemBuilder: (BuildContext context, int index) { return SizedBox( diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index b52360bc2a..354126d9a7 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import '../rendering/mock_canvas.dart'; @@ -34,9 +35,11 @@ Widget buildTest({ ScrollController controller, String title ='TTTTTTTT' }) { child: MediaQuery( data: const MediaQueryData(), child: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, body: DefaultTabController( length: 4, child: NestedScrollView( + dragStartBehavior: DragStartBehavior.down, controller: controller, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ @@ -79,6 +82,7 @@ Widget buildTest({ ScrollController controller, String title ='TTTTTTTT' }) { ], ), ListView( + dragStartBehavior: DragStartBehavior.down, children: [ Container( height: 100.0, @@ -90,6 +94,7 @@ Widget buildTest({ ScrollController controller, String title ='TTTTTTTT' }) { child: const Center(child: Text('ccc1')), ), ListView( + dragStartBehavior: DragStartBehavior.down, children: [ Container( height: 10000.0, @@ -361,6 +366,7 @@ void main() { DefaultTabController( length: _tabs.length, // This is the number of tabs. child: NestedScrollView( + dragStartBehavior: DragStartBehavior.down, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { buildCount += 1; // THIS LINE IS NOT IN THE ORIGINAL -- ADDED FOR TEST // These are the slivers that show up in the "outer" scroll view. @@ -390,12 +396,14 @@ void main() { bottom: TabBar( // These are the widgets to put in each tab in the tab bar. tabs: _tabs.map((String name) => Tab(text: name)).toList(), + dragStartBehavior: DragStartBehavior.down, ), ), ), ]; }, body: TabBarView( + dragStartBehavior: DragStartBehavior.down, // These are the contents of the tab views, below the tabs. children: _tabs.map((String name) { return SafeArea( @@ -416,6 +424,7 @@ void main() { // it allows the list to remember its scroll position when // the tab view is not on the screen. key: PageStorageKey(name), + dragStartBehavior: DragStartBehavior.down, slivers: [ SliverOverlapInjector( // This is the flip side of the SliverOverlapAbsorber above. @@ -590,6 +599,7 @@ void main() { child: DefaultTabController( length: 1, child: NestedScrollView( + dragStartBehavior: DragStartBehavior.down, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ const SliverPersistentHeader( @@ -598,6 +608,7 @@ void main() { ]; }, body: SingleChildScrollView( + dragStartBehavior: DragStartBehavior.down, child: Container( height: 1000.0, child: const Placeholder(key: key2), diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index c8ecdbec21..4c858ad3b7 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'semantics_tester.dart'; import 'states.dart'; @@ -19,8 +20,10 @@ void main() { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( + dragStartBehavior: DragStartBehavior.down, children: kStates.map((String state) { return GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, diff --git a/packages/flutter/test/widgets/scroll_notification_test.dart b/packages/flutter/test/widgets/scroll_notification_test.dart index 0fbadfad88..b4cdcecddb 100644 --- a/packages/flutter/test/widgets/scroll_notification_test.dart +++ b/packages/flutter/test/widgets/scroll_notification_test.dart @@ -60,6 +60,7 @@ void main() { return false; }, child: SingleChildScrollView( + dragStartBehavior: DragStartBehavior.down, child: SizedBox( height: 1200.0, child: NotificationListener( @@ -70,11 +71,14 @@ void main() { }, child: Container( padding: const EdgeInsets.all(50.0), - child: const SingleChildScrollView(child: SizedBox(height: 1200.0)) - ) - ) - ) - ) + child: const SingleChildScrollView( + child: SizedBox(height: 1200.0), + dragStartBehavior: DragStartBehavior.down, + ), + ), + ), + ), + ), )); final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart index 15e161da36..3fcd382337 100644 --- a/packages/flutter/test/widgets/scroll_view_test.dart +++ b/packages/flutter/test/widgets/scroll_view_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'states.dart'; @@ -16,6 +17,7 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: ListView( + dragStartBehavior: DragStartBehavior.down, children: kStates.map((String state) { return GestureDetector( onTap: () { @@ -26,6 +28,7 @@ void main() { color: const Color(0xFF0000FF), child: Text(state), ), + dragStartBehavior: DragStartBehavior.down, ); }).toList(), ), @@ -54,6 +57,7 @@ void main() { return Directionality( textDirection: TextDirection.ltr, child: ListView( + dragStartBehavior: DragStartBehavior.down, children: kStates.take(n).map((String state) { return Container( height: 200.0, @@ -85,11 +89,13 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( + dragStartBehavior: DragStartBehavior.down, slivers: [ SliverList( delegate: SliverChildListDelegate( kStates.map((String state) { return GestureDetector( + dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, diff --git a/packages/flutter/test/widgets/scrollable_fling_test.dart b/packages/flutter/test/widgets/scrollable_fling_test.dart index 2dd30a0410..68b8ba6125 100644 --- a/packages/flutter/test/widgets/scrollable_fling_test.dart +++ b/packages/flutter/test/widgets/scrollable_fling_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; const TextStyle testFont = TextStyle( color: Color(0xFF00FF00), @@ -19,6 +20,7 @@ Future pumpTest(WidgetTester tester, TargetPlatform platform) async { home: Container( color: const Color(0xFF111111), child: ListView.builder( + dragStartBehavior: DragStartBehavior.down, itemBuilder: (BuildContext context, int index) { return Text('$index', style: testFont); }, @@ -64,7 +66,7 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: ListView(children: textWidgets) + child: ListView(children: textWidgets, dragStartBehavior: DragStartBehavior.down) ), ); @@ -92,7 +94,7 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: ListView(children: textWidgets) + child: ListView(children: textWidgets, dragStartBehavior: DragStartBehavior.down) ), ); diff --git a/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart b/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart index c06da6d576..2f7f0bf2f0 100644 --- a/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart +++ b/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; const List items = [0, 1, 2, 3, 4, 5]; @@ -18,6 +19,7 @@ void main() { child: Container( height: 50.0, child: ListView( + dragStartBehavior: DragStartBehavior.down, itemExtent: 290.0, scrollDirection: Axis.horizontal, children: items.map((int item) { @@ -25,6 +27,7 @@ void main() { child: GestureDetector( onTap: () { tapped.add(item); }, child: Text('$item'), + dragStartBehavior: DragStartBehavior.down, ), ); }).toList(), @@ -60,6 +63,7 @@ void main() { child: Container( width: 50.0, child: ListView( + dragStartBehavior: DragStartBehavior.down, itemExtent: 290.0, scrollDirection: Axis.vertical, children: items.map((int item) { @@ -67,6 +71,7 @@ void main() { child: GestureDetector( onTap: () { tapped.add(item); }, child: Text('$item'), + dragStartBehavior: DragStartBehavior.down, ), ); }).toList(), diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index 2dcf7e6c26..986eb6be89 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; import 'semantics_tester.dart'; @@ -266,6 +267,7 @@ void main() { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( + dragStartBehavior: DragStartBehavior.down, itemExtent: 20.0, itemBuilder: (BuildContext context, int index) { return Text('entry $index'); diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 8b77bb2646..bfefa12213 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; // Start of block of code where widget creation location line numbers and // columns will impact whether tests pass. @@ -396,6 +397,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { key: inspectorKey, selectButtonBuilder: selectButtonBuilder, child: ListView( + dragStartBehavior: DragStartBehavior.down, children: [ Container( key: childKey, @@ -1509,7 +1511,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { _CreationLocation location = knownLocations[id]; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(49)); + expect(location.line, equals(50)); expect(location.column, equals(9)); expect(count, equals(1)); @@ -1518,7 +1520,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { location = knownLocations[id]; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(87)); + expect(location.line, equals(88)); expect(location.column, equals(12)); expect(count, equals(1)); @@ -1543,7 +1545,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { location = knownLocations[id]; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(49)); + expect(location.line, equals(50)); expect(location.column, equals(9)); expect(count, equals(3)); // 3 clock widget instances rebuilt. @@ -1552,7 +1554,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { location = knownLocations[id]; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(87)); + expect(location.line, equals(88)); expect(location.column, equals(12)); expect(count, equals(3)); // 3 clock widget instances rebuilt.