[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~~
This commit is contained in:
xubaolin 2024-03-13 15:10:48 +08:00 committed by GitHub
parent 1da48594bc
commit c83237f37c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1098 additions and 35 deletions

View file

@ -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<CupertinoApp> {

View file

@ -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<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
// 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<int, Offset> _moveDeltaBeforeFrame = <int, Offset>{};
// 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<int> _acceptedActivePointers = <int>[];
// 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,
}

View file

@ -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,
}

View file

@ -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<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
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;
},

View file

@ -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<PointerDeviceKind>? dragDevices,
MultitouchDragStrategy? multitouchDragStrategy,
this.multitouchDragStrategy,
Set<LogicalKeyboardKey>? 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<PointerDeviceKind>? _dragDevices;
final MultitouchDragStrategy? _multitouchDragStrategy;
final MultitouchDragStrategy? multitouchDragStrategy;
final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
MultitouchDragStrategy get multitouchDragStrategy => _multitouchDragStrategy ?? delegate.multitouchDragStrategy;
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
@override
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
MultitouchDragStrategy getMultitouchDragStrategy(BuildContext context) {
return multitouchDragStrategy ?? delegate.getMultitouchDragStrategy(context);
}
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {

View file

@ -758,7 +758,7 @@ class ScrollableState extends State<Scrollable> 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<Scrollable> 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;
},

View file

@ -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(

View file

@ -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.

View file

@ -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();

View file

@ -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,

View file

@ -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<String> log = <String>[];
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, <String>[
'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<String> log = <String>[];
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, <String>[
'-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<String> log = <String>[];
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, <String>[
'-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<String> log = <String>[];
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, <String>[
'-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));

View file

@ -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<NestedScrollViewState>(

View file

@ -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 <Widget>[
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>{ 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 <Widget>[
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>{ 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