Support flipping mouse scrolling axes through modifier keys (#115610)
* Maybe maybe * Nit * One more nit * ++ * Fix test * REview feedback * Add comment about ios * ++ * Doc nit * Handle trackpads * Review feedback
This commit is contained in:
parent
54405bfa38
commit
e69ea6dee4
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart' show LogicalKeyboardKey;
|
||||||
|
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'overscroll_indicator.dart';
|
import 'overscroll_indicator.dart';
|
||||||
@ -100,6 +101,7 @@ class ScrollBehavior {
|
|||||||
bool? scrollbars,
|
bool? scrollbars,
|
||||||
bool? overscroll,
|
bool? overscroll,
|
||||||
Set<PointerDeviceKind>? dragDevices,
|
Set<PointerDeviceKind>? dragDevices,
|
||||||
|
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||||
ScrollPhysics? physics,
|
ScrollPhysics? physics,
|
||||||
TargetPlatform? platform,
|
TargetPlatform? platform,
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
@ -112,9 +114,10 @@ class ScrollBehavior {
|
|||||||
delegate: this,
|
delegate: this,
|
||||||
scrollbars: scrollbars ?? true,
|
scrollbars: scrollbars ?? true,
|
||||||
overscroll: overscroll ?? true,
|
overscroll: overscroll ?? true,
|
||||||
|
dragDevices: dragDevices,
|
||||||
|
pointerAxisModifiers: pointerAxisModifiers,
|
||||||
physics: physics,
|
physics: physics,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
dragDevices: dragDevices,
|
|
||||||
androidOverscrollIndicator: androidOverscrollIndicator
|
androidOverscrollIndicator: androidOverscrollIndicator
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -132,6 +135,25 @@ class ScrollBehavior {
|
|||||||
/// impossible to select text in scrollable containers and is not recommended.
|
/// impossible to select text in scrollable containers and is not recommended.
|
||||||
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
|
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
|
||||||
|
|
||||||
|
/// A set of [LogicalKeyboardKey]s that, when any or all are pressed in
|
||||||
|
/// combination with a [PointerDeviceKind.mouse] pointer scroll event, will
|
||||||
|
/// flip the axes of the scroll input.
|
||||||
|
///
|
||||||
|
/// This will for example, result in the input of a vertical mouse wheel, to
|
||||||
|
/// move the [ScrollPosition] of a [ScrollView] with an [Axis.horizontal]
|
||||||
|
/// scroll direction.
|
||||||
|
///
|
||||||
|
/// If other keys exclusive of this set are pressed during a scroll event, in
|
||||||
|
/// conjunction with keys from this set, the scroll input will still be
|
||||||
|
/// flipped.
|
||||||
|
///
|
||||||
|
/// Defaults to [LogicalKeyboardKey.shiftLeft],
|
||||||
|
/// [LogicalKeyboardKey.shiftRight].
|
||||||
|
Set<LogicalKeyboardKey> get pointerAxisModifiers => <LogicalKeyboardKey>{
|
||||||
|
LogicalKeyboardKey.shiftLeft,
|
||||||
|
LogicalKeyboardKey.shiftRight,
|
||||||
|
};
|
||||||
|
|
||||||
/// Applies a [RawScrollbar] to the child widget on desktop platforms.
|
/// Applies a [RawScrollbar] to the child widget on desktop platforms.
|
||||||
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
|
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
// When modifying this function, consider modifying the implementation in
|
// When modifying this function, consider modifying the implementation in
|
||||||
@ -261,12 +283,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
|||||||
required this.delegate,
|
required this.delegate,
|
||||||
this.scrollbars = true,
|
this.scrollbars = true,
|
||||||
this.overscroll = true,
|
this.overscroll = true,
|
||||||
|
Set<PointerDeviceKind>? dragDevices,
|
||||||
|
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.platform,
|
this.platform,
|
||||||
Set<PointerDeviceKind>? dragDevices,
|
|
||||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||||
}) : _androidOverscrollIndicator = androidOverscrollIndicator,
|
}) : _androidOverscrollIndicator = androidOverscrollIndicator,
|
||||||
_dragDevices = dragDevices;
|
_dragDevices = dragDevices,
|
||||||
|
_pointerAxisModifiers = pointerAxisModifiers;
|
||||||
|
|
||||||
final ScrollBehavior delegate;
|
final ScrollBehavior delegate;
|
||||||
final bool scrollbars;
|
final bool scrollbars;
|
||||||
@ -274,12 +298,16 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
|||||||
final ScrollPhysics? physics;
|
final ScrollPhysics? physics;
|
||||||
final TargetPlatform? platform;
|
final TargetPlatform? platform;
|
||||||
final Set<PointerDeviceKind>? _dragDevices;
|
final Set<PointerDeviceKind>? _dragDevices;
|
||||||
|
final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
|
||||||
@override
|
@override
|
||||||
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
|
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
|
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
|
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
|
||||||
|
|
||||||
@ -303,17 +331,19 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
|||||||
ScrollBehavior copyWith({
|
ScrollBehavior copyWith({
|
||||||
bool? scrollbars,
|
bool? scrollbars,
|
||||||
bool? overscroll,
|
bool? overscroll,
|
||||||
|
Set<PointerDeviceKind>? dragDevices,
|
||||||
|
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||||
ScrollPhysics? physics,
|
ScrollPhysics? physics,
|
||||||
TargetPlatform? platform,
|
TargetPlatform? platform,
|
||||||
Set<PointerDeviceKind>? dragDevices,
|
|
||||||
AndroidOverscrollIndicator? androidOverscrollIndicator
|
AndroidOverscrollIndicator? androidOverscrollIndicator
|
||||||
}) {
|
}) {
|
||||||
return delegate.copyWith(
|
return delegate.copyWith(
|
||||||
scrollbars: scrollbars ?? this.scrollbars,
|
scrollbars: scrollbars ?? this.scrollbars,
|
||||||
overscroll: overscroll ?? this.overscroll,
|
overscroll: overscroll ?? this.overscroll,
|
||||||
|
dragDevices: dragDevices ?? this.dragDevices,
|
||||||
|
pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
|
||||||
physics: physics ?? this.physics,
|
physics: physics ?? this.physics,
|
||||||
platform: platform ?? this.platform,
|
platform: platform ?? this.platform,
|
||||||
dragDevices: dragDevices ?? this.dragDevices,
|
|
||||||
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -333,9 +363,10 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
|||||||
return oldDelegate.delegate.runtimeType != delegate.runtimeType
|
return oldDelegate.delegate.runtimeType != delegate.runtimeType
|
||||||
|| oldDelegate.scrollbars != scrollbars
|
|| oldDelegate.scrollbars != scrollbars
|
||||||
|| oldDelegate.overscroll != overscroll
|
|| oldDelegate.overscroll != overscroll
|
||||||
|
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|
||||||
|
|| !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
|
||||||
|| oldDelegate.physics != physics
|
|| oldDelegate.physics != physics
|
||||||
|| oldDelegate.platform != platform
|
|| oldDelegate.platform != platform
|
||||||
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|
|
||||||
|| delegate.shouldNotify(oldDelegate.delegate);
|
|| delegate.shouldNotify(oldDelegate.delegate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,12 +756,32 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the delta that should result from applying [event] with axis and
|
// Returns the delta that should result from applying [event] with axis,
|
||||||
// direction taken into account.
|
// direction, and any modifiers specified by the ScrollBehavior taken into
|
||||||
|
// account.
|
||||||
double _pointerSignalEventDelta(PointerScrollEvent event) {
|
double _pointerSignalEventDelta(PointerScrollEvent event) {
|
||||||
double delta = widget.axis == Axis.horizontal
|
late double delta;
|
||||||
? event.scrollDelta.dx
|
final Set<LogicalKeyboardKey> pressed = HardwareKeyboard.instance.logicalKeysPressed;
|
||||||
: event.scrollDelta.dy;
|
final bool flipAxes = pressed.any(_configuration.pointerAxisModifiers.contains) &&
|
||||||
|
// Axes are only flipped for physical mouse wheel input.
|
||||||
|
// On some platforms, like web, trackpad input is handled through pointer
|
||||||
|
// signals, but should not be included in this axis modifying behavior.
|
||||||
|
// This is because on a trackpad, all directional axes are available to
|
||||||
|
// the user, while mouse scroll wheels typically are restricted to one
|
||||||
|
// axis.
|
||||||
|
event.kind == PointerDeviceKind.mouse;
|
||||||
|
|
||||||
|
switch (widget.axis) {
|
||||||
|
case Axis.horizontal:
|
||||||
|
delta = flipAxes
|
||||||
|
? event.scrollDelta.dy
|
||||||
|
: event.scrollDelta.dx;
|
||||||
|
break;
|
||||||
|
case Axis.vertical:
|
||||||
|
delta = flipAxes
|
||||||
|
? event.scrollDelta.dx
|
||||||
|
: event.scrollDelta.dy;
|
||||||
|
}
|
||||||
|
|
||||||
if (axisDirectionIsReversed(widget.axisDirection)) {
|
if (axisDirectionIsReversed(widget.axisDirection)) {
|
||||||
delta *= -1;
|
delta *= -1;
|
||||||
|
@ -16,13 +16,17 @@ Future<void> pumpTest(
|
|||||||
TargetPlatform? platform, {
|
TargetPlatform? platform, {
|
||||||
bool scrollable = true,
|
bool scrollable = true,
|
||||||
bool reverse = false,
|
bool reverse = false,
|
||||||
|
Set<LogicalKeyboardKey>? axisModifier,
|
||||||
|
Axis scrollDirection = Axis.vertical,
|
||||||
ScrollController? controller,
|
ScrollController? controller,
|
||||||
bool enableMouseDrag = true,
|
bool enableMouseDrag = true,
|
||||||
}) async {
|
}) async {
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
|
scrollBehavior: const NoScrollbarBehavior().copyWith(
|
||||||
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
|
dragDevices: enableMouseDrag
|
||||||
: null,
|
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
|
||||||
|
: null,
|
||||||
|
pointerAxisModifiers: axisModifier,
|
||||||
),
|
),
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
platform: platform,
|
platform: platform,
|
||||||
@ -30,9 +34,13 @@ Future<void> pumpTest(
|
|||||||
home: CustomScrollView(
|
home: CustomScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
reverse: reverse,
|
reverse: reverse,
|
||||||
|
scrollDirection: scrollDirection,
|
||||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||||
slivers: const <Widget>[
|
slivers: <Widget>[
|
||||||
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
|
SliverToBoxAdapter(child: SizedBox(
|
||||||
|
height: scrollDirection == Axis.vertical ? 2000.0 : null,
|
||||||
|
width: scrollDirection == Axis.horizontal ? 2000.0 : null,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@ -399,6 +407,118 @@ void main() {
|
|||||||
expect(getScrollOffset(tester), 20.0);
|
expect(getScrollOffset(tester), 20.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Scrolls horizontally when shift is pressed by default', (WidgetTester tester) async {
|
||||||
|
await pumpTest(
|
||||||
|
tester,
|
||||||
|
debugDefaultTargetPlatformOverride,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||||
|
testPointer.hover(scrollEventLocation);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input flipped to horizontal and accepted.
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
testWidgets('Scroll axis is not flipped for trackpad', (WidgetTester tester) async {
|
||||||
|
await pumpTest(
|
||||||
|
tester,
|
||||||
|
debugDefaultTargetPlatformOverride,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.trackpad);
|
||||||
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||||
|
testPointer.hover(scrollEventLocation);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not flipped.
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
testWidgets('Scrolls horizontally when custom key is pressed', (WidgetTester tester) async {
|
||||||
|
await pumpTest(
|
||||||
|
tester,
|
||||||
|
debugDefaultTargetPlatformOverride,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
axisModifier: <LogicalKeyboardKey>{ LogicalKeyboardKey.altLeft },
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||||
|
testPointer.hover(scrollEventLocation);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input flipped to horizontal and accepted.
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
testWidgets('Still scrolls horizontally when other keys are pressed at the same time', (WidgetTester tester) async {
|
||||||
|
await pumpTest(
|
||||||
|
tester,
|
||||||
|
debugDefaultTargetPlatformOverride,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
axisModifier: <LogicalKeyboardKey>{ LogicalKeyboardKey.altLeft },
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||||
|
testPointer.hover(scrollEventLocation);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 0.0);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical flipped & accepted.
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
||||||
|
// Vertical input not accepted
|
||||||
|
expect(getScrollOffset(tester), 20.0);
|
||||||
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
group('setCanDrag to false with active drag gesture: ', () {
|
group('setCanDrag to false with active drag gesture: ', () {
|
||||||
Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) {
|
Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) {
|
||||||
return tester.pumpWidget(
|
return tester.pumpWidget(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user