On Linux the Home page, a library's Recommended tab and movie/show detail pages could not be scrolled at all, while Browse, Collections, Playlists, search, downloads and settings scrolled normally. Those three screens are exactly the ones whose viewport is covered by horizontal hub rows. A wheel, trackball, trackpoint or button-scroll event expresses one-axis intent, but the host reports both axes in the same event: on Linux only touchpad-sourced scrolling becomes a pan/zoom sequence that resolves in the gesture arena, and everything else arrives as one PointerScrollEvent carrying whatever dx the device produced. A Scrollable claims a scroll signal as soon as the delta along its own axis is non-zero, and the deepest claimant wins the PointerSignalResolver, so a tenth of a pixel of dx handed the whole event to the horizontal row and the page behind it never moved. Collapse a scroll signal onto its dominant axis in the binding, before hit testing - the last point where the delta can still be corrected, because a Scrollable has already registered with the resolver by the time it sees the event. Pan/zoom events are left alone, so trackpad panning keeps both axes, and a horizontal wheel or tilt still scrolls the row it points at. close #2081
117 lines
4.2 KiB
Dart
117 lines
4.2 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/utils/pointer_scroll_axis.dart';
|
|
|
|
/// The production binding's axis lock, over the standard test binding, so the
|
|
/// widget cases below exercise the same path a real wheel/trackball event takes.
|
|
class _AxisLockTestBinding extends AutomatedTestWidgetsFlutterBinding with PointerScrollAxisLock {}
|
|
|
|
void main() {
|
|
_AxisLockTestBinding();
|
|
|
|
PointerScrollEvent scrollEvent(Offset delta) => PointerScrollEvent(
|
|
viewId: 7,
|
|
timeStamp: const Duration(milliseconds: 1234),
|
|
kind: PointerDeviceKind.mouse,
|
|
device: 3,
|
|
position: const Offset(40, 60),
|
|
scrollDelta: delta,
|
|
embedderId: 9,
|
|
);
|
|
|
|
group('lockScrollSignalToDominantAxis', () {
|
|
test('keeps a single-axis signal identical', () {
|
|
final vertical = scrollEvent(const Offset(0, 53));
|
|
final horizontal = scrollEvent(const Offset(-53, 0));
|
|
final still = scrollEvent(Offset.zero);
|
|
|
|
expect(lockScrollSignalToDominantAxis(vertical), same(vertical));
|
|
expect(lockScrollSignalToDominantAxis(horizontal), same(horizontal));
|
|
expect(lockScrollSignalToDominantAxis(still), same(still));
|
|
});
|
|
|
|
test('drops the minor axis of a diagonal signal', () {
|
|
expect(lockScrollSignalToDominantAxis(scrollEvent(const Offset(1.5, 15))).scrollDelta, const Offset(0, 15));
|
|
expect(lockScrollSignalToDominantAxis(scrollEvent(const Offset(-0.1, -15))).scrollDelta, const Offset(0, -15));
|
|
expect(lockScrollSignalToDominantAxis(scrollEvent(const Offset(20, -3))).scrollDelta, const Offset(20, 0));
|
|
});
|
|
|
|
test('breaks an exact tie toward the vertical axis', () {
|
|
expect(lockScrollSignalToDominantAxis(scrollEvent(const Offset(-9, 9))).scrollDelta, const Offset(0, 9));
|
|
});
|
|
|
|
test('preserves every other field of the signal', () {
|
|
final locked = lockScrollSignalToDominantAxis(scrollEvent(const Offset(2, 30)));
|
|
|
|
expect(locked.viewId, 7);
|
|
expect(locked.timeStamp, const Duration(milliseconds: 1234));
|
|
expect(locked.kind, PointerDeviceKind.mouse);
|
|
expect(locked.device, 3);
|
|
expect(locked.position, const Offset(40, 60));
|
|
expect(locked.embedderId, 9);
|
|
});
|
|
});
|
|
|
|
group('a diagonal wheel over a nested row', () {
|
|
// The shape that froze in #2081: a vertical page whose viewport is covered
|
|
// by horizontal rows (hub sections, cast strips, season tabs).
|
|
late ScrollController pageController;
|
|
late List<ScrollController> rowControllers;
|
|
|
|
Widget buildPage() {
|
|
pageController = ScrollController();
|
|
rowControllers = List.generate(8, (_) => ScrollController());
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView.builder(
|
|
controller: pageController,
|
|
itemCount: rowControllers.length,
|
|
itemBuilder: (context, row) => SizedBox(
|
|
height: 200,
|
|
child: ListView.builder(
|
|
controller: rowControllers[row],
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: 20,
|
|
itemBuilder: (context, index) => SizedBox(width: 120, child: Text('$row-$index')),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
tearDown(() {
|
|
pageController.dispose();
|
|
for (final controller in rowControllers) {
|
|
controller.dispose();
|
|
}
|
|
});
|
|
|
|
Future<void> scrollOverRow(WidgetTester tester, Offset delta) async {
|
|
final pointer = TestPointer(1, PointerDeviceKind.mouse);
|
|
await tester.sendEventToBinding(pointer.hover(tester.getCenter(find.text('1-0'))));
|
|
await tester.sendEventToBinding(pointer.scroll(delta));
|
|
await tester.pump();
|
|
}
|
|
|
|
testWidgets('scrolls the page, not the row', (tester) async {
|
|
await tester.pumpWidget(buildPage());
|
|
|
|
await scrollOverRow(tester, const Offset(1.5, 120));
|
|
|
|
expect(pageController.offset, 120);
|
|
expect(rowControllers[1].offset, 0);
|
|
});
|
|
|
|
testWidgets('still scrolls the row when the wheel is horizontal', (tester) async {
|
|
await tester.pumpWidget(buildPage());
|
|
|
|
await scrollOverRow(tester, const Offset(120, 1.5));
|
|
|
|
expect(rowControllers[1].offset, 120);
|
|
expect(pageController.offset, 0);
|
|
});
|
|
});
|
|
}
|