mirror of
https://github.com/flutter/flutter
synced 2024-08-25 11:07:06 +00:00
[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:
parent
1da48594bc
commit
c83237f37c
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue