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/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart' show LogicalKeyboardKey;
|
||||
|
||||
import 'framework.dart';
|
||||
import 'overscroll_indicator.dart';
|
||||
@ -100,6 +101,7 @@ class ScrollBehavior {
|
||||
bool? scrollbars,
|
||||
bool? overscroll,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||
ScrollPhysics? physics,
|
||||
TargetPlatform? platform,
|
||||
@Deprecated(
|
||||
@ -112,9 +114,10 @@ class ScrollBehavior {
|
||||
delegate: this,
|
||||
scrollbars: scrollbars ?? true,
|
||||
overscroll: overscroll ?? true,
|
||||
dragDevices: dragDevices,
|
||||
pointerAxisModifiers: pointerAxisModifiers,
|
||||
physics: physics,
|
||||
platform: platform,
|
||||
dragDevices: dragDevices,
|
||||
androidOverscrollIndicator: androidOverscrollIndicator
|
||||
);
|
||||
}
|
||||
@ -132,6 +135,25 @@ class ScrollBehavior {
|
||||
/// impossible to select text in scrollable containers and is not recommended.
|
||||
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.
|
||||
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
|
||||
// When modifying this function, consider modifying the implementation in
|
||||
@ -261,12 +283,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
||||
required this.delegate,
|
||||
this.scrollbars = true,
|
||||
this.overscroll = true,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||
this.physics,
|
||||
this.platform,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||
}) : _androidOverscrollIndicator = androidOverscrollIndicator,
|
||||
_dragDevices = dragDevices;
|
||||
_dragDevices = dragDevices,
|
||||
_pointerAxisModifiers = pointerAxisModifiers;
|
||||
|
||||
final ScrollBehavior delegate;
|
||||
final bool scrollbars;
|
||||
@ -274,12 +298,16 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
||||
final ScrollPhysics? physics;
|
||||
final TargetPlatform? platform;
|
||||
final Set<PointerDeviceKind>? _dragDevices;
|
||||
final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
|
||||
@override
|
||||
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
|
||||
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
|
||||
|
||||
@override
|
||||
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
|
||||
|
||||
@override
|
||||
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
|
||||
|
||||
@ -303,17 +331,19 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
||||
ScrollBehavior copyWith({
|
||||
bool? scrollbars,
|
||||
bool? overscroll,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
Set<LogicalKeyboardKey>? pointerAxisModifiers,
|
||||
ScrollPhysics? physics,
|
||||
TargetPlatform? platform,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
AndroidOverscrollIndicator? androidOverscrollIndicator
|
||||
}) {
|
||||
return delegate.copyWith(
|
||||
scrollbars: scrollbars ?? this.scrollbars,
|
||||
overscroll: overscroll ?? this.overscroll,
|
||||
dragDevices: dragDevices ?? this.dragDevices,
|
||||
pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
|
||||
physics: physics ?? this.physics,
|
||||
platform: platform ?? this.platform,
|
||||
dragDevices: dragDevices ?? this.dragDevices,
|
||||
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
||||
);
|
||||
}
|
||||
@ -333,9 +363,10 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|
||||
return oldDelegate.delegate.runtimeType != delegate.runtimeType
|
||||
|| oldDelegate.scrollbars != scrollbars
|
||||
|| oldDelegate.overscroll != overscroll
|
||||
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|
||||
|| !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
|
||||
|| oldDelegate.physics != physics
|
||||
|| oldDelegate.platform != platform
|
||||
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|
||||
|| 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
|
||||
// direction taken into account.
|
||||
// Returns the delta that should result from applying [event] with axis,
|
||||
// direction, and any modifiers specified by the ScrollBehavior taken into
|
||||
// account.
|
||||
double _pointerSignalEventDelta(PointerScrollEvent event) {
|
||||
double delta = widget.axis == Axis.horizontal
|
||||
late double delta;
|
||||
final Set<LogicalKeyboardKey> pressed = HardwareKeyboard.instance.logicalKeysPressed;
|
||||
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)) {
|
||||
delta *= -1;
|
||||
|
@ -16,13 +16,17 @@ Future<void> pumpTest(
|
||||
TargetPlatform? platform, {
|
||||
bool scrollable = true,
|
||||
bool reverse = false,
|
||||
Set<LogicalKeyboardKey>? axisModifier,
|
||||
Axis scrollDirection = Axis.vertical,
|
||||
ScrollController? controller,
|
||||
bool enableMouseDrag = true,
|
||||
}) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
|
||||
scrollBehavior: const NoScrollbarBehavior().copyWith(
|
||||
dragDevices: enableMouseDrag
|
||||
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
|
||||
: null,
|
||||
pointerAxisModifiers: axisModifier,
|
||||
),
|
||||
theme: ThemeData(
|
||||
platform: platform,
|
||||
@ -30,9 +34,13 @@ Future<void> pumpTest(
|
||||
home: CustomScrollView(
|
||||
controller: controller,
|
||||
reverse: reverse,
|
||||
scrollDirection: scrollDirection,
|
||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
slivers: const <Widget>[
|
||||
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
|
||||
slivers: <Widget>[
|
||||
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);
|
||||
});
|
||||
|
||||
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: ', () {
|
||||
Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) {
|
||||
return tester.pumpWidget(
|
||||
|
Loading…
x
Reference in New Issue
Block a user