Re-apply "Send nextId in addition to previousId for traversal order" (#14694)

Re-apply "Send nextId in addition to previousId for traversal order (#14676)"
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 5dadd8a..4a273cc 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-f5a4a9378740c3d5996583a9ed1f7e28ff08ee85
+05c5f817eb731569ddaad6397ee9ce000d37f2f1
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index 5852abe..079740d 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -92,6 +92,7 @@
     @required this.decreasedValue,
     @required this.hint,
     @required this.textDirection,
+    @required this.nextNodeId,
     @required this.previousNodeId,
     @required this.rect,
     @required this.textSelection,
@@ -151,6 +152,10 @@
   /// [increasedValue], and [decreasedValue].
   final TextDirection textDirection;
 
+  /// The index indicating the ID of the next node in the traversal order after
+  /// this node for the platform's accessibility services.
+  final int nextNodeId;
+
   /// The index indicating the ID of the previous node in the traversal order before
   /// this node for the platform's accessibility services.
   final int previousNodeId;
@@ -237,6 +242,7 @@
     properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
     properties.add(new StringProperty('hint', hint, defaultValue: ''));
     properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
+    properties.add(new IntProperty('nextNodeId', nextNodeId, defaultValue: null));
     properties.add(new IntProperty('previousNodeId', previousNodeId, defaultValue: null));
     if (textSelection?.isValid == true)
       properties.add(new MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]'));
@@ -258,6 +264,7 @@
         && typedOther.decreasedValue == decreasedValue
         && typedOther.hint == hint
         && typedOther.textDirection == textDirection
+        && typedOther.nextNodeId == nextNodeId
         && typedOther.previousNodeId == previousNodeId
         && typedOther.rect == rect
         && setEquals(typedOther.tags, tags)
@@ -269,7 +276,7 @@
   }
 
   @override
-  int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, previousNodeId, rect, tags, textSelection, scrollPosition, scrollExtentMax, scrollExtentMin, transform);
+  int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, nextNodeId, previousNodeId, rect, tags, textSelection, scrollPosition, scrollExtentMax, scrollExtentMin, transform);
 }
 
 class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
@@ -1067,10 +1074,30 @@
 
   /// The sort order for ordering the traversal of [SemanticsNode]s by the
   /// platform's accessibility services (e.g. VoiceOver on iOS and TalkBack on
-  /// Android). This is used to determine the [previousNodeId] during a semantics update.
+  /// Android). This is used to determine the [nextNodeId] and [previousNodeId]
+  /// during a semantics update.
   SemanticsSortOrder _sortOrder;
   SemanticsSortOrder get sortOrder => _sortOrder;
 
+  /// The ID of the next node in the traversal order after this node.
+  ///
+  /// Only valid after at least one semantics update has been built.
+  ///
+  /// This is the value passed to the engine to tell it what the order
+  /// should be for traversing semantics nodes.
+  ///
+  /// If this is set to -1, it will indicate that there is no next node to
+  /// the engine (i.e. this is the last node in the sort order). When it is
+  /// null, it means that no semantics update has been built yet.
+  int _nextNodeId;
+  void _updateNextNodeId(int value) {
+    if (value == _nextNodeId)
+      return;
+    _nextNodeId = value;
+    _markDirty();
+  }
+  int get nextNodeId => _nextNodeId;
+
   /// The ID of the previous node in the traversal order before this node.
   ///
   /// Only valid after at least one semantics update has been built.
@@ -1194,6 +1221,7 @@
     String increasedValue = _increasedValue;
     String decreasedValue = _decreasedValue;
     TextDirection textDirection = _textDirection;
+    int nextNodeId = _nextNodeId;
     int previousNodeId = _previousNodeId;
     Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
     TextSelection textSelection = _textSelection;
@@ -1207,6 +1235,7 @@
         flags |= node._flags;
         actions |= node._actionsAsBits;
         textDirection ??= node._textDirection;
+        nextNodeId ??= node._nextNodeId;
         previousNodeId ??= node._previousNodeId;
         textSelection ??= node._textSelection;
         scrollPosition ??= node._scrollPosition;
@@ -1247,6 +1276,7 @@
       decreasedValue: decreasedValue,
       hint: hint,
       textDirection: textDirection,
+      nextNodeId: nextNodeId,
       previousNodeId: previousNodeId,
       rect: rect,
       transform: transform,
@@ -1289,6 +1319,7 @@
       increasedValue: data.increasedValue,
       hint: data.hint,
       textDirection: data.textDirection,
+      nextNodeId: data.nextNodeId,
       previousNodeId: data.previousNodeId,
       textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
       textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
@@ -1363,6 +1394,7 @@
     properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
     properties.add(new StringProperty('hint', _hint, defaultValue: ''));
     properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
+    properties.add(new IntProperty('nextNodeId', _nextNodeId, defaultValue: null));
     properties.add(new IntProperty('previousNodeId', _previousNodeId, defaultValue: null));
     properties.add(new DiagnosticsProperty<SemanticsSortOrder>('sortOrder', sortOrder, defaultValue: null));
     if (_textSelection?.isValid == true)
@@ -1539,10 +1571,10 @@
     super.dispose();
   }
 
-  // Updates the previousNodeId IDs on the semantics nodes. These IDs are used
-  // on the platform side to order the nodes for traversal by the accessibility
-  // services. If the previousNodeId for a node changes, the node will be marked as
-  // dirty.
+  // Updates the nextNodeId and previousNodeId IDs on the semantics nodes. These
+  // IDs are used on the platform side to order the nodes for traversal by the
+  // accessibility services. If the nextNodeId or previousNodeId for a node
+  // changes, the node will be marked as dirty.
   void _updateTraversalOrder() {
     final List<_TraversalSortNode> nodesInSemanticsTraversalOrder = <_TraversalSortNode>[];
     SemanticsSortOrder currentSortOrder = new SemanticsSortOrder(keys: <SemanticsSortKey>[]);
@@ -1577,12 +1609,20 @@
       return true;
     }
     rootSemanticsNode.visitChildren(visitor);
+
+    if (nodesInSemanticsTraversalOrder.isEmpty)
+      return;
+
     nodesInSemanticsTraversalOrder.sort();
-    int previousNodeId = -1;
-    for (_TraversalSortNode node in nodesInSemanticsTraversalOrder) {
-      node.node._updatePreviousNodeId(previousNodeId);
-      previousNodeId = node.node.id;
+    _TraversalSortNode node = nodesInSemanticsTraversalOrder.removeLast();
+    node.node._updateNextNodeId(-1);
+    while (nodesInSemanticsTraversalOrder.isNotEmpty) {
+      final _TraversalSortNode previousNode = nodesInSemanticsTraversalOrder.removeLast();
+      node.node._updatePreviousNodeId(previousNode.node.id);
+      previousNode.node._updateNextNodeId(node.node.id);
+      node = previousNode;
     }
+    node.node._updatePreviousNodeId(-1);
   }
 
   /// Update the semantics using [Window.updateSemantics].
diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart
index 7647d72..df8abd6 100644
--- a/packages/flutter/test/semantics/semantics_test.dart
+++ b/packages/flutter/test/semantics/semantics_test.dart
@@ -348,7 +348,7 @@
 
     expect(
       minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
-      'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, invisible, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null, previousNodeId: null, sortOrder: null, scrollExtentMin: null, scrollPosition: null, scrollExtentMax: null)\n'
+      'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, invisible, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null, nextNodeId: null, previousNodeId: null, sortOrder: null, scrollExtentMin: null, scrollPosition: null, scrollExtentMax: null)\n'
     );
 
     final SemanticsConfiguration config = new SemanticsConfiguration()
diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart
index 9c11b5a..ae925ced 100644
--- a/packages/flutter/test/widgets/semantics_test.dart
+++ b/packages/flutter/test/widgets/semantics_test.dart
@@ -417,6 +417,7 @@
           rect: TestSemantics.fullScreen,
           actions: allActions.fold(0, (int previous, SemanticsAction action) => previous | action.index),
           previousNodeId: -1,
+          nextNodeId: -1,
         ),
       ],
     );
@@ -613,36 +614,51 @@
     expect(semanticsUpdateCount, 1);
     expect(semantics, hasSemantics(
       new TestSemantics(
+        id: 0,
         children: <TestSemantics>[
           new TestSemantics(
+            id: 2,
+            nextNodeId: 5,
             previousNodeId: -1,
             children: <TestSemantics>[
               new TestSemantics(
+                id: 3,
                 label: r'Label 1',
                 textDirection: TextDirection.ltr,
+                nextNodeId: -1,
                 previousNodeId: 4,
               ),
               new TestSemantics(
+                id: 4,
                 label: r'Label 2',
                 textDirection: TextDirection.ltr,
+                nextNodeId: 3,
                 previousNodeId: 6,
               ),
               new TestSemantics(
+                id: 5,
+                nextNodeId: 8,
                 previousNodeId: 2,
                 children: <TestSemantics>[
                   new TestSemantics(
+                    id: 6,
                     label: r'Label 3',
                     textDirection: TextDirection.ltr,
+                    nextNodeId: 4,
                     previousNodeId: 7,
                   ),
                   new TestSemantics(
+                    id: 7,
                     label: r'Label 4',
                     textDirection: TextDirection.ltr,
+                    nextNodeId: 6,
                     previousNodeId: 8,
                   ),
                   new TestSemantics(
+                    id: 8,
                     label: r'Label 5',
                     textDirection: TextDirection.ltr,
+                    nextNodeId: 7,
                     previousNodeId: 5,
                   ),
                 ],
@@ -650,7 +666,7 @@
             ],
           ),
         ],
-      ), ignoreTransform: true, ignoreRect: true, ignoreId: true),
+      ), ignoreTransform: true, ignoreRect: true),
     );
     semantics.dispose();
   });
@@ -713,26 +729,31 @@
           new TestSemantics(
             label: r'Label 1',
             textDirection: TextDirection.ltr,
+            nextNodeId: -1,
             previousNodeId: 3,
           ),
           new TestSemantics(
             label: r'Label 2',
             textDirection: TextDirection.ltr,
+            nextNodeId: 2,
             previousNodeId: 4,
           ),
           new TestSemantics(
             label: r'Label 3',
             textDirection: TextDirection.ltr,
+            nextNodeId: 3,
             previousNodeId: 5,
           ),
           new TestSemantics(
             label: r'Label 4',
             textDirection: TextDirection.ltr,
+            nextNodeId: 4,
             previousNodeId: 6,
           ),
           new TestSemantics(
             label: r'Label 5',
             textDirection: TextDirection.ltr,
+            nextNodeId: 5,
             previousNodeId: -1,
           ),
         ],
@@ -802,31 +823,37 @@
       new TestSemantics(
         children: <TestSemantics>[
           new TestSemantics(
+            nextNodeId: 5,
             previousNodeId: -1,
             children: <TestSemantics>[
               new TestSemantics(
                 label: r'Label 1',
                 textDirection: TextDirection.ltr,
+                nextNodeId: 7,
                 previousNodeId: 5,
               ),
               new TestSemantics(
                 label: r'Label 2',
                 textDirection: TextDirection.ltr,
+                nextNodeId: -1,
                 previousNodeId: 6,
               ),
               new TestSemantics(
                 label: r'Label 3',
                 textDirection: TextDirection.ltr,
+                nextNodeId: 3,
                 previousNodeId: 2,
               ),
               new TestSemantics(
                 label: r'Label 4',
                 textDirection: TextDirection.ltr,
+                nextNodeId: 4,
                 previousNodeId: 7,
               ),
               new TestSemantics(
                 label: r'Label 5',
                 textDirection: TextDirection.ltr,
+                nextNodeId: 6,
                 previousNodeId: 3,
               ),
             ],
diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart
index b6a5233..440d9d4 100644
--- a/packages/flutter/test/widgets/semantics_tester.dart
+++ b/packages/flutter/test/widgets/semantics_tester.dart
@@ -43,6 +43,7 @@
     this.decreasedValue: '',
     this.hint: '',
     this.textDirection,
+    this.nextNodeId,
     this.previousNodeId,
     this.rect,
     this.transform,
@@ -71,6 +72,7 @@
     this.hint: '',
     this.textDirection,
     this.previousNodeId,
+    this.nextNodeId,
     this.transform,
     this.textSelection,
     this.children: const <TestSemantics>[],
@@ -106,6 +108,7 @@
     this.increasedValue: '',
     this.decreasedValue: '',
     this.textDirection,
+    this.nextNodeId,
     this.previousNodeId,
     this.rect,
     Matrix4 transform,
@@ -173,6 +176,10 @@
   /// is also set.
   final TextDirection textDirection;
 
+  /// The ID of the node that is next in the semantics traversal order after
+  /// this node.
+  final int nextNodeId;
+
   /// The ID of the node that is previous in the semantics traversal order before
   /// this node.
   final int previousNodeId;
@@ -258,6 +265,8 @@
       return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
     if (textDirection != null && textDirection != nodeData.textDirection)
       return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
+    if (nextNodeId != null && nextNodeId != nodeData.nextNodeId)
+      return fail('expected node id $id to have nextNodeId "$nextNodeId" but found "${nodeData.nextNodeId}".');
     if (previousNodeId != null && previousNodeId != nodeData.previousNodeId)
       return fail('expected node id $id to have previousNodeId "$previousNodeId" but found "${nodeData.previousNodeId}".');
     if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '' || node.increasedValue != '' || node.decreasedValue != '') && nodeData.textDirection == null)
@@ -311,6 +320,8 @@
       buf.writeln('$indent  hint: \'$hint\',');
     if (textDirection != null)
       buf.writeln('$indent  textDirection: $textDirection,');
+    if (nextNodeId != null)
+      buf.writeln('$indent  nextNodeId: $nextNodeId,');
     if (previousNodeId != null)
       buf.writeln('$indent  previousNodeId: $previousNodeId,');
     if (textSelection?.isValid == true)
@@ -522,6 +533,8 @@
       buf.writeln('  hint: r\'${node.hint}\',');
     if (node.textDirection != null)
       buf.writeln('  textDirection: ${node.textDirection},');
+    if (node.nextNodeId != null)
+      buf.writeln('  nextNodeId: ${node.nextNodeId},');
     if (node.previousNodeId != null)
       buf.writeln('  previousNodeId: ${node.previousNodeId},');
 
diff --git a/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart b/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart
index 4afd069..59a1895 100644
--- a/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart
+++ b/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart
@@ -105,14 +105,17 @@
         new TestSemantics(
           children: <TestSemantics>[
             new TestSemantics(
+              nextNodeId: 4,
               previousNodeId: -1,
               children: <TestSemantics>[
                 new TestSemantics(
+                  nextNodeId: 2,
                   previousNodeId: 1,
                   children: <TestSemantics>[
                     new TestSemantics(
                       label: r'Plain text',
                       textDirection: TextDirection.ltr,
+                      nextNodeId: 3,
                       previousNodeId: 4,
                     ),
                     new TestSemantics(
@@ -124,6 +127,7 @@
                       decreasedValue: r'test-decreasedValue',
                       hint: r'test-hint',
                       textDirection: TextDirection.rtl,
+                      nextNodeId: -1,
                       previousNodeId: 2,
                     ),
                   ],