Send scroll progress with ScrollCompletedSemanticsEvent (#12263) * Send scroll progress with ScrollCompletedSemanticsEvent This requires engine change https://github.com/flutter/engine/pull/4144 * fix analyze warning * review comment * Roll engine to 45b11f742d38ebf564a5a832b1af00661d1a31fa * fix test
diff --git a/bin/internal/engine.version b/bin/internal/engine.version index acec090..1941d4e 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version
@@ -1 +1 @@ -90ba98e741007cf249db26517ff8efea1a56057e +45b11f742d38ebf564a5a832b1af00661d1a31fa
diff --git a/packages/flutter/lib/src/rendering/semantics_event.dart b/packages/flutter/lib/src/rendering/semantics_event.dart index c47c66f..a38def8 100644 --- a/packages/flutter/lib/src/rendering/semantics_event.dart +++ b/packages/flutter/lib/src/rendering/semantics_event.dart
@@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; + /// An event that can be send by the application to notify interested listeners /// that something happened to the user interface (e.g. a view scrolled). /// @@ -39,9 +42,57 @@ /// This event should be sent after a scroll action is completed. It is /// interpreted by assistive technologies to provide additional feedback about /// the just completed scroll action to the user. - // TODO(goderbauer): add more metadata to this event (e.g. how far are we scrolled?). - ScrollCompletedSemanticsEvent() : super('scroll'); + /// + /// The parameters [axis], [pixels], [minScrollExtent], and [maxScrollExtent] are + /// required and may not be null. + ScrollCompletedSemanticsEvent({ + @required this.axis, + @required this.pixels, + @required this.maxScrollExtent, + @required this.minScrollExtent + }) : assert(axis != null), + assert(pixels != null), + assert(maxScrollExtent != null), + assert(minScrollExtent != null), + super('scroll'); + + /// The axis in which the scroll view was scrolled. + /// + /// See also [ScrollPosition.axis]. + final Axis axis; + + /// The current scroll position, in logical pixels. + /// + /// See also [ScrollPosition.pixels]. + final double pixels; + + /// The minimum in-range value for [pixels]. + /// + /// See also [ScrollPosition.minScrollExtent]. + final double minScrollExtent; + + /// The maximum in-range value for [pixels]. + /// + /// See also [ScrollPosition.maxScrollExtent]. + final double maxScrollExtent; @override - Map<String, dynamic> toMap() => <String, dynamic>{}; + Map<String, dynamic> toMap() { + final Map<String, dynamic> map = <String, dynamic>{ + 'pixels': pixels.clamp(minScrollExtent, maxScrollExtent), + 'minScrollExtent': minScrollExtent, + 'maxScrollExtent': maxScrollExtent, + }; + + switch (axis) { + case Axis.horizontal: + map['axis'] = 'h'; + break; + case Axis.vertical: + map['axis'] = 'v'; + break; + } + + return map; + } }
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 83b43a9..aba0365 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -281,7 +281,12 @@ if (_semanticsScrollEventScheduled) return; SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { - _gestureDetectorKey.currentState?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent()); + _gestureDetectorKey.currentState?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent( + axis: position.axis, + pixels: position.pixels, + minScrollExtent: position.minScrollExtent, + maxScrollExtent: position.maxScrollExtent, + )); _semanticsScrollEventScheduled = false; }); _semanticsScrollEventScheduled = true;
diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index df4d185..9437631 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart
@@ -197,7 +197,7 @@ expect(tester.getTopLeft(find.byWidget(semantics[1])).dy, kToolbarHeight); }); - testWidgets('scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async { + testWidgets('vertical scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async { final List<dynamic> messages = <dynamic>[]; SystemChannels.accessibility.setMockMessageHandler((dynamic message) { messages.add(message); @@ -218,12 +218,72 @@ expect(messages, isNot(hasLength(0))); expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue); + Map<String, Object> message = messages.last['data']; + expect(message['axis'], 'v'); + expect(message['pixels'], isPositive); + expect(message['minScrollExtent'], 0.0); + expect(message['maxScrollExtent'], 520.0); + messages.clear(); await flingDown(tester); expect(messages, isNot(hasLength(0))); expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue); + message = messages.last['data']; + expect(message['axis'], 'v'); + expect(message['pixels'], isNonNegative); + expect(message['minScrollExtent'], 0.0); + expect(message['maxScrollExtent'], 520.0); + + semantics.dispose(); + }); + + testWidgets('horizontal scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async { + final List<dynamic> messages = <dynamic>[]; + SystemChannels.accessibility.setMockMessageHandler((dynamic message) { + messages.add(message); + }); + + final SemanticsTester semantics = new SemanticsTester(tester); + + final List<Widget> children = <Widget>[]; + for (int i = 0; i < 80; i++) + children.add(new Container( + child: new Text('$i'), + width: 100.0, + )); + await tester.pumpWidget(new Directionality( + textDirection: TextDirection.ltr, + child: new ListView( + children: children, + scrollDirection: Axis.horizontal, + ), + )); + + await flingLeft(tester); + + expect(messages, isNot(hasLength(0))); + expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue); + + Map<String, Object> message = messages.last['data']; + expect(message['axis'], 'h'); + expect(message['pixels'], isPositive); + expect(message['minScrollExtent'], 0.0); + expect(message['maxScrollExtent'], 7200.0); + + messages.clear(); + await flingRight(tester); + + expect(messages, isNot(hasLength(0))); + expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue); + + message = messages.last['data']; + expect(message['axis'], 'h'); + expect(message['pixels'], isNonNegative); + expect(message['minScrollExtent'], 0.0); + expect(message['maxScrollExtent'], 7200.0); + semantics.dispose(); }); @@ -255,17 +315,17 @@ }); } -Future<Null> flingUp(WidgetTester tester, { int repetitions: 1 }) async { - while (repetitions-- > 0) { - await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 1000.0); - await tester.pump(); - await tester.pump(const Duration(seconds: 5)); - } -} +Future<Null> flingUp(WidgetTester tester, { int repetitions: 1 }) => fling(tester, const Offset(0.0, -200.0), repetitions); -Future<Null> flingDown(WidgetTester tester, { int repetitions: 1 }) async { +Future<Null> flingDown(WidgetTester tester, { int repetitions: 1 }) => fling(tester, const Offset(0.0, 200.0), repetitions); + +Future<Null> flingRight(WidgetTester tester, { int repetitions: 1 }) => fling(tester, const Offset(200.0, 0.0), repetitions); + +Future<Null> flingLeft(WidgetTester tester, { int repetitions: 1 }) => fling(tester, const Offset(-200.0, 0.0), repetitions); + +Future<Null> fling(WidgetTester tester, Offset offset, int repetitions) async { while (repetitions-- > 0) { - await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 1000.0); + await tester.fling(find.byType(ListView), offset, 1000.0); await tester.pump(); await tester.pump(const Duration(seconds: 5)); }