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));
   }