From c83237f37c3cde442f3b40b9d13110ebd9920e67 Mon Sep 17 00:00:00 2001 From: xubaolin Date: Wed, 13 Mar 2024 15:10:48 +0800 Subject: [PATCH] [New feature]Introduce iOS multi-touch drag behavior (#141355) Fixes #38926 This patch implements the iOS behavior pointed out by @dkwingsmt at #38926 , which is also consistent with the performance of my settings application on the iPhone. ### iOS behavior (horizontal or vertical drag) ## Algorithm When dragging: delta(combined) = max(i of n that are positive) delta(i) - max(i of n that are negative) delta(i) It means that, if two fingers are moving +50 and +10 respectively, it will move +50; if they're moving at +50 and -10 respectively, it will move +40. ~~TODO~~ ~~Write some test cases~~ --- packages/flutter/lib/src/cupertino/app.dart | 4 + .../flutter/lib/src/gestures/monodrag.dart | 237 ++++++- .../flutter/lib/src/gestures/recognizer.dart | 33 +- .../lib/src/widgets/gesture_detector.dart | 5 + .../lib/src/widgets/scroll_configuration.dart | 27 +- .../flutter/lib/src/widgets/scrollable.dart | 4 +- packages/flutter/test/cupertino/app_test.dart | 19 + .../test/cupertino/date_picker_test.dart | 8 +- .../flutter/test/cupertino/picker_test.dart | 4 +- .../flutter/test/cupertino/refresh_test.dart | 6 +- packages/flutter/test/gestures/drag_test.dart | 636 +++++++++++++++++- .../test/widgets/nested_scroll_view_test.dart | 1 + .../test/widgets/scroll_behavior_test.dart | 149 +++- 13 files changed, 1098 insertions(+), 35 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart index 78ef191ea29..b82216954c1 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart @@ -2,6 +2,7 @@ // 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 'package:flutter/widgets.dart'; import 'button.dart'; @@ -492,6 +493,9 @@ class CupertinoScrollBehavior extends ScrollBehavior { } return const BouncingScrollPhysics(); } + + @override + MultitouchDragStrategy getMultitouchDragStrategy(BuildContext context) => MultitouchDragStrategy.averageBoundaryPointers; } class _CupertinoAppState extends State { diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 571e0049234..3ed4b3ad991 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math'; + import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import 'constants.dart'; import 'drag_details.dart'; @@ -119,6 +122,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// will only track the latest active (accepted by this recognizer) pointer, which /// appears to be only one finger dragging. /// + /// If set to [MultitouchDragStrategy.averageBoundaryPointers], all active + /// pointers will be tracked, and the result is computed from the boundary pointers. + /// /// If set to [MultitouchDragStrategy.sumAllPointers], /// all active pointers will be tracked together and the scrolling offset /// is the sum of the offsets of all active pointers @@ -128,7 +134,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// /// See also: /// - /// * [MultitouchDragStrategy], which defines two different drag strategies for + /// * [MultitouchDragStrategy], which defines several different drag strategies for /// multi-finger drag. MultitouchDragStrategy multitouchDragStrategy; @@ -323,11 +329,27 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { Offset _getDeltaForDetails(Offset delta); double? _getPrimaryValueFromOffset(Offset value); + + /// The axis (horizontal or vertical) corresponding to the primary drag direction. + /// + /// The [PanGestureRecognizer] returns null. + _DragDirection? _getPrimaryDragAxis() => null; bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop); bool _hasDragThresholdBeenMet = false; final Map _velocityTrackers = {}; + // The move delta of each pointer before the next frame. + // + // The key is the pointer ID. It is cleared whenever a new batch of pointer events is detected. + final Map _moveDeltaBeforeFrame = {}; + + // The timestamp of all events of the current frame. + // + // On a event with a different timestamp, the event is considered a new batch. + Duration? _frameTimeStamp; + Offset _lastUpdatedDeltaForPan = Offset.zero; + @override bool isPointerAllowed(PointerEvent event) { if (_initialButtons == null) { @@ -389,13 +411,194 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { final bool result; switch (multitouchDragStrategy) { case MultitouchDragStrategy.sumAllPointers: + case MultitouchDragStrategy.averageBoundaryPointers: result = true; case MultitouchDragStrategy.latestPointer: - result = _acceptedActivePointers.length <= 1 || pointer == _acceptedActivePointers.last; + result = _activePointer == null || pointer == _activePointer; } return result; } + void _recordMoveDeltaForMultitouch(int pointer, Offset localDelta) { + if (multitouchDragStrategy != MultitouchDragStrategy.averageBoundaryPointers) { + assert(_frameTimeStamp == null); + assert(_moveDeltaBeforeFrame.isEmpty); + return; + } + + assert(_frameTimeStamp == SchedulerBinding.instance.currentSystemFrameTimeStamp); + + if (_state != _DragState.accepted || localDelta == Offset.zero) { + return; + } + + if (_moveDeltaBeforeFrame.containsKey(pointer)) { + final Offset offset = _moveDeltaBeforeFrame[pointer]!; + _moveDeltaBeforeFrame[pointer] = offset + localDelta; + } else { + _moveDeltaBeforeFrame[pointer] = localDelta; + } + } + + double _getSumDelta({ + required int pointer, + required bool positive, + required _DragDirection axis, + }) { + double sum = 0.0; + + if (!_moveDeltaBeforeFrame.containsKey(pointer)) { + return sum; + } + + final Offset offset = _moveDeltaBeforeFrame[pointer]!; + if (positive) { + if (axis == _DragDirection.vertical) { + sum = max(offset.dy, 0.0); + } else { + sum = max(offset.dx, 0.0); + } + } else { + if (axis == _DragDirection.vertical) { + sum = min(offset.dy, 0.0); + } else { + sum = min(offset.dx, 0.0); + } + } + + return sum; + } + + int? _getMaxSumDeltaPointer({ + required bool positive, + required _DragDirection axis, + }) { + if (_moveDeltaBeforeFrame.isEmpty) { + return null; + } + + int? ret; + double? max; + double sum; + for (final int pointer in _moveDeltaBeforeFrame.keys) { + sum = _getSumDelta(pointer: pointer, positive: positive, axis: axis); + if (ret == null) { + ret = pointer; + max = sum; + } else { + if (positive) { + if (sum > max!) { + ret = pointer; + max = sum; + } + } else { + if (sum < max!) { + ret = pointer; + max = sum; + } + } + } + } + assert(ret != null); + return ret; + } + + Offset _resolveLocalDeltaForMultitouch(int pointer, Offset localDelta) { + if (multitouchDragStrategy != MultitouchDragStrategy.averageBoundaryPointers) { + if (_frameTimeStamp != null) { + _moveDeltaBeforeFrame.clear(); + _frameTimeStamp = null; + _lastUpdatedDeltaForPan = Offset.zero; + } + return localDelta; + } + + final Duration currentSystemFrameTimeStamp = SchedulerBinding.instance.currentSystemFrameTimeStamp; + if (_frameTimeStamp != currentSystemFrameTimeStamp) { + _moveDeltaBeforeFrame.clear(); + _lastUpdatedDeltaForPan = Offset.zero; + _frameTimeStamp = currentSystemFrameTimeStamp; + } + + assert(_frameTimeStamp == SchedulerBinding.instance.currentSystemFrameTimeStamp); + + final _DragDirection? axis = _getPrimaryDragAxis(); + + if (_state != _DragState.accepted || localDelta == Offset.zero || (_moveDeltaBeforeFrame.isEmpty && axis != null)) { + return localDelta; + } + + final double dx,dy; + if (axis == _DragDirection.horizontal) { + dx = _resolveDelta(pointer: pointer, axis: _DragDirection.horizontal, localDelta: localDelta); + assert(dx.abs() <= localDelta.dx.abs()); + dy = 0.0; + } else if (axis == _DragDirection.vertical) { + dx = 0.0; + dy = _resolveDelta(pointer: pointer, axis: _DragDirection.vertical, localDelta: localDelta); + assert(dy.abs() <= localDelta.dy.abs()); + } else { + final double averageX = _resolveDeltaForPanGesture(axis: _DragDirection.horizontal, localDelta: localDelta); + final double averageY = _resolveDeltaForPanGesture(axis: _DragDirection.vertical, localDelta: localDelta); + final Offset updatedDelta = Offset(averageX, averageY) - _lastUpdatedDeltaForPan; + _lastUpdatedDeltaForPan = Offset(averageX, averageY); + dx = updatedDelta.dx; + dy = updatedDelta.dy; + } + + return Offset(dx, dy); + } + + double _resolveDelta({ + required int pointer, + required _DragDirection axis, + required Offset localDelta, + }) { + final bool positive = axis == _DragDirection.horizontal ? localDelta.dx > 0 : localDelta.dy > 0; + final double delta = axis == _DragDirection.horizontal ? localDelta.dx : localDelta.dy; + final int? maxSumDeltaPointer = _getMaxSumDeltaPointer(positive: positive, axis: axis); + assert(maxSumDeltaPointer != null); + + if (maxSumDeltaPointer == pointer) { + return delta; + } else { + final double maxSumDelta = _getSumDelta(pointer: maxSumDeltaPointer!, positive: positive, axis: axis); + final double curPointerSumDelta = _getSumDelta(pointer: pointer, positive: positive, axis: axis); + if (positive) { + if (curPointerSumDelta + delta > maxSumDelta) { + return curPointerSumDelta + delta - maxSumDelta; + } else { + return 0.0; + } + } else { + if (curPointerSumDelta + delta < maxSumDelta) { + return curPointerSumDelta + delta - maxSumDelta; + } else { + return 0.0; + } + } + } + } + + double _resolveDeltaForPanGesture({ + required _DragDirection axis, + required Offset localDelta, + }) { + final double delta = axis == _DragDirection.horizontal ? localDelta.dx : localDelta.dy; + final int pointerCount = _acceptedActivePointers.length; + assert(pointerCount >= 1); + + double sum = delta; + for (final Offset offset in _moveDeltaBeforeFrame.values) { + if (axis == _DragDirection.horizontal) { + sum += offset.dx; + } else { + sum += offset.dy; + } + } + return sum / pointerCount; + } + @override void handleEvent(PointerEvent event) { assert(_state != _DragState.ready); @@ -424,6 +627,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan); final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan); _finalPosition = OffsetPair(local: localPosition, global: position); + final Offset resolvedDelta = _resolveLocalDeltaForMultitouch(event.pointer, localDelta); switch (_state) { case _DragState.ready || _DragState.possible: _pendingDragOffset += OffsetPair(local: localDelta, global: delta); @@ -447,12 +651,13 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { case _DragState.accepted: _checkUpdate( sourceTimeStamp: event.timeStamp, - delta: _getDeltaForDetails(localDelta), - primaryDelta: _getPrimaryValueFromOffset(localDelta), + delta: _getDeltaForDetails(resolvedDelta), + primaryDelta: _getPrimaryValueFromOffset(resolvedDelta), globalPosition: position, localPosition: localPosition, ); } + _recordMoveDeltaForMultitouch(event.pointer, localDelta); } if (event case PointerUpEvent() || PointerCancelEvent() || PointerPanZoomEndEvent()) { _giveUpPointer(event.pointer); @@ -460,11 +665,18 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { } final List _acceptedActivePointers = []; + // This value is used when the multitouch strategy is `latestPointer`, + // it keeps track of the last accepted pointer. If this active pointer + // leave up, it will be set to the first accepted pointer. + // Refer to the implementation of Android `RecyclerView`(line 3846): + // https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java + int? _activePointer; @override void acceptGesture(int pointer) { assert(!_acceptedActivePointers.contains(pointer)); _acceptedActivePointers.add(pointer); + _activePointer = pointer; if (!onlyAcceptDragOnThreshold || _hasDragThresholdBeenMet) { _checkDrag(pointer); } @@ -502,6 +714,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { if (!_acceptedActivePointers.remove(pointer)) { resolvePointer(pointer, GestureDisposition.rejected); } + + _moveDeltaBeforeFrame.remove(pointer); + if (_activePointer == pointer) { + _activePointer = + _acceptedActivePointers.isNotEmpty ? _acceptedActivePointers.first : null; + } } void _checkDown() { @@ -687,6 +905,9 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { @override double _getPrimaryValueFromOffset(Offset value) => value.dy; + @override + _DragDirection? _getPrimaryDragAxis() => _DragDirection.vertical; + @override String get debugDescription => 'vertical drag'; } @@ -744,6 +965,9 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer { @override double _getPrimaryValueFromOffset(Offset value) => value.dx; + @override + _DragDirection? _getPrimaryDragAxis() => _DragDirection.horizontal; + @override String get debugDescription => 'horizontal drag'; } @@ -801,3 +1025,8 @@ class PanGestureRecognizer extends DragGestureRecognizer { @override String get debugDescription => 'pan'; } + +enum _DragDirection { + horizontal, + vertical, +} diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 8c732b49603..37794bdf8de 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -51,24 +51,53 @@ enum DragStartBehavior { /// Configuration of multi-finger drag strategy on multi-touch devices. /// /// When dragging with only one finger, there's no difference in behavior -/// between the two settings. +/// between all the settings. /// /// Used by [DragGestureRecognizer.multitouchDragStrategy]. enum MultitouchDragStrategy { /// Only the latest active pointer is tracked by the recognizer. /// - /// If the tracked pointer is released, the latest of the remaining active + /// If the tracked pointer is released, the first accepted of the remaining active /// pointers will continue to be tracked. /// /// This is the behavior typically seen on Android. latestPointer, + /// All active pointers will be tracked, and the result is computed from + /// the boundary pointers. + /// + /// The scrolling offset is determined by the maximum deltas of both directions. + /// + /// If the user is dragging with 3 pointers at the same time, each having + /// \[+10, +20, +33\] pixels of offset, the recognizer will report a delta of 33 pixels. + /// + /// If the user is dragging with 5 pointers at the same time, each having + /// \[+10, +20, +33, -1, -12\] pixels of offset, the recognizer will report a + /// delta of (+33) + (-12) = 21 pixels. + /// + /// The panning [PanGestureRecognizer] offset is the average of all pointers. + /// + /// If the user is dragging with 3 pointers at the same time, each having + /// \[+10, +50, -30\] pixels of offset in one direction (horizontal or vertical), + /// the recognizer will report a delta of (10 + 50 -30) / 3 = 10 pixels in this direction. + /// + /// This is the behavior typically seen on iOS. + averageBoundaryPointers, + /// All active pointers will be tracked together. The scrolling offset /// is the sum of the offsets of all active pointers. /// /// When a [Scrollable] drives scrolling by this drag strategy, the scrolling /// speed will double or triple, depending on how many fingers are dragging /// at the same time. + /// + /// If the user is dragging with 3 pointers at the same time, each having + /// \[+10, +20, +33\] pixels of offset, the recognizer will report a delta + /// of 10 + 20 + 33 = 63 pixels. + /// + /// If the user is dragging with 5 pointers at the same time, each having + /// \[+10, +20, +33, -1, -12\] pixels of offset, the recognizer will report + /// a delta of 10 + 20 + 33 - 1 - 12 = 50 pixels. sumAllPointers, } diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 43c7caa7646..3f28b919adb 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'framework.dart'; import 'media_query.dart'; +import 'scroll_configuration.dart'; export 'package:flutter/gestures.dart' show DragDownDetails, @@ -1020,6 +1021,7 @@ class GestureDetector extends StatelessWidget { Widget build(BuildContext context) { final Map gestures = {}; final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context); + final ScrollBehavior configuration = ScrollConfiguration.of(context); if (onTapDown != null || onTapUp != null || @@ -1137,6 +1139,7 @@ class GestureDetector extends StatelessWidget { ..onEnd = onVerticalDragEnd ..onCancel = onVerticalDragCancel ..dragStartBehavior = dragStartBehavior + ..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context) ..gestureSettings = gestureSettings ..supportedDevices = supportedDevices; }, @@ -1158,6 +1161,7 @@ class GestureDetector extends StatelessWidget { ..onEnd = onHorizontalDragEnd ..onCancel = onHorizontalDragCancel ..dragStartBehavior = dragStartBehavior + ..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context) ..gestureSettings = gestureSettings ..supportedDevices = supportedDevices; }, @@ -1179,6 +1183,7 @@ class GestureDetector extends StatelessWidget { ..onEnd = onPanEnd ..onCancel = onPanCancel ..dragStartBehavior = dragStartBehavior + ..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context) ..gestureSettings = gestureSettings ..supportedDevices = supportedDevices; }, diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart index 599a2aa9173..a2b6bd4e0ec 100644 --- a/packages/flutter/lib/src/widgets/scroll_configuration.dart +++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart @@ -110,8 +110,20 @@ class ScrollBehavior { /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.multitouchDragStrategy} /// /// By default, [MultitouchDragStrategy.latestPointer] is configured to - /// create drag gestures for all platforms. - MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.latestPointer; + /// create drag gestures for non-Apple platforms, and + /// [MultitouchDragStrategy.averageBoundaryPointers] for Apple platforms. + MultitouchDragStrategy getMultitouchDragStrategy(BuildContext context) { + switch (getPlatform(context)) { + case TargetPlatform.macOS: + case TargetPlatform.iOS: + return MultitouchDragStrategy.averageBoundaryPointers; + case TargetPlatform.linux: + case TargetPlatform.windows: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return MultitouchDragStrategy.latestPointer; + } + } /// A set of [LogicalKeyboardKey]s that, when any or all are pressed in /// combination with a [PointerDeviceKind.mouse] pointer scroll event, will @@ -253,12 +265,11 @@ class _WrappedScrollBehavior implements ScrollBehavior { this.scrollbars = true, this.overscroll = true, Set? dragDevices, - MultitouchDragStrategy? multitouchDragStrategy, + this.multitouchDragStrategy, Set? pointerAxisModifiers, this.physics, this.platform, }) : _dragDevices = dragDevices, - _multitouchDragStrategy = multitouchDragStrategy, _pointerAxisModifiers = pointerAxisModifiers; final ScrollBehavior delegate; @@ -267,17 +278,19 @@ class _WrappedScrollBehavior implements ScrollBehavior { final ScrollPhysics? physics; final TargetPlatform? platform; final Set? _dragDevices; - final MultitouchDragStrategy? _multitouchDragStrategy; + final MultitouchDragStrategy? multitouchDragStrategy; final Set? _pointerAxisModifiers; @override Set get dragDevices => _dragDevices ?? delegate.dragDevices; @override - MultitouchDragStrategy get multitouchDragStrategy => _multitouchDragStrategy ?? delegate.multitouchDragStrategy; + Set get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers; @override - Set get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers; + MultitouchDragStrategy getMultitouchDragStrategy(BuildContext context) { + return multitouchDragStrategy ?? delegate.getMultitouchDragStrategy(context); + } @override Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index f86e24efdff..b66c7ec334e 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -758,7 +758,7 @@ class ScrollableState extends State with TickerProviderStateMixin, R ..maxFlingVelocity = _physics?.maxFlingVelocity ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) ..dragStartBehavior = widget.dragStartBehavior - ..multitouchDragStrategy = _configuration.multitouchDragStrategy + ..multitouchDragStrategy = _configuration.getMultitouchDragStrategy(context) ..gestureSettings = _mediaQueryGestureSettings ..supportedDevices = _configuration.dragDevices; }, @@ -780,7 +780,7 @@ class ScrollableState extends State with TickerProviderStateMixin, R ..maxFlingVelocity = _physics?.maxFlingVelocity ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) ..dragStartBehavior = widget.dragStartBehavior - ..multitouchDragStrategy = _configuration.multitouchDragStrategy + ..multitouchDragStrategy = _configuration.getMultitouchDragStrategy(context) ..gestureSettings = _mediaQueryGestureSettings ..supportedDevices = _configuration.dragDevices; }, diff --git a/packages/flutter/test/cupertino/app_test.dart b/packages/flutter/test/cupertino/app_test.dart index 3da1c08ac77..df359b9d8dc 100644 --- a/packages/flutter/test/cupertino/app_test.dart +++ b/packages/flutter/test/cupertino/app_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -352,6 +353,24 @@ void main() { expect(ScrollConfiguration.of(capturedContext).runtimeType, CupertinoScrollBehavior); }); + testWidgets('CupertinoApp has correct default multitouchDragStrategy', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + CupertinoApp( + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + + final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); + expect(scrollBehavior.runtimeType, CupertinoScrollBehavior); + expect(scrollBehavior.getMultitouchDragStrategy(capturedContext), MultitouchDragStrategy.averageBoundaryPointers); + }); + testWidgets('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( diff --git a/packages/flutter/test/cupertino/date_picker_test.dart b/packages/flutter/test/cupertino/date_picker_test.dart index 9a358ccf596..1d725f23ace 100644 --- a/packages/flutter/test/cupertino/date_picker_test.dart +++ b/packages/flutter/test/cupertino/date_picker_test.dart @@ -416,7 +416,7 @@ void main() { ), ); - await tester.drag(find.text('10'), const Offset(0.0, 32.0), touchSlopY: 0, warnIfMissed: false); // see top of file + await tester.drag(find.text('10'), const Offset(0.0, 32.0), pointer: 1, touchSlopY: 0, warnIfMissed: false); // see top of file await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); @@ -438,7 +438,7 @@ void main() { ), ); - await tester.drag(find.text('9'), const Offset(0.0, 32.0), touchSlopY: 0, warnIfMissed: false); // see top of file + await tester.drag(find.text('9'), const Offset(0.0, 32.0), pointer: 1, touchSlopY: 0, warnIfMissed: false); // see top of file await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); @@ -1246,14 +1246,14 @@ void main() { ), ); - await tester.drag(find.text('27'), const Offset(0.0, -32.0), touchSlopY: 0.0, warnIfMissed: false); // see top of file + await tester.drag(find.text('27'), const Offset(0.0, -32.0), pointer: 1, touchSlopY: 0.0, warnIfMissed: false); // see top of file await tester.pump(); expect( date, DateTime(2018, 2, 28), ); - await tester.drag(find.text('28'), const Offset(0.0, -32.0), touchSlopY: 0.0, warnIfMissed: false); // see top of file + await tester.drag(find.text('28'), const Offset(0.0, -32.0), pointer: 1, touchSlopY: 0.0, warnIfMissed: false); // see top of file await tester.pump(); // Once to trigger the post frame animate call. // Callback doesn't transiently go into invalid dates. diff --git a/packages/flutter/test/cupertino/picker_test.dart b/packages/flutter/test/cupertino/picker_test.dart index 4215227cb17..ac8c8ef9615 100644 --- a/packages/flutter/test/cupertino/picker_test.dart +++ b/packages/flutter/test/cupertino/picker_test.dart @@ -343,7 +343,7 @@ void main() { ); // Drag it by a bit but not enough to move to the next item. - await tester.drag(find.text('10'), const Offset(0.0, 30.0), touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer + await tester.drag(find.text('10'), const Offset(0.0, 30.0), pointer: 1, touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer // The item that was in the center now moved a bit. expect( @@ -360,7 +360,7 @@ void main() { expect(selectedItems.isEmpty, true); // Drag it by enough to move to the next item. - await tester.drag(find.text('10'), const Offset(0.0, 70.0), touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer + await tester.drag(find.text('10'), const Offset(0.0, 70.0), pointer: 1, touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer await tester.pumpAndSettle(); diff --git a/packages/flutter/test/cupertino/refresh_test.dart b/packages/flutter/test/cupertino/refresh_test.dart index 5601753a471..7d92df5b1dc 100644 --- a/packages/flutter/test/cupertino/refresh_test.dart +++ b/packages/flutter/test/cupertino/refresh_test.dart @@ -705,7 +705,7 @@ void main() { ), ); - await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0); + await tester.drag(find.text('0'), const Offset(0.0, 150.0), pointer: 1, touchSlopY: 0.0); await tester.pump(); expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); @@ -748,7 +748,7 @@ void main() { // Start another drag by an amount that would have been enough to // trigger another refresh if it were in the right state. - await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0, warnIfMissed: false); + await tester.drag(find.text('0'), const Offset(0.0, 150.0), pointer: 1, touchSlopY: 0.0, warnIfMissed: false); await tester.pump(); // Instead, it's still in the done state because the sliver never @@ -779,7 +779,7 @@ void main() { ); // Start another drag. It's now in drag mode. - await tester.drag(find.text('0'), const Offset(0.0, 40.0), touchSlopY: 0.0); + await tester.drag(find.text('0'), const Offset(0.0, 40.0), pointer: 1, touchSlopY: 0.0); await tester.pump(); expect(mockHelper.invocations, contains(matchesBuilder( refreshState: RefreshIndicatorMode.drag, diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index 9df98dd782f..b29be48e13c 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/gestures.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; import 'gesture_tester.dart'; @@ -553,9 +554,9 @@ void main() { testGesture('Drag with multiple pointers in down behavior - default', (GestureTester tester) { final HorizontalDragGestureRecognizer drag1 = - HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; + HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; final VerticalDragGestureRecognizer drag2 = - VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; + VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; addTearDown(drag1.dispose); addTearDown(drag2.dispose); @@ -592,7 +593,7 @@ void main() { tester.route(down6); log.add('-d'); - // Current latest active pointer is pointer6. + // Current active pointer is pointer6. // Should not trigger the drag1-update. tester.route(pointer5.move(const Offset(0.0, 100.0))); @@ -600,19 +601,19 @@ void main() { tester.route(pointer5.move(const Offset(70.0, 70.0))); log.add('-f'); - // Latest active pointer can trigger the drag1-update. + // The active pointer can trigger the drag1-update. tester.route(pointer6.move(const Offset(0.0, 100.0))); log.add('-g'); tester.route(pointer6.move(const Offset(70.0, 70.0))); log.add('-h'); - // Release the latest active pointer. + // Release the active pointer. tester.route(pointer6.up()); log.add('-i'); - // Current latest active pointer is pointer5. + // Current active pointer should be pointer5. - // Latest active pointer can trigger the drag1-update. + // The active pointer can trigger the drag1-update. tester.route(pointer5.move(const Offset(0.0, 100.0))); log.add('-j'); tester.route(pointer5.move(const Offset(70.0, 70.0))); @@ -648,6 +649,622 @@ void main() { ]); }); + testGesture('Drag with multiple pointers in down behavior - latestPointer', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag1 = + HorizontalDragGestureRecognizer() + ..multitouchDragStrategy = MultitouchDragStrategy.latestPointer + ..dragStartBehavior = DragStartBehavior.down; + final VerticalDragGestureRecognizer drag2 = + VerticalDragGestureRecognizer() + ..multitouchDragStrategy = MultitouchDragStrategy.latestPointer + ..dragStartBehavior = DragStartBehavior.down; + addTearDown(drag1.dispose); + addTearDown(drag2.dispose); + + final List log = []; + drag1.onDown = (_) { log.add('drag1-down'); }; + drag1.onStart = (_) { log.add('drag1-start'); }; + drag1.onUpdate = (_) { log.add('drag1-update'); }; + drag1.onEnd = (_) { log.add('drag1-end'); }; + drag1.onCancel = () { log.add('drag1-cancel'); }; + drag2.onDown = (_) { log.add('drag2-down'); }; + drag2.onStart = (_) { log.add('drag2-start'); }; + drag2.onUpdate = (_) { log.add('drag2-update'); }; + drag2.onEnd = (_) { log.add('drag2-end'); }; + drag2.onCancel = () { log.add('drag2-cancel'); }; + + final TestPointer pointer5 = TestPointer(5); + final PointerDownEvent down5 = pointer5.down(const Offset(10.0, 10.0)); + drag1.addPointer(down5); + drag2.addPointer(down5); + tester.closeArena(5); + tester.route(down5); + log.add('-a'); + + tester.route(pointer5.move(const Offset(100.0, 0.0))); + log.add('-b'); + tester.route(pointer5.move(const Offset(50.0, 50.0))); + log.add('-c'); + + final TestPointer pointer6 = TestPointer(6); + final PointerDownEvent down6 = pointer6.down(const Offset(20.0, 20.0)); + drag1.addPointer(down6); + drag2.addPointer(down6); + tester.closeArena(6); + tester.route(down6); + log.add('-d'); + + // Current active pointer is pointer6. + + // Should not trigger the drag1-update. + tester.route(pointer5.move(const Offset(0.0, 100.0))); + log.add('-e'); + tester.route(pointer5.move(const Offset(70.0, 70.0))); + log.add('-f'); + + // The active pointer can trigger the drag1-update. + tester.route(pointer6.move(const Offset(0.0, 100.0))); + log.add('-g'); + tester.route(pointer6.move(const Offset(70.0, 70.0))); + log.add('-h'); + + final TestPointer pointer7 = TestPointer(7); + final PointerDownEvent down7 = pointer7.down(const Offset(20.0, 20.0)); + drag1.addPointer(down7); + drag2.addPointer(down7); + tester.closeArena(7); + tester.route(down7); + log.add('-i'); + + // Current active pointer is pointer7. + + // Release the active pointer. + tester.route(pointer7.up()); + log.add('-j'); + + // Current active pointer should be pointer5 (the first accepted pointer). + + // The active pointer can trigger the drag1-update. + tester.route(pointer5.move(const Offset(0.0, 100.0))); + log.add('-k'); + tester.route(pointer5.move(const Offset(70.0, 70.0))); + log.add('-l'); + + tester.route(pointer5.up()); + tester.route(pointer6.up()); + + expect(log, [ + 'drag1-down', + 'drag2-down', + '-a', + 'drag2-cancel', + 'drag1-start', + 'drag1-update', + '-b', + 'drag1-update', + '-c', + 'drag2-down', + 'drag2-cancel', + '-d', + '-e', + '-f', + 'drag1-update', + '-g', + 'drag1-update', + '-h', + 'drag2-down', + 'drag2-cancel', + '-i', + '-j', + 'drag1-update', + '-k', + 'drag1-update', + '-l', + 'drag1-end' + ]); + }); + + testGesture('Horizontal drag with multiple pointers - averageBoundaryPointers', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag = + HorizontalDragGestureRecognizer() + ..multitouchDragStrategy = MultitouchDragStrategy.averageBoundaryPointers; + + final List log = []; + drag.onUpdate = (DragUpdateDetails details) { log.add('drag-update (${details.delta})'); }; + + final TestPointer pointer5 = TestPointer(5); + final PointerDownEvent down5 = pointer5.down(Offset.zero); + drag.addPointer(down5); + tester.closeArena(5); + tester.route(down5); + + log.add('-a'); + // #5 pointer move to right 100.0, received delta should be (100.0, 0.0). + tester.route(pointer5.move(const Offset(100.0, 0.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(100, 0), } + + // Put down the second pointer 6. + final TestPointer pointer6 = TestPointer(6); + final PointerDownEvent down6 = pointer6.down(Offset.zero); + drag.addPointer(down6); + tester.closeArena(6); + tester.route(down6); + + log.add('-b'); + // #6 pointer move to right 110.0, received delta should be (10, 0.0). + tester.route(pointer6.move(const Offset(110.0, 0.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(100, 0), 6: Offset(110, 0),} + + // Put down the second pointer 7. + final TestPointer pointer7 = TestPointer(7); + final PointerDownEvent down7 = pointer7.down(Offset.zero); + drag.addPointer(down7); + tester.closeArena(7); + tester.route(down7); + + log.add('-c'); + // #7 pointer move to left 100, received delta should be (-100.0, 0.0). + tester.route(pointer7.move(const Offset(-100.0, 0.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(100, 0), + // 6: Offset(110, 0), + // 7: Offset(-100, 0), + // } + + // Put down the second pointer 8. + final TestPointer pointer8= TestPointer(8); + final PointerDownEvent down8 = pointer8.down(Offset.zero); + drag.addPointer(down8); + tester.closeArena(8); + tester.route(down8); + + log.add('-d'); + // #8 pointer move to left 110, received delta should be (-10, 0.0). + tester.route(pointer8.move(const Offset(-110.0, 0.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(100, 0), + // 6: Offset(110, 0), + // 7: Offset(-100, 0), + // 8: Offset(-110, 0), + // } + + log.add('-e'); + // #5 pointer move to right 20.0, received delta should be (10.0, 0.0). + tester.route(pointer5.move(const Offset(120.0, 0.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(120, 0), + // 6: Offset(110, 0), + // 7: Offset(-100, 0), + // 8: Offset(-110, 0), + // } + + log.add('-f'); + // #7 pointer move to left 20, received delta should be (-10.0, 0.0). + tester.route(pointer7.move(const Offset(-120.0, 0.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(120, 0), + // 6: Offset(110, 0), + // 7: Offset(-120, 0), + // 8: Offset(-110, 0), + // } + + // Trigger a new frame. + SchedulerBinding.instance.handleBeginFrame(const Duration(milliseconds: 100)); + SchedulerBinding.instance.handleDrawFrame(); + + // _moveDeltaBeforeFrame = { } + + log.add('-g'); + // #6 pointer move to right 10.0, received delta should be (10, 0.0). + tester.route(pointer6.move(const Offset(120, 0.0))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(10, 0), + // } + + log.add('-h'); + // #8 pointer move to left 10, received delta should be (-10, 0.0). + tester.route(pointer8.move(const Offset(-120, 0.0))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(10, 0), + // 8: Offset(-10, 0), + // } + + log.add('-i'); + // #5 pointer move to right 10.0, received delta should be (0.0, 0.0). + tester.route(pointer5.move(const Offset(130, 0.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(10, 0), + // 6: Offset(10, 0), + // 8: Offset(-10, 0), + // } + + log.add('-j'); + // #7 pointer move to left 10, received delta should be (0.0, 0.0). + tester.route(pointer7.move(const Offset(-130.0, 0.0))); + + tester.route(pointer5.up()); + tester.route(pointer6.up()); + tester.route(pointer7.up()); + tester.route(pointer8.up()); + + // Tear down 'currentSystemFrameTimeStamp' + SchedulerBinding.instance.handleBeginFrame(Duration.zero); + SchedulerBinding.instance.handleDrawFrame(); + + expect(log, [ + '-a', + 'drag-update (Offset(100.0, 0.0))', + '-b', + 'drag-update (Offset(10.0, 0.0))', + '-c', + 'drag-update (Offset(-100.0, 0.0))', + '-d', + 'drag-update (Offset(-10.0, 0.0))', + '-e', + 'drag-update (Offset(10.0, 0.0))', + '-f', + 'drag-update (Offset(-10.0, 0.0))', + '-g', + 'drag-update (Offset(10.0, 0.0))', + '-h', + 'drag-update (Offset(-10.0, 0.0))', + '-i', + 'drag-update (Offset(0.0, 0.0))', + '-j', + 'drag-update (Offset(0.0, 0.0))' + ]); + }); + + testGesture('Vertical drag with multiple pointers - averageBoundaryPointers', (GestureTester tester) { + final VerticalDragGestureRecognizer drag = + VerticalDragGestureRecognizer() + ..multitouchDragStrategy = MultitouchDragStrategy.averageBoundaryPointers; + + final List log = []; + drag.onUpdate = (DragUpdateDetails details) { log.add('drag-update (${details.delta})'); }; + + final TestPointer pointer5 = TestPointer(5); + final PointerDownEvent down5 = pointer5.down(Offset.zero); + drag.addPointer(down5); + tester.closeArena(5); + tester.route(down5); + + log.add('-a'); + // #5 pointer move to down 100.0, received delta should be (0.0, 100.0). + tester.route(pointer5.move(const Offset(0.0, 100.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(0, 100), } + + // Put down the second pointer 6. + final TestPointer pointer6 = TestPointer(6); + final PointerDownEvent down6 = pointer6.down(Offset.zero); + drag.addPointer(down6); + tester.closeArena(6); + tester.route(down6); + + log.add('-b'); + // #6 pointer move to down 110.0, received delta should be (0, 10.0). + tester.route(pointer6.move(const Offset(0.0, 110.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(0, 100), 6: Offset(0, 110),} + + // Put down the second pointer 7. + final TestPointer pointer7 = TestPointer(7); + final PointerDownEvent down7 = pointer7.down(Offset.zero); + drag.addPointer(down7); + tester.closeArena(7); + tester.route(down7); + + log.add('-c'); + // #7 pointer move to up 100, received delta should be (0.0, -100.0). + tester.route(pointer7.move(const Offset(0.0, -100.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(0, 100), + // 6: Offset(0, 110), + // 7: Offset(0, -100), + // } + + // Put down the second pointer 8. + final TestPointer pointer8= TestPointer(8); + final PointerDownEvent down8 = pointer8.down(Offset.zero); + drag.addPointer(down8); + tester.closeArena(8); + tester.route(down8); + + log.add('-d'); + // #8 pointer move to up 110, received delta should be (0, -10.0). + tester.route(pointer8.move(const Offset(0.0, -110.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(0, 100), + // 6: Offset(0, 110), + // 7: Offset(0, -100), + // 8: Offset(0, -110), + // } + + log.add('-e'); + // #5 pointer move to down 20.0, received delta should be (0.0, 10.0). + tester.route(pointer5.move(const Offset(0.0, 120.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(0, 120), + // 6: Offset(0, 110), + // 7: Offset(0, -100), + // 8: Offset(0, -110), + // } + + log.add('-f'); + // #7 pointer move to up 20, received delta should be (0.0, -10.0). + tester.route(pointer7.move(const Offset(0.0, -120.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(0, 120), + // 6: Offset(0, 110), + // 7: Offset(0, -120), + // 8: Offset(0, -110), + // } + + // Trigger a new frame. + SchedulerBinding.instance.handleBeginFrame(const Duration(milliseconds: 100)); + SchedulerBinding.instance.handleDrawFrame(); + + // _moveDeltaBeforeFrame = { } + + log.add('-g'); + // #6 pointer move to down 10.0, received delta should be (0, 10.0). + tester.route(pointer6.move(const Offset(0, 120.0))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(0, 10), + // } + + log.add('-h'); + // #8 pointer move to up 10, received delta should be (0, -10.0). + tester.route(pointer8.move(const Offset(0, -120.0))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(0, 10), + // 8: Offset(0, -10), + // } + + log.add('-i'); + // #5 pointer move to down 10.0, received delta should be (0.0, 0.0). + tester.route(pointer5.move(const Offset(0, 130.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(0, 10), + // 6: Offset(0, 10), + // 8: Offset(0, -10), + // } + + log.add('-j'); + // #7 pointer move to up 10, received delta should be (0.0, 0.0). + tester.route(pointer7.move(const Offset(0.0, -130.0))); + + tester.route(pointer5.up()); + tester.route(pointer6.up()); + tester.route(pointer7.up()); + tester.route(pointer8.up()); + + // Tear down 'currentSystemFrameTimeStamp' + SchedulerBinding.instance.handleBeginFrame(Duration.zero); + SchedulerBinding.instance.handleDrawFrame(); + + expect(log, [ + '-a', + 'drag-update (Offset(0.0, 100.0))', + '-b', + 'drag-update (Offset(0.0, 10.0))', + '-c', + 'drag-update (Offset(0.0, -100.0))', + '-d', + 'drag-update (Offset(0.0, -10.0))', + '-e', + 'drag-update (Offset(0.0, 10.0))', + '-f', + 'drag-update (Offset(0.0, -10.0))', + '-g', + 'drag-update (Offset(0.0, 10.0))', + '-h', + 'drag-update (Offset(0.0, -10.0))', + '-i', + 'drag-update (Offset(0.0, 0.0))', + '-j', + 'drag-update (Offset(0.0, 0.0))' + ]); + }); + + testGesture('Pan drag with multiple pointers - averageBoundaryPointers', (GestureTester tester) { + final PanGestureRecognizer drag = + PanGestureRecognizer() + ..multitouchDragStrategy = MultitouchDragStrategy.averageBoundaryPointers; + + final List log = []; + drag.onUpdate = (DragUpdateDetails details) { log.add('drag-update (${details.delta})'); }; + + final TestPointer pointer5 = TestPointer(5); + final PointerDownEvent down5 = pointer5.down(Offset.zero); + drag.addPointer(down5); + tester.closeArena(5); + tester.route(down5); + + log.add('-a'); + // #5 pointer move (100.0, 100.0), received delta should be (100.0, 100.0). + // offset = 100 / 1 + // delta = offset - 0 (last offset) + tester.route(pointer5.move(const Offset(100.0, 100.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(100, 100), } + + // Put down the second pointer 6. + final TestPointer pointer6 = TestPointer(6); + final PointerDownEvent down6 = pointer6.down(Offset.zero); + drag.addPointer(down6); + tester.closeArena(6); + tester.route(down6); + + log.add('-b'); + // #6 pointer move (110.0, 110.0), received delta should be (5, 5). + // offset = (100 + 110) / 2 + // delta = offset - 100 (last offset) + + tester.route(pointer6.move(const Offset(110.0, 110.0))); + + // _moveDeltaBeforeFrame = { 5: Offset(100, 100), 6: Offset(110, 110),} + + // Put down the second pointer 7. + final TestPointer pointer7 = TestPointer(7); + final PointerDownEvent down7 = pointer7.down(Offset.zero); + drag.addPointer(down7); + tester.closeArena(7); + tester.route(down7); + + log.add('-c'); + // #7 pointer move (-100.0, -100.0), received delta should be (-68.3, -68.3). + // offset = (100 + 110 -100) / 3 + // delta = offset - 105(last offset) + tester.route(pointer7.move(const Offset(-100.0, -100.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(100, 100), + // 6: Offset(110, 110), + // 7: Offset(-100, -100), + // } + + // Put down the second pointer 8. + final TestPointer pointer8= TestPointer(8); + final PointerDownEvent down8 = pointer8.down(Offset.zero); + drag.addPointer(down8); + tester.closeArena(8); + tester.route(down8); + + log.add('-d'); + // #8 pointer (-110.0, -110.0), received delta should be (-36.7, -36.7). + // offset = (100 + 110 -100 - 110) / 4 + // delta = offset - 36.7(last offset) + tester.route(pointer8.move(const Offset(-110.0, -110.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(100, 100), + // 6: Offset(110, 110), + // 7: Offset(-100, -100), + // 8: Offset(-110, -110), + // } + + log.add('-e'); + // #5 pointer move (20.0, 20.0), received delta should be (5.0, 5.0). + // offset = (100 + 110 -100 - 110 + 20) / 4 + // delta = offset - 0 (last offset) + tester.route(pointer5.move(const Offset(120.0, 120.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(120, 120), + // 6: Offset(110, 110), + // 7: Offset(-100, -100), + // 8: Offset(-110, -110), + // } + + log.add('-f'); + // #7 pointer move (-20.0, -20.0), received delta should be (-5.0, -5.0). + // offset = (120 + 110 -100 - 110 - 20) / 4 + // delta = offset - 5 (last offset) + tester.route(pointer7.move(const Offset(-120.0, -120.0))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(120, 120), + // 6: Offset(110, 110), + // 7: Offset(-120, -120), + // 8: Offset(-110, -110), + // } + + // Trigger a new frame. + SchedulerBinding.instance.handleBeginFrame(const Duration(milliseconds: 100)); + SchedulerBinding.instance.handleDrawFrame(); + + // _moveDeltaBeforeFrame = { } + + log.add('-g'); + // #6 pointer move (10.0, 10.0), received delta should be (2.5, 2.5). + // offset = 10 / 4 + // delta = offset - 0 (last offset) + tester.route(pointer6.move(const Offset(120, 120))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(10, 10), + // } + + log.add('-h'); + // #8 pointer move (-10.0, -10.0), received delta should be (-2.5, -2.5). + // offset = (10 - 10) / 4 + // delta = offset - 2.5 (last offset) + tester.route(pointer8.move(const Offset(-120, -120))); + + // _moveDeltaBeforeFrame = { + // 6: Offset(10, 10), + // 8: Offset(-10, -10), + // } + + log.add('-i'); + // #5 pointer move (10.0, 10.0), received delta should be (2.5, 2.5). + // offset = (10 - 10 + 10) / 4 + // delta = offset - 0 (last offset) + tester.route(pointer5.move(const Offset(130, 130))); + + // _moveDeltaBeforeFrame = { + // 5: Offset(10, 10), + // 6: Offset(10, 10), + // 8: Offset(-10, -10), + // } + + log.add('-j'); + // #7 pointer move (-10.0, -10.0), received delta should be (-2.5, -2.5). + // offset = (10 + 10 - 10 - 10) / 4 + // delta = offset - 2.5 (last offset) + tester.route(pointer7.move(const Offset(-130.0, -130.0))); + + tester.route(pointer5.up()); + tester.route(pointer6.up()); + tester.route(pointer7.up()); + tester.route(pointer8.up()); + + // Tear down 'currentSystemFrameTimeStamp' + SchedulerBinding.instance.handleBeginFrame(Duration.zero); + SchedulerBinding.instance.handleDrawFrame(); + + expect(log, [ + '-a', + 'drag-update (Offset(100.0, 100.0))', + '-b', + 'drag-update (Offset(5.0, 5.0))', + '-c', + 'drag-update (Offset(-68.3, -68.3))', + '-d', + 'drag-update (Offset(-36.7, -36.7))', + '-e', + 'drag-update (Offset(5.0, 5.0))', + '-f', + 'drag-update (Offset(-5.0, -5.0))', + '-g', + 'drag-update (Offset(2.5, 2.5))', + '-h', + 'drag-update (Offset(-2.5, -2.5))', + '-i', + 'drag-update (Offset(2.5, 2.5))', + '-j', + 'drag-update (Offset(-2.5, -2.5))' + ]); + }); + testGesture('Clamp max velocity', (GestureTester tester) { final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; addTearDown(drag.dispose); @@ -1793,6 +2410,11 @@ void main() { expect(updatedScrollDelta, isNull); expect(didEndPan, isFalse); + tester.route(touchDown); + expect(didStartPan, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndPan, isFalse); + tester.route(touchPointer.move(const Offset(25.0, 25.0))); expect(didStartPan, isFalse); expect(updatedScrollDelta, const Offset(5.0, 5.0)); diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index 94a4d16137a..d2a06c8c518 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -3070,6 +3070,7 @@ void main() { await tester.drag( find.byType(CustomScrollView), const Offset(0.0, -20.0), + pointer: 1, ); await tester.pumpAndSettle(); final NestedScrollViewState nestedScrollView = tester.state( diff --git a/packages/flutter/test/widgets/scroll_behavior_test.dart b/packages/flutter/test/widgets/scroll_behavior_test.dart index 9a34537e123..3cca9d2e9f0 100644 --- a/packages/flutter/test/widgets/scroll_behavior_test.dart +++ b/packages/flutter/test/widgets/scroll_behavior_test.dart @@ -154,7 +154,7 @@ void main() { expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('ScrollBehavior multitouchDragStrategy test', (WidgetTester tester) async { + testWidgets('ScrollBehavior multitouchDragStrategy test - 1', (WidgetTester tester) async { const ScrollBehavior behavior1 = ScrollBehavior(); final ScrollBehavior behavior2 = const ScrollBehavior().copyWith( multitouchDragStrategy: MultitouchDragStrategy.sumAllPointers @@ -201,11 +201,11 @@ void main() { await gesture2.moveBy(const Offset(0, -50)); await tester.pump(); - // The default multitouchDragStrategy should be MultitouchDragStrategy.latestPointer. - // Only the latest active pointer be tracked. + // The default multitouchDragStrategy is 'latestPointer' or 'averageBoundaryPointers, + // the received delta should be 50.0. expect(controller.position.pixels, 50.0); - // Change to MultitouchDragStrategy.sumAllPointers. + // Change to sumAllPointers. await tester.pumpWidget(buildFrame(behavior2)); await gesture1.moveBy(const Offset(0, -50)); @@ -218,6 +218,147 @@ void main() { expect(controller.position.pixels, 50.0 + 50.0 + 50.0); }, variant: TargetPlatformVariant.all()); + testWidgets('ScrollBehavior multitouchDragStrategy test (non-Apple platforms) - 2', (WidgetTester tester) async { + const ScrollBehavior behavior1 = ScrollBehavior(); + final ScrollBehavior behavior2 = const ScrollBehavior().copyWith( + multitouchDragStrategy: MultitouchDragStrategy.averageBoundaryPointers + ); + final ScrollController controller = ScrollController(); + late BuildContext capturedContext; + addTearDown(() => controller.dispose()); + + Widget buildFrame(ScrollBehavior behavior) { + return Directionality( + textDirection: TextDirection.ltr, + child: ScrollConfiguration( + behavior: behavior, + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return ListView( + controller: controller, + children: const [ + SizedBox( + height: 1000.0, + width: 1000.0, + child: Text('I Love Flutter!'), + ), + ], + ); + }, + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(behavior1)); + + expect(controller.position.pixels, 0.0); + + final Offset listLocation = tester.getCenter(find.byType(ListView)); + + final TestGesture gesture1 = await tester.createGesture(pointer: 1); + await gesture1.down(listLocation); + await tester.pump(); + + final TestGesture gesture2 = await tester.createGesture(pointer: 2); + await gesture2.down(listLocation); + await tester.pump(); + + await gesture1.moveBy(const Offset(0, -50)); + await tester.pump(); + + await gesture2.moveBy(const Offset(0, -40)); + await tester.pump(); + + // The default multitouchDragStrategy is latestPointer. + // Only the latest active pointer be tracked. + final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); + expect(scrollBehavior.getMultitouchDragStrategy(capturedContext), MultitouchDragStrategy.latestPointer); + expect(controller.position.pixels, 40.0); + + // Change to averageBoundaryPointers. + await tester.pumpWidget(buildFrame(behavior2)); + + await gesture1.moveBy(const Offset(0, -70)); + await tester.pump(); + + await gesture2.moveBy(const Offset(0, -60)); + await tester.pump(); + + expect(controller.position.pixels, 40.0 + 70.0); + }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.windows })); + + testWidgets('ScrollBehavior multitouchDragStrategy test (Apple platforms) - 3', (WidgetTester tester) async { + const ScrollBehavior behavior1 = ScrollBehavior(); + final ScrollBehavior behavior2 = const ScrollBehavior().copyWith( + multitouchDragStrategy: MultitouchDragStrategy.latestPointer + ); + final ScrollController controller = ScrollController(); + late BuildContext capturedContext; + addTearDown(() => controller.dispose()); + + Widget buildFrame(ScrollBehavior behavior) { + return Directionality( + textDirection: TextDirection.ltr, + child: ScrollConfiguration( + behavior: behavior, + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return ListView( + controller: controller, + children: const [ + SizedBox( + height: 1000.0, + width: 1000.0, + child: Text('I Love Flutter!'), + ), + ], + ); + }, + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(behavior1)); + + expect(controller.position.pixels, 0.0); + + final Offset listLocation = tester.getCenter(find.byType(ListView)); + + final TestGesture gesture1 = await tester.createGesture(pointer: 1); + await gesture1.down(listLocation); + await tester.pump(); + + final TestGesture gesture2 = await tester.createGesture(pointer: 2); + await gesture2.down(listLocation); + await tester.pump(); + + await gesture1.moveBy(const Offset(0, -40)); + await tester.pump(); + + await gesture2.moveBy(const Offset(0, -50)); + await tester.pump(); + + // The default multitouchDragStrategy is averageBoundaryPointers. + final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); + expect(scrollBehavior.getMultitouchDragStrategy(capturedContext), MultitouchDragStrategy.averageBoundaryPointers); + expect(controller.position.pixels, 50.0); + + // Change to latestPointer. + await tester.pumpWidget(buildFrame(behavior2)); + + await gesture1.moveBy(const Offset(0, -50)); + await tester.pump(); + + await gesture2.moveBy(const Offset(0, -40)); + await tester.pump(); + + expect(controller.position.pixels, 50.0 + 40.0); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + group('ScrollBehavior configuration is maintained over multiple copies', () { testWidgets('dragDevices', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673