Reland: Add OrderedFocusTraversalPolicy and FocusTraversalGrou… (#50672)

This re-lands #49235 with the addition of includeSemantics flag on the Focus widget so that the FocusTraversalGroup can create a Focus widget without affecting the semantics tree.

The FocusTraversalGroup uses the Focus widget to create a grouping of descendants for traversal, but doesn't actually participate in focus (canRequestFocus is always false), so we don't want it to add a Semantics widget in that case, since that can cause semantics changes. The canRequestFocus attribute can also be used when a widget is disabled, so we do sometimes want to include Semantics even if that is false, but not in the case where it is always false, as for FocusTraversalGroup.

- Added a test to make sure that FocusTraversalGroup doesn't add any semantics information.
diff --git a/dev/manual_tests/lib/actions.dart b/dev/manual_tests/lib/actions.dart
index 1ee6150..02839ff 100644
--- a/dev/manual_tests/lib/actions.dart
+++ b/dev/manual_tests/lib/actions.dart
@@ -435,7 +435,7 @@
         kUndoActionKey: () => kUndoAction,
         kRedoActionKey: () => kRedoAction,
       },
-      child: DefaultFocusTraversal(
+      child: FocusTraversalGroup(
         policy: ReadingOrderTraversalPolicy(),
         child: Shortcuts(
           shortcuts: <LogicalKeySet, Intent>{
diff --git a/dev/manual_tests/lib/focus.dart b/dev/manual_tests/lib/focus.dart
index 361988e..00e5c2d 100644
--- a/dev/manual_tests/lib/focus.dart
+++ b/dev/manual_tests/lib/focus.dart
@@ -142,7 +142,7 @@
   Widget build(BuildContext context) {
     final TextTheme textTheme = Theme.of(context).textTheme;
 
-    return DefaultFocusTraversal(
+    return FocusTraversalGroup(
       policy: ReadingOrderTraversalPolicy(),
       child: FocusScope(
         debugLabel: 'Scope',
diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart
index 11e645c..d4bdee6 100644
--- a/packages/flutter/lib/src/widgets/app.dart
+++ b/packages/flutter/lib/src/widgets/app.dart
@@ -1395,7 +1395,7 @@
       debugLabel: '<Default WidgetsApp Shortcuts>',
       child: Actions(
         actions: widget.actions ?? WidgetsApp.defaultActions,
-        child: DefaultFocusTraversal(
+        child: FocusTraversalGroup(
           policy: ReadingOrderTraversalPolicy(),
           child: _MediaQueryFromWindow(
             child: Localizations(
diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart
index 3921f36..2455bd9 100644
--- a/packages/flutter/lib/src/widgets/focus_manager.dart
+++ b/packages/flutter/lib/src/widgets/focus_manager.dart
@@ -232,12 +232,12 @@
 /// particular direction, is determined by the [FocusTraversalPolicy] in force.
 ///
 /// The ambient policy is determined by looking up the widget hierarchy for a
-/// [DefaultFocusTraversal] widget, and obtaining the focus traversal policy
+/// [FocusTraversalGroup] widget, and obtaining the focus traversal policy
 /// from it. Different focus nodes can inherit difference policies, so part of
 /// the app can go in widget order, and part can go in reading order, depending
 /// upon the use case.
 ///
-/// Predefined policies include [WidgetOrderFocusTraversalPolicy],
+/// Predefined policies include [WidgetOrderTraversalPolicy],
 /// [ReadingOrderTraversalPolicy], and [DirectionalFocusTraversalPolicyMixin],
 /// but custom policies can be built based upon these policies.
 ///
@@ -363,8 +363,8 @@
 ///    events to focused nodes.
 ///  * [FocusTraversalPolicy], a class used to determine how to move the focus
 ///    to other nodes.
-///  * [DefaultFocusTraversal], a widget used to configure the default focus
-///    traversal policy for a widget subtree.
+///  * [FocusTraversalGroup], a widget used to group together and configure the
+///    focus traversal policy for a widget subtree.
 class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
   /// Creates a focus node.
   ///
@@ -427,8 +427,8 @@
   ///
   /// See also:
   ///
-  ///  * [DefaultFocusTraversal], a widget that sets the traversal policy for
-  ///    its descendants.
+  ///  * [FocusTraversalGroup], a widget used to group together and configure the
+  ///    focus traversal policy for a widget subtree.
   ///  * [FocusTraversalPolicy], a class that can be extended to describe a
   ///    traversal policy.
   bool get canRequestFocus {
@@ -518,7 +518,8 @@
     return _descendants;
   }
 
-  /// Returns all descendants which do not have the [skipTraversal] flag set.
+  /// Returns all descendants which do not have the [skipTraversal] and do have
+  /// the [canRequestFocus] flag set.
   Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus);
 
   /// An [Iterable] over the ancestors of this node.
@@ -779,7 +780,7 @@
       _manager?.primaryFocus?._setAsFocusedChildForScope();
     }
     if (oldScope != null && child.context != null && child.enclosingScope != oldScope) {
-      DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope);
+      FocusTraversalGroup.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope);
     }
     if (child._requestFocusWhenReparented) {
       child._doRequestFocus();
@@ -918,19 +919,19 @@
   /// [FocusTraversalPolicy.next] method.
   ///
   /// Returns true if it successfully found a node and requested focus.
-  bool nextFocus() => DefaultFocusTraversal.of(context).next(this);
+  bool nextFocus() => FocusTraversalGroup.of(context).next(this);
 
   /// Request to move the focus to the previous focus node, by calling the
   /// [FocusTraversalPolicy.previous] method.
   ///
   /// Returns true if it successfully found a node and requested focus.
-  bool previousFocus() => DefaultFocusTraversal.of(context).previous(this);
+  bool previousFocus() => FocusTraversalGroup.of(context).previous(this);
 
   /// Request to move the focus to the nearest focus node in the given
   /// direction, by calling the [FocusTraversalPolicy.inDirection] method.
   ///
   /// Returns true if it successfully found a node and requested focus.
-  bool focusInDirection(TraversalDirection direction) => DefaultFocusTraversal.of(context).inDirection(this, direction);
+  bool focusInDirection(TraversalDirection direction) => FocusTraversalGroup.of(context).inDirection(this, direction);
 
   @override
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart
index cb9ce51..b2318f9 100644
--- a/packages/flutter/lib/src/widgets/focus_scope.dart
+++ b/packages/flutter/lib/src/widgets/focus_scope.dart
@@ -149,8 +149,10 @@
     this.debugLabel,
     this.canRequestFocus,
     this.skipTraversal,
+    this.includeSemantics = true,
   })  : assert(child != null),
         assert(autofocus != null),
+        assert(includeSemantics != null),
         super(key: key);
 
   /// A debug label for this widget.
@@ -231,6 +233,17 @@
   /// still be focused explicitly.
   final bool skipTraversal;
 
+  /// Include semantics information in this [Focus] widget.
+  ///
+  /// If true, this [Focus] widget will include a [Semantics] node that
+  /// indicates the [Semantics.focusable] and [Semantics.focused] properties.
+  ///
+  /// Is is not typical to set this to false, as that can affect the semantics
+  /// information available to accessibility systems.
+  ///
+  /// Must not be null, defaults to true.
+  final bool includeSemantics;
+
   /// {@template flutter.widgets.Focus.canRequestFocus}
   /// If true, this widget may request the primary focus.
   ///
@@ -459,13 +472,17 @@
   @override
   Widget build(BuildContext context) {
     _focusAttachment.reparent();
-    return _FocusMarker(
-      node: focusNode,
-      child: Semantics(
+    Widget child = widget.child;
+    if (widget.includeSemantics) {
+      child = Semantics(
         focusable: _canRequestFocus,
         focused: _hasPrimaryFocus,
         child: widget.child,
-      ),
+      );
+    }
+    return _FocusMarker(
+      node: focusNode,
+      child: child,
     );
   }
 }
diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart
index c88ed39..22a09b8 100644
--- a/packages/flutter/lib/src/widgets/focus_traversal.dart
+++ b/packages/flutter/lib/src/widgets/focus_traversal.dart
@@ -4,6 +4,7 @@
 
 import 'dart:ui';
 
+import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
 
@@ -11,14 +12,59 @@
 import 'basic.dart';
 import 'editable_text.dart';
 import 'focus_manager.dart';
+import 'focus_scope.dart';
 import 'framework.dart';
 import 'scroll_position.dart';
 import 'scrollable.dart';
 
+// BuildContext/Element doesn't have a parent accessor, but it can be simulated
+// with visitAncestorElements. _getAncestor is needed because
+// context.getElementForInheritedWidgetOfExactType will return itself if it
+// happens to be of the correct type. _getAncestor should be O(count), since we
+// always return false at a specific ancestor. By default it returns the parent,
+// which is O(1).
+BuildContext _getAncestor(BuildContext context, {int count = 1}) {
+  BuildContext target;
+  context.visitAncestorElements((Element ancestor) {
+    count--;
+    if (count == 0) {
+      target = ancestor;
+      return false;
+    }
+    return true;
+  });
+  return target;
+}
+
+void _focusAndEnsureVisible(
+  FocusNode node, {
+  ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
+}) {
+  node.requestFocus();
+  Scrollable.ensureVisible(node.context, alignment: 1.0, alignmentPolicy: alignmentPolicy);
+}
+
+// A class to temporarily hold information about FocusTraversalGroups when
+// sorting their contents.
+class _FocusTraversalGroupInfo {
+  _FocusTraversalGroupInfo(
+    _FocusTraversalGroupMarker marker, {
+    FocusTraversalPolicy defaultPolicy,
+    List<FocusNode> members,
+  })  : groupNode = marker?.focusNode,
+        policy = marker?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
+        members = members ?? <FocusNode>[];
+
+  final FocusNode groupNode;
+  final FocusTraversalPolicy policy;
+  final List<FocusNode> members;
+}
+
 /// A direction along either the horizontal or vertical axes.
 ///
-/// This is used by the [DirectionalFocusTraversalPolicyMixin] to indicate which
-/// direction to traverse in.
+/// This is used by the [DirectionalFocusTraversalPolicyMixin], and
+/// [Focus.focusInDirection] to indicate which direction to look in for the next
+/// focus.
 enum TraversalDirection {
   /// Indicates a direction above the currently focused widget.
   up,
@@ -43,7 +89,7 @@
 }
 
 /// An object used to specify a focus traversal policy used for configuring a
-/// [DefaultFocusTraversal] widget.
+/// [FocusTraversalGroup] widget.
 ///
 /// The focus traversal policy is what determines which widget is "next",
 /// "previous", or in a direction from the currently focused [FocusNode].
@@ -51,40 +97,61 @@
 /// One of the pre-defined subclasses may be used, or define a custom policy to
 /// create a unique focus order.
 ///
+/// When defining your own, your subclass should implement [sortDescendants] to
+/// provide the order in which you would like the descendants to be traversed.
+///
 /// See also:
 ///
 ///  * [FocusNode], for a description of the focus system.
-///  * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
-///    [Focus] nodes below it in the widget hierarchy.
+///  * [FocusTraversalGroup], a widget that groups together and imposes a
+///    traversal policy on the [Focus] nodes below it in the widget hierarchy.
 ///  * [FocusNode], which is affected by the traversal policy.
-///  * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
+///  * [WidgetOrderTraversalPolicy], a policy that relies on the widget
 ///    creation order to describe the order of traversal.
 ///  * [ReadingOrderTraversalPolicy], a policy that describes the order as the
 ///    natural "reading order" for the current [Directionality].
+///  * [OrderedTraversalPolicy], a policy that describes the order
+///    explicitly using [FocusTraversalOrder] widgets.
 ///  * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
 ///    focus traversal in a direction.
-abstract class FocusTraversalPolicy {
+@immutable
+abstract class FocusTraversalPolicy extends Diagnosticable {
+  /// A const constructor so subclasses can be const.
+  const FocusTraversalPolicy();
+
   /// Returns the node that should receive focus if there is no current focus
-  /// in the [FocusScopeNode] that [currentNode] belongs to.
+  /// in the nearest [FocusScopeNode] that `currentNode` belongs to.
   ///
   /// This is used by [next]/[previous]/[inDirection] to determine which node to
-  /// focus if they are called, but no node is currently focused.
+  /// focus if they are called when no node is currently focused.
   ///
-  /// It is also used by the [FocusManager] to know which node to focus
-  /// initially if no nodes are focused.
+  /// The `currentNode` argument must not be null.
   ///
-  /// If the [direction] is null, then it should find the appropriate first node
-  /// for next/previous, and if direction is non-null, should find the
-  /// appropriate first node in that direction.
-  ///
-  /// The [currentNode] argument must not be null.
-  FocusNode findFirstFocus(FocusNode currentNode);
+  /// The default implementation returns the [FocusScopeNode.focusedChild], if
+  /// set, on the nearest scope of the `currentNode`, otherwise, returns the
+  /// first node from [sortDescendants], or the given `currentNode` if there are
+  /// no descendants.
+  FocusNode findFirstFocus(FocusNode currentNode) {
+    assert(currentNode != null);
+    final FocusScopeNode scope = currentNode.nearestScope;
+    FocusNode candidate = scope.focusedChild;
+    if (candidate == null && scope.descendants.isNotEmpty) {
+      final Iterable<FocusNode> sorted = _sortAllDescendants(scope);
+      candidate = sorted.isNotEmpty ? sorted.first : null;
+    }
 
-  /// Returns the node in the given [direction] that should receive focus if
-  /// there is no current focus in the scope to which the [currentNode] belongs.
+    // If we still didn't find any candidate, use the current node as a
+    // fallback.
+    candidate ??= currentNode;
+    return candidate;
+  }
+
+  /// Returns the first node in the given `direction` that should receive focus
+  /// if there is no current focus in the scope to which the `currentNode`
+  /// belongs.
   ///
   /// This is typically used by [inDirection] to determine which node to focus
-  /// if it is called, but no node is currently focused.
+  /// if it is called when no node is currently focused.
   ///
   /// All arguments must not be null.
   FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction);
@@ -121,7 +188,7 @@
   /// Returns true if it successfully found a node and requested focus.
   ///
   /// The [currentNode] argument must not be null.
-  bool next(FocusNode currentNode);
+  bool next(FocusNode currentNode) => _moveFocus(currentNode, forward: true);
 
   /// Focuses the previous widget in the focus scope that contains the given
   /// [currentNode].
@@ -133,7 +200,7 @@
   /// Returns true if it successfully found a node and requested focus.
   ///
   /// The [currentNode] argument must not be null.
-  bool previous(FocusNode currentNode);
+  bool previous(FocusNode currentNode) => _moveFocus(currentNode, forward: false);
 
   /// Focuses the next widget in the given [direction] in the focus scope that
   /// contains the given [currentNode].
@@ -146,15 +213,165 @@
   ///
   /// All arguments must not be null.
   bool inDirection(FocusNode currentNode, TraversalDirection direction);
+
+  /// Sorts the given `descendants` into focus order.
+  ///
+  /// Subclasses should override this to implement a different sort for [next]
+  /// and [previous] to use in their ordering. If the returned iterable omits a
+  /// node that is a descendant of the given scope, then the user will be unable
+  /// to use next/previous keyboard traversal to reach that node, and if that
+  /// node is used as the originator of a call to next/previous (i.e. supplied
+  /// as the argument to [next] or [previous]), then the next or previous node
+  /// will not be able to be determined and the focus will not change.
+  ///
+  /// This is not used for directional focus ([inDirection]), only for
+  /// determining the focus order for [next] and [previous].
+  ///
+  /// When implementing an override for this function, be sure to use
+  /// [mergeSort] instead of Dart's default list sorting algorithm when sorting
+  /// items, since the default algorithm is not stable (items deemed to be equal
+  /// can appear in arbitrary order, and change positions between sorts), whereas
+  /// [mergeSort] is stable.
+  @protected
+  Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants);
+
+  _FocusTraversalGroupMarker _getMarker(BuildContext context) {
+    return context?.getElementForInheritedWidgetOfExactType<_FocusTraversalGroupMarker>()?.widget as _FocusTraversalGroupMarker;
+  }
+
+  // Sort all descendants, taking into account the FocusTraversalGroup
+  // that they are each in, and filtering out non-traversable/focusable nodes.
+  List<FocusNode> _sortAllDescendants(FocusScopeNode scope) {
+    assert(scope != null);
+    final _FocusTraversalGroupMarker scopeGroupMarker = _getMarker(scope.context);
+    final FocusTraversalPolicy defaultPolicy = scopeGroupMarker?.policy ?? ReadingOrderTraversalPolicy();
+    // Build the sorting data structure, separating descendants into groups.
+    final Map<FocusNode, _FocusTraversalGroupInfo> groups = <FocusNode, _FocusTraversalGroupInfo>{};
+    for (final FocusNode node in scope.descendants) {
+      final _FocusTraversalGroupMarker groupMarker = _getMarker(node.context);
+      final FocusNode groupNode = groupMarker?.focusNode;
+      // Group nodes need to be added to their parent's node, or to the "null"
+      // node if no parent is found. This creates the hierarchy of group nodes
+      // and makes it so the entire group is sorted along with the other members
+      // of the parent group.
+      if (node == groupNode) {
+        // To find the parent of the group node, we need to skip over the parent
+        // of the Focus node in _FocusTraversalGroupState.build, and start
+        // looking with that node's parent, since _getMarker will return the
+        // context it was called on if it matches the type.
+        final BuildContext parentContext = _getAncestor(groupNode.context, count: 2);
+        final _FocusTraversalGroupMarker parentMarker = _getMarker(parentContext);
+        final FocusNode parentNode = parentMarker?.focusNode;
+        groups[parentNode] ??= _FocusTraversalGroupInfo(parentMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+        assert(!groups[parentNode].members.contains(node));
+        groups[parentNode].members.add(groupNode);
+        continue;
+      }
+      // Skip non-focusable and non-traversable nodes in the same way that
+      // FocusScopeNode.traversalDescendants would.
+      if (node.canRequestFocus && !node.skipTraversal) {
+        groups[groupNode] ??= _FocusTraversalGroupInfo(groupMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+        assert(!groups[groupNode].members.contains(node));
+        groups[groupNode].members.add(node);
+      }
+    }
+
+    // Sort the member lists using the individual policy sorts.
+    final Set<FocusNode> groupKeys = groups.keys.toSet();
+    for (final FocusNode key in groups.keys) {
+      final List<FocusNode> sortedMembers = groups[key].policy.sortDescendants(groups[key].members).toList();
+      groups[key].members.clear();
+      groups[key].members.addAll(sortedMembers);
+    }
+
+    // Traverse the group tree, adding the children of members in the order they
+    // appear in the member lists.
+    final List<FocusNode> sortedDescendants = <FocusNode>[];
+    void visitGroups(_FocusTraversalGroupInfo info) {
+      for (final FocusNode node in info.members) {
+        if (groupKeys.contains(node)) {
+          // This is a policy group focus node. Replace it with the members of
+          // the corresponding policy group.
+          visitGroups(groups[node]);
+        } else {
+          sortedDescendants.add(node);
+        }
+      }
+    }
+
+    visitGroups(groups[scopeGroupMarker?.focusNode]);
+    assert(
+      sortedDescendants.toSet().difference(scope.traversalDescendants.toSet()).isEmpty,
+      'sorted descendants contains more nodes than it should: (${sortedDescendants.toSet().difference(scope.traversalDescendants.toSet())})'
+    );
+    assert(
+      scope.traversalDescendants.toSet().difference(sortedDescendants.toSet()).isEmpty,
+      'sorted descendants are missing some nodes: (${scope.traversalDescendants.toSet().difference(sortedDescendants.toSet())})'
+    );
+    return sortedDescendants;
+  }
+
+  // Moves the focus to the next node in the FocusScopeNode nearest to the
+  // currentNode argument, either in a forward or reverse direction, depending
+  // on the value of the forward argument.
+  //
+  // This function is called by the next and previous members to move to the
+  // next or previous node, respectively.
+  //
+  // Uses findFirstFocus to find the first node if there is no
+  // FocusScopeNode.focusedChild set. If there is a focused child for the
+  // scope, then it calls sortDescendants to get a sorted list of descendants,
+  // and then finds the node after the current first focus of the scope if
+  // forward is true, and the node before it if forward is false.
+  //
+  // Returns true if a node requested focus.
+  @protected
+  bool _moveFocus(FocusNode currentNode, {@required bool forward}) {
+    assert(forward != null);
+    if (currentNode == null) {
+      return false;
+    }
+    final FocusScopeNode nearestScope = currentNode.nearestScope;
+    invalidateScopeData(nearestScope);
+    final FocusNode focusedChild = nearestScope.focusedChild;
+    if (focusedChild == null) {
+      final FocusNode firstFocus = findFirstFocus(currentNode);
+      if (firstFocus != null) {
+        _focusAndEnsureVisible(
+          firstFocus,
+          alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
+        );
+        return true;
+      }
+    }
+    final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope);
+    if (forward && focusedChild == sortedNodes.last) {
+      _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
+      return true;
+    }
+    if (!forward && focusedChild == sortedNodes.first) {
+      _focusAndEnsureVisible(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
+      return true;
+    }
+
+    final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
+    FocusNode previousNode;
+    for (final FocusNode node in maybeFlipped) {
+      if (previousNode == focusedChild) {
+        _focusAndEnsureVisible(
+          node,
+          alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
+        );
+        return true;
+      }
+      previousNode = node;
+    }
+    return false;
+  }
 }
 
-@protected
-void _focusAndEnsureVisible(FocusNode node, {ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit}) {
-  node.requestFocus();
-  Scrollable.ensureVisible(node.context, alignment: 1.0, alignmentPolicy: alignmentPolicy);
-}
-
-/// A policy data object for use by the [DirectionalFocusTraversalPolicyMixin]
+// A policy data object for use by the DirectionalFocusTraversalPolicyMixin so
+// it can keep track of the traversal history.
 class _DirectionalPolicyDataEntry {
   const _DirectionalPolicyDataEntry({@required this.direction, @required this.node})
       : assert(direction != null),
@@ -187,17 +404,20 @@
 /// For instance, if the focus moves down, down, down, and then up, up, up, it
 /// will follow the same path through the widgets in both directions. However,
 /// if it moves down, down, down, left, right, and then up, up, up, it may not
-/// follow the same path on the way up as it did on the way down.
+/// follow the same path on the way up as it did on the way down, since changing
+/// the axis of motion resets the history.
 ///
 /// See also:
 ///
 ///  * [FocusNode], for a description of the focus system.
-///  * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
-///    [Focus] nodes below it in the widget hierarchy.
-///  * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
+///  * [FocusTraversalGroup], a widget that groups together and imposes a
+///    traversal policy on the [Focus] nodes below it in the widget hierarchy.
+///  * [WidgetOrderTraversalPolicy], a policy that relies on the widget
 ///    creation order to describe the order of traversal.
 ///  * [ReadingOrderTraversalPolicy], a policy that describes the order as the
 ///    natural "reading order" for the current [Directionality].
+///  * [OrderedTraversalPolicy], a policy that describes the order
+///    explicitly using [FocusTraversalOrder] widgets.
 mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
   final Map<FocusScopeNode, _DirectionalPolicyData> _policyData = <FocusScopeNode, _DirectionalPolicyData>{};
 
@@ -238,10 +458,10 @@
     return null;
   }
 
-  FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) {
+  FocusNode _sortAndFindInitial(FocusNode currentNode, {bool vertical, bool first}) {
     final Iterable<FocusNode> nodes = currentNode.nearestScope.traversalDescendants;
     final List<FocusNode> sorted = nodes.toList();
-    sorted.sort((FocusNode a, FocusNode b) {
+    mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) {
       if (vertical) {
         if (first) {
           return a.rect.top.compareTo(b.rect.top);
@@ -257,8 +477,9 @@
       }
     });
 
-    if (sorted.isNotEmpty)
+    if (sorted.isNotEmpty) {
       return sorted.first;
+    }
 
     return null;
   }
@@ -280,7 +501,7 @@
     final Iterable<FocusNode> nodes = nearestScope.traversalDescendants;
     assert(!nodes.contains(nearestScope));
     final List<FocusNode> sorted = nodes.toList();
-    sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
+    mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
     Iterable<FocusNode> result;
     switch (direction) {
       case TraversalDirection.left:
@@ -305,7 +526,7 @@
     Iterable<FocusNode> nodes,
   ) {
     final List<FocusNode> sorted = nodes.toList();
-    sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy));
+    mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy));
     switch (direction) {
       case TraversalDirection.up:
         return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
@@ -329,9 +550,9 @@
       if (policyData.history.last.node.parent == null) {
         // If a node has been removed from the tree, then we should stop
         // referencing it and reset the scope data so that we don't try and
-        // request focus on it. This can happen in slivers where the rendered node
-        // has been unmounted. This has the side effect that hysteresis might not
-        // be avoided when items that go off screen get unmounted.
+        // request focus on it. This can happen in slivers where the rendered
+        // node has been unmounted. This has the side effect that hysteresis
+        // might not be avoided when items that go off screen get unmounted.
         invalidateScopeData(nearestScope);
         return false;
       }
@@ -344,14 +565,14 @@
           return false;
         }
         ScrollPositionAlignmentPolicy alignmentPolicy;
-        switch(direction) {
+        switch (direction) {
           case TraversalDirection.up:
           case TraversalDirection.left:
             alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
             break;
           case TraversalDirection.right:
           case TraversalDirection.down:
-          alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
+            alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
             break;
         }
         _focusAndEnsureVisible(
@@ -486,12 +707,14 @@
         final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity);
         final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
         if (inBand.isNotEmpty) {
-          // The inBand list is already sorted by horizontal distance, so pick the closest one.
+          // The inBand list is already sorted by horizontal distance, so pick
+          // the closest one.
           found = inBand.first;
           break;
         }
-        // Only out-of-band targets remain, so pick the one that is closest the to the center line horizontally.
-        sorted.sort((FocusNode a, FocusNode b) {
+        // Only out-of-band targets remain, so pick the one that is closest the
+        // to the center line horizontally.
+        mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) {
           return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs());
         });
         found = sorted.first;
@@ -516,12 +739,14 @@
         final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom);
         final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
         if (inBand.isNotEmpty) {
-          // The inBand list is already sorted by vertical distance, so pick the closest one.
+          // The inBand list is already sorted by vertical distance, so pick the
+          // closest one.
           found = inBand.first;
           break;
         }
-        // Only out-of-band targets remain, so pick the one that is closest the to the center line vertically.
-        sorted.sort((FocusNode a, FocusNode b) {
+        // Only out-of-band targets remain, so pick the one that is closest the
+        // to the center line vertically.
+        mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) {
           return (a.rect.center.dy - focusedChild.rect.center.dy).abs().compareTo((b.rect.center.dy - focusedChild.rect.center.dy).abs());
         });
         found = sorted.first;
@@ -539,10 +764,10 @@
           break;
         case TraversalDirection.down:
         case TraversalDirection.right:
-        _focusAndEnsureVisible(
-          found,
-          alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
-        );
+          _focusAndEnsureVisible(
+            found,
+            alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
+          );
           break;
       }
       return true;
@@ -560,115 +785,159 @@
 /// See also:
 ///
 ///  * [FocusNode], for a description of the focus system.
-///  * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
-///    [Focus] nodes below it in the widget hierarchy.
+///  * [FocusTraversalGroup], a widget that groups together and imposes a
+///    traversal policy on the [Focus] nodes below it in the widget hierarchy.
 ///  * [ReadingOrderTraversalPolicy], a policy that describes the order as the
 ///    natural "reading order" for the current [Directionality].
 ///  * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
 ///    focus traversal in a direction.
-class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
-  /// Creates a const [WidgetOrderFocusTraversalPolicy].
-  WidgetOrderFocusTraversalPolicy();
-
+///  * [OrderedTraversalPolicy], a policy that describes the order
+///    explicitly using [FocusTraversalOrder] widgets.
+class WidgetOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
   @override
-  FocusNode findFirstFocus(FocusNode currentNode) {
-    assert(currentNode != null);
-    final FocusScopeNode scope = currentNode.nearestScope;
-    // Start with the candidate focus as the focused child of this scope, if
-    // there is one. Otherwise start with this node itself. Keep going down
-    // through scopes until an ultimately focusable item is found, a scope
-    // doesn't have a focusedChild, or a non-scope is encountered.
-    FocusNode candidate = scope.focusedChild;
-    if (candidate == null) {
-      if (scope.traversalChildren.isNotEmpty) {
-        candidate = scope.traversalChildren.first;
-      } else {
-        candidate = currentNode;
-      }
-    }
-    while (candidate is FocusScopeNode && candidate.focusedChild != null) {
-      final FocusScopeNode candidateScope = candidate as FocusScopeNode;
-      candidate = candidateScope.focusedChild;
-    }
-    return candidate;
-  }
-
-  // Moves the focus to the next or previous node, depending on whether forward
-  // is true or not.
-  bool _move(FocusNode currentNode, {@required bool forward}) {
-    if (currentNode == null) {
-      return false;
-    }
-    final FocusScopeNode nearestScope = currentNode.nearestScope;
-    invalidateScopeData(nearestScope);
-    final FocusNode focusedChild = nearestScope.focusedChild;
-    if (focusedChild == null) {
-      final FocusNode firstFocus = findFirstFocus(currentNode);
-      if (firstFocus != null) {
-        _focusAndEnsureVisible(
-            firstFocus,
-            alignmentPolicy: forward
-                ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd
-                : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
-        );
-        return true;
-      }
-    }
-    FocusNode previousNode;
-    FocusNode firstNode;
-    FocusNode lastNode;
-    bool visit(FocusNode node) {
-      for (final FocusNode visited in node.traversalChildren) {
-        firstNode ??= visited;
-        if (!visit(visited)) {
-          return false;
-        }
-        if (forward) {
-          if (previousNode == focusedChild) {
-            _focusAndEnsureVisible(visited, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
-            return false; // short circuit the traversal.
-          }
-        } else {
-          if (previousNode != null && visited == focusedChild) {
-            _focusAndEnsureVisible(previousNode, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
-            return false; // short circuit the traversal.
-          }
-        }
-        previousNode = visited;
-        lastNode = visited;
-      }
-      return true; // continue traversal
-    }
-
-    if (visit(nearestScope)) {
-      if (forward) {
-        if (firstNode != null) {
-          _focusAndEnsureVisible(firstNode, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
-          return true;
-        }
-      } else {
-        if (lastNode != null) {
-          _focusAndEnsureVisible(lastNode, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
-          return true;
-        }
-      }
-      return false;
-    }
-    return true;
-  }
-
-  @override
-  bool next(FocusNode currentNode) => _move(currentNode, forward: true);
-
-  @override
-  bool previous(FocusNode currentNode) => _move(currentNode, forward: false);
+  Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants) => descendants;
 }
 
-class _SortData {
-  _SortData(this.node) : rect = node.rect;
+// This class exists mainly for efficiency reasons: the rect is copied out of
+// the node, because it will be accessed many times in the reading order
+// algorithm, and the FocusNode.rect accessor does coordinate transformation. If
+// not for this optimization, it could just be removed, and the node used
+// directly.
+//
+// It's also a convenient place to put some utility functions having to do with
+// the sort data.
+class _ReadingOrderSortData extends Diagnosticable {
+  _ReadingOrderSortData(this.node)
+      : assert(node != null),
+        rect = node.rect,
+        directionality = _findDirectionality(node.context);
 
+  final TextDirection directionality;
   final Rect rect;
   final FocusNode node;
+
+  // Find the directionality in force for a build context without creating a
+  // dependency.
+  static TextDirection _findDirectionality(BuildContext context) {
+    return (context.getElementForInheritedWidgetOfExactType<Directionality>()?.widget as Directionality)?.textDirection;
+  }
+
+  /// Finds the common Directional ancestor of an entire list of groups.
+  static TextDirection commonDirectionalityOf(List<_ReadingOrderSortData> list) {
+    final Iterable<Set<Directionality>> allAncestors = list.map<Set<Directionality>>((_ReadingOrderSortData member) => member.directionalAncestors.toSet());
+    Set<Directionality> common;
+    for (final Set<Directionality> ancestorSet in allAncestors) {
+      common ??= ancestorSet;
+      common = common.intersection(ancestorSet);
+    }
+    if (common.isEmpty) {
+      // If there is no common ancestor, then arbitrarily pick the
+      // directionality of the first group, which is the equivalent of the "first
+      // strongly typed" item in a bidi algorithm.
+      return list.first.directionality;
+    }
+    // Find the closest common ancestor. The memberAncestors list contains the
+    // ancestors for all members, but the first member's ancestry was
+    // added in order from nearest to furthest, so we can still use that
+    // to determine the closest one.
+    return list.first.directionalAncestors.firstWhere(common.contains).textDirection;
+  }
+
+  static void sortWithDirectionality(List<_ReadingOrderSortData> list, TextDirection directionality) {
+    mergeSort<_ReadingOrderSortData>(list, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) {
+      switch (directionality) {
+        case TextDirection.ltr:
+          return a.rect.left.compareTo(b.rect.left);
+        case TextDirection.rtl:
+          return b.rect.right.compareTo(a.rect.right);
+      }
+      assert(false, 'Unhandled directionality $directionality');
+      return 0;
+    });
+  }
+
+  /// Returns the list of Directionality ancestors, in order from nearest to
+  /// furthest.
+  Iterable<Directionality> get directionalAncestors {
+    List<Directionality> getDirectionalityAncestors(BuildContext context) {
+      final List<Directionality> result = <Directionality>[];
+      InheritedElement directionalityElement = context.getElementForInheritedWidgetOfExactType<Directionality>();
+      while (directionalityElement != null) {
+        result.add(directionalityElement.widget as Directionality);
+        directionalityElement = _getAncestor(directionalityElement)?.getElementForInheritedWidgetOfExactType<Directionality>();
+      }
+      return result;
+    }
+
+    _directionalAncestors ??= getDirectionalityAncestors(node.context);
+    return _directionalAncestors;
+  }
+
+  List<Directionality> _directionalAncestors;
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<TextDirection>('directionality', directionality));
+    properties.add(StringProperty('name', node.debugLabel, defaultValue: null));
+    properties.add(DiagnosticsProperty<Rect>('rect', rect));
+  }
+}
+
+// A class for containing group data while sorting in reading order while taking
+// into account the ambient directionality.
+class _ReadingOrderDirectionalGroupData extends Diagnosticable {
+  _ReadingOrderDirectionalGroupData(this.members);
+
+  final List<_ReadingOrderSortData> members;
+
+  TextDirection get directionality => members.first.directionality;
+
+  Rect _rect;
+  Rect get rect {
+    if (_rect == null) {
+      for (final Rect rect in members.map<Rect>((_ReadingOrderSortData data) => data.rect)) {
+        _rect ??= rect;
+        _rect = _rect.expandToInclude(rect);
+      }
+    }
+    return _rect;
+  }
+
+  List<Directionality> get memberAncestors {
+    if (_memberAncestors == null) {
+      _memberAncestors = <Directionality>[];
+      for (final _ReadingOrderSortData member in members) {
+        _memberAncestors.addAll(member.directionalAncestors);
+      }
+    }
+    return _memberAncestors;
+  }
+
+  List<Directionality> _memberAncestors;
+
+  static void sortWithDirectionality(List<_ReadingOrderDirectionalGroupData> list, TextDirection directionality) {
+    mergeSort<_ReadingOrderDirectionalGroupData>(list, compare: (_ReadingOrderDirectionalGroupData a, _ReadingOrderDirectionalGroupData b) {
+      switch (directionality) {
+        case TextDirection.ltr:
+          return a.rect.left.compareTo(b.rect.left);
+        case TextDirection.rtl:
+          return b.rect.right.compareTo(a.rect.right);
+      }
+      assert(false, 'Unhandled directionality $directionality');
+      return 0;
+    });
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<TextDirection>('directionality', directionality));
+    properties.add(DiagnosticsProperty<Rect>('rect', rect));
+    properties.add(IterableProperty<String>('members', members.map<String>((_ReadingOrderSortData member) {
+      return '"${member.node.debugLabel}"(${member.rect})';
+    })));
+  }
 }
 
 /// Traverses the focus order in "reading order".
@@ -682,160 +951,623 @@
 /// 3. Pick the closest to the beginning of the reading order from among the
 ///    nodes discovered above.
 ///
-/// It uses the ambient directionality in the context for the enclosing scope to
-/// determine which direction is "reading order".
+/// It uses the ambient [Directionality] in the context for the enclosing scope
+/// to determine which direction is "reading order".
 ///
 /// See also:
 ///
 ///  * [FocusNode], for a description of the focus system.
-///  * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
-///    [Focus] nodes below it in the widget hierarchy.
-///  * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
+///  * [FocusTraversalGroup], a widget that groups together and imposes a
+///    traversal policy on the [Focus] nodes below it in the widget hierarchy.
+///  * [WidgetOrderTraversalPolicy], a policy that relies on the widget
 ///    creation order to describe the order of traversal.
 ///  * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
 ///    focus traversal in a direction.
+///  * [OrderedTraversalPolicy], a policy that describes the order
+///    explicitly using [FocusTraversalOrder] widgets.
 class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
-  @override
-  FocusNode findFirstFocus(FocusNode currentNode) {
-    assert(currentNode != null);
-    final FocusScopeNode scope = currentNode.nearestScope;
-    FocusNode candidate = scope.focusedChild;
-    if (candidate == null && scope.traversalChildren.isNotEmpty) {
-      candidate = _sortByGeometry(scope).first;
+  // Collects the given candidates into groups by directionality. The candidates
+  // have already been sorted as if they all had the directionality of the
+  // nearest Directionality ancestor.
+  List<_ReadingOrderDirectionalGroupData> _collectDirectionalityGroups(Iterable<_ReadingOrderSortData> candidates) {
+    TextDirection currentDirection = candidates.first.directionality;
+    List<_ReadingOrderSortData> currentGroup = <_ReadingOrderSortData>[];
+    final List<_ReadingOrderDirectionalGroupData> result = <_ReadingOrderDirectionalGroupData>[];
+    // Split candidates into runs of the same directionality.
+    for (final _ReadingOrderSortData candidate in candidates) {
+      if (candidate.directionality == currentDirection) {
+        currentGroup.add(candidate);
+        continue;
+      }
+      currentDirection = candidate.directionality;
+      result.add(_ReadingOrderDirectionalGroupData(currentGroup));
+      currentGroup = <_ReadingOrderSortData>[candidate];
+    }
+    if (currentGroup.isNotEmpty) {
+      result.add(_ReadingOrderDirectionalGroupData(currentGroup));
+    }
+    // Sort each group separately. Each group has the same directionality.
+    for (final _ReadingOrderDirectionalGroupData bandGroup in result) {
+      if (bandGroup.members.length == 1) {
+        continue; // No need to sort one node.
+      }
+      _ReadingOrderSortData.sortWithDirectionality(bandGroup.members, bandGroup.directionality);
+    }
+    return result;
+  }
+
+  _ReadingOrderSortData _pickNext(List<_ReadingOrderSortData> candidates) {
+    // Find the topmost node by sorting on the top of the rectangles.
+    mergeSort<_ReadingOrderSortData>(candidates, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) => a.rect.top.compareTo(b.rect.top));
+    final _ReadingOrderSortData topmost = candidates.first;
+
+    // Find the candidates that are in the same horizontal band as the current one.
+    List<_ReadingOrderSortData> inBand(_ReadingOrderSortData current, Iterable<_ReadingOrderSortData> candidates) {
+      final Rect band = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom);
+      return candidates.where((_ReadingOrderSortData item) {
+        return !item.rect.intersect(band).isEmpty;
+      }).toList();
     }
 
-    // If we still didn't find any candidate, use the current node as a
-    // fallback.
-    candidate ??= currentNode;
-    candidate ??= FocusManager.instance.rootScope;
-    return candidate;
+    final List<_ReadingOrderSortData> inBandOfTop = inBand(topmost, candidates);
+    // It has to have at least topmost in it if the topmost is not degenerate.
+    assert(topmost.rect.isEmpty || inBandOfTop.isNotEmpty);
+
+    // The topmost rect in is in a band by itself, so just return that one.
+    if (inBandOfTop.length <= 1) {
+      return topmost;
+    }
+
+    // Now that we know there are others in the same band as the topmost, then pick
+    // the one at the beginning, depending on the text direction in force.
+
+    // Find out the directionality of the nearest common Directionality
+    // ancestor for all nodes. This provides a base directionality to use for
+    // the ordering of the groups.
+    final TextDirection nearestCommonDirectionality = _ReadingOrderSortData.commonDirectionalityOf(inBandOfTop);
+
+    // Do an initial common-directionality-based sort to get consistent geometric
+    // ordering for grouping into directionality groups. It has to use the
+    // common directionality to be able to group into sane groups for the
+    // given directionality, since rectangles can overlap and give different
+    // results for different directionalities.
+    _ReadingOrderSortData.sortWithDirectionality(inBandOfTop, nearestCommonDirectionality);
+
+    // Collect the top band into internally sorted groups with shared directionality.
+    final List<_ReadingOrderDirectionalGroupData> bandGroups = _collectDirectionalityGroups(inBandOfTop);
+    if (bandGroups.length == 1) {
+      // There's only one directionality group, so just send back the first
+      // one in that group, since it's already sorted.
+      return bandGroups.first.members.first;
+    }
+
+    // Sort the groups based on the common directionality and bounding boxes.
+    _ReadingOrderDirectionalGroupData.sortWithDirectionality(bandGroups, nearestCommonDirectionality);
+    return bandGroups.first.members.first;
   }
 
   // Sorts the list of nodes based on their geometry into the desired reading
   // order based on the directionality of the context for each node.
-  Iterable<FocusNode> _sortByGeometry(FocusScopeNode scope) {
-    final Iterable<FocusNode> nodes = scope.traversalDescendants;
-    if (nodes.length <= 1) {
-      return nodes;
+  @override
+  Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants) {
+    assert(descendants != null);
+    if (descendants.length <= 1) {
+      return descendants;
     }
 
-    Iterable<_SortData> inBand(_SortData current, Iterable<_SortData> candidates) {
-      final Rect wide = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom);
-      return candidates.where((_SortData item) {
-        return !item.rect.intersect(wide).isEmpty;
-      });
-    }
-
-    final TextDirection textDirection = scope.context == null ? TextDirection.ltr : Directionality.of(scope.context);
-    _SortData pickFirst(List<_SortData> candidates) {
-      int compareBeginningSide(_SortData a, _SortData b) {
-        return textDirection == TextDirection.ltr ? a.rect.left.compareTo(b.rect.left) : -a.rect.right.compareTo(b.rect.right);
-      }
-
-      int compareTopSide(_SortData a, _SortData b) {
-        return a.rect.top.compareTo(b.rect.top);
-      }
-
-      // Get the topmost
-      candidates.sort(compareTopSide);
-      final _SortData topmost = candidates.first;
-      // If there are any others in the band of the topmost, then pick the
-      // leftmost one.
-      final List<_SortData> inBandOfTop = inBand(topmost, candidates).toList();
-      inBandOfTop.sort(compareBeginningSide);
-      if (inBandOfTop.isNotEmpty) {
-        return inBandOfTop.first;
-      }
-      return topmost;
-    }
-
-    final List<_SortData> data = <_SortData>[
-      for (final FocusNode node in nodes) _SortData(node),
+    final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[
+      for (final FocusNode node in descendants) _ReadingOrderSortData(node),
     ];
 
-    // Pick the initial widget as the one that is leftmost in the band of the
-    // topmost, or the topmost, if there are no others in its band.
-    final List<_SortData> sortedList = <_SortData>[];
-    final List<_SortData> unplaced = data.toList();
-    _SortData current = pickFirst(unplaced);
-    sortedList.add(current);
+    final List<FocusNode> sortedList = <FocusNode>[];
+    final List<_ReadingOrderSortData> unplaced = data;
+
+    // Pick the initial widget as the one that is at the beginning of the band
+    // of the topmost, or the topmost, if there are no others in its band.
+    _ReadingOrderSortData current = _pickNext(unplaced);
+    sortedList.add(current.node);
     unplaced.remove(current);
 
+    // Go through each node, picking the next one after eliminating the previous
+    // one, since removing the previously picked node will expose a new band in
+    // which to choose candidates.
     while (unplaced.isNotEmpty) {
-      final _SortData next = pickFirst(unplaced);
+      final _ReadingOrderSortData next = _pickNext(unplaced);
       current = next;
-      sortedList.add(current);
+      sortedList.add(current.node);
       unplaced.remove(current);
     }
-    return sortedList.map((_SortData item) => item.node);
+    return sortedList;
   }
-
-  // Moves the focus forward or backward in reading order, depending on the
-  // value of the forward argument.
-  bool _move(FocusNode currentNode, {@required bool forward}) {
-    final FocusScopeNode nearestScope = currentNode.nearestScope;
-    invalidateScopeData(nearestScope);
-    final FocusNode focusedChild = nearestScope.focusedChild;
-    if (focusedChild == null) {
-      final FocusNode firstFocus = findFirstFocus(currentNode);
-      if (firstFocus != null) {
-        _focusAndEnsureVisible(
-          firstFocus,
-          alignmentPolicy: forward
-              ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd
-              : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
-        );
-        return true;
-      }
-    }
-    final List<FocusNode> sortedNodes = _sortByGeometry(nearestScope).toList();
-    if (forward && focusedChild == sortedNodes.last) {
-      _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
-      return true;
-    }
-    if (!forward && focusedChild == sortedNodes.first) {
-      _focusAndEnsureVisible(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
-      return true;
-    }
-
-    final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
-    FocusNode previousNode;
-    for (final FocusNode node in maybeFlipped) {
-      if (previousNode == focusedChild) {
-        _focusAndEnsureVisible(
-          node,
-          alignmentPolicy: forward
-            ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd
-            : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
-        );
-        return true;
-      }
-      previousNode = node;
-    }
-    return false;
-  }
-
-  @override
-  bool next(FocusNode currentNode) => _move(currentNode, forward: true);
-
-  @override
-  bool previous(FocusNode currentNode) => _move(currentNode, forward: false);
 }
 
-/// A widget that describes the inherited focus policy for focus traversal.
+/// Base class for all sort orders for [OrderedTraversalPolicy] traversal.
 ///
-/// By default, traverses in widget order using
-/// [ReadingOrderFocusTraversalPolicy].
+/// {@template flutter.widgets.focusorder.comparable}
+/// Only orders of the same type are comparable. If a set of widgets in the same
+/// [FocusTraversalGroup] contains orders that are not comparable with each other, it
+/// will assert, since the ordering between such keys is undefined. To avoid
+/// collisions, use a [FocusTraversalGroup] to group similarly ordered widgets
+/// together.
+///
+/// When overriding, [doCompare] must be overridden instead of [compareTo],
+/// which calls [doCompare] to do the actual comparison.
+/// {@endtemplate}
+///
+/// See also:
+///
+///  * [FocusTraversalGroup], a widget that groups together and imposes a
+///    traversal policy on the [Focus] nodes below it in the widget hierarchy.
+///  * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
+///    for the [OrderedFocusTraversalPolicy] to use.
+///  * [NumericFocusOrder], for a focus order that describes its order with a
+///    `double`.
+///  * [LexicalFocusOrder], a focus order that assigns a string-based lexical
+///    traversal order to a [FocusTraversalOrder] widget.
+@immutable
+abstract class FocusOrder extends Diagnosticable implements Comparable<FocusOrder> {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const FocusOrder();
+
+  /// Compares this object to another [Comparable].
+  ///
+  /// When overriding [FocusOrder], implement [doCompare] instead of this
+  /// function to do the actual comparison.
+  ///
+  /// Returns a value like a [Comparator] when comparing `this` to [other].
+  /// That is, it returns a negative integer if `this` is ordered before [other],
+  /// a positive integer if `this` is ordered after [other],
+  /// and zero if `this` and [other] are ordered together.
+  ///
+  /// The [other] argument must be a value that is comparable to this object.
+  @override
+  @nonVirtual
+  int compareTo(FocusOrder other) {
+    assert(
+        runtimeType == other.runtimeType,
+        "The sorting algorithm must not compare incomparable keys, since they don't "
+        'know how to order themselves relative to each other. Comparing $this with $other');
+    return doCompare(other);
+  }
+
+  /// The subclass implementation called by [compareTo] to compare orders.
+  ///
+  /// The argument is guaranteed to be of the same [runtimeType] as this object.
+  ///
+  /// The method should return a negative number if this object comes earlier in
+  /// the sort order than the `other` argument; and a positive number if it
+  /// comes later in the sort order than `other`. Returning zero causes the
+  /// system to fall back to the secondary sort order defined by
+  /// [OrderedTraversalPolicy.secondary]
+  @protected
+  int doCompare(covariant FocusOrder other);
+}
+
+/// Can be given to a [FocusTraversalOrder] widget to assign a numerical order
+/// to a widget subtree that is using a [OrderedTraversalPolicy] to define the
+/// order in which widgets should be traversed with the keyboard.
+///
+/// {@macro flutter.widgets.focusorder.comparable}
+///
+/// See also:
+///
+///  * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
+///    for the [OrderedFocusTraversalPolicy] to use.
+class NumericFocusOrder extends FocusOrder {
+  /// Const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const NumericFocusOrder(this.order) : assert(order != null);
+
+  /// The numerical order to assign to the widget subtree using
+  /// [FocusTraversalOrder].
+  ///
+  /// Determines the placement of this widget in a sequence of widgets that defines
+  /// the order in which this node is traversed by the focus policy.
+  ///
+  /// Lower values will be traversed first.
+  final double order;
+
+  @override
+  int doCompare(NumericFocusOrder other) => order.compareTo(other.order);
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DoubleProperty('order', order));
+  }
+}
+
+/// Can be given to a [FocusTraversalOrder] widget to use a String to assign a
+/// lexical order to a widget subtree that is using a
+/// [OrderedTraversalPolicy] to define the order in which widgets should be
+/// traversed with the keyboard.
+///
+/// This sorts strings using Dart's default string comparison, which is not
+/// locale specific.
+///
+/// {@macro flutter.widgets.focusorder.comparable}
+///
+/// See also:
+///
+///  * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
+///    for the [OrderedFocusTraversalPolicy] to use.
+class LexicalFocusOrder extends FocusOrder {
+  /// Const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const LexicalFocusOrder(this.order) : assert(order != null);
+
+  /// The String that defines the lexical order to assign to the widget subtree
+  /// using [FocusTraversalOrder].
+  ///
+  /// Determines the placement of this widget in a sequence of widgets that defines
+  /// the order in which this node is traversed by the focus policy.
+  ///
+  /// Lower lexical values will be traversed first (e.g. 'a' comes before 'z').
+  final String order;
+
+  @override
+  int doCompare(LexicalFocusOrder other) => order.compareTo(other.order);
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(StringProperty('order', order));
+  }
+}
+
+// Used to help sort the focus nodes in an OrderedFocusTraversalPolicy.
+class _OrderedFocusInfo {
+  const _OrderedFocusInfo({@required this.node, @required this.order})
+      : assert(node != null),
+        assert(order != null);
+
+  final FocusNode node;
+  final FocusOrder order;
+}
+
+/// A [FocusTraversalPolicy] that orders nodes by an explicit order that resides
+/// in the nearest [FocusTraversalOrder] widget ancestor.
+///
+/// {@macro flutter.widgets.focusorder.comparable}
+///
+/// {@tool dartpad --template=stateless_widget_scaffold_center}
+/// This sample shows how to assign a traversal order to a widget. In the
+/// example, the focus order goes from bottom right (the "One" button) to top
+/// left (the "Six" button).
+///
+/// ```dart preamble
+/// class DemoButton extends StatelessWidget {
+///   const DemoButton({this.name, this.autofocus = false, this.order});
+///
+///   final String name;
+///   final bool autofocus;
+///   final double order;
+///
+///   void _handleOnPressed() {
+///     print('Button $name pressed.');
+///     debugDumpFocusTree();
+///   }
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     return FocusTraversalOrder(
+///       order: NumericFocusOrder(order),
+///       child: FlatButton(
+///         autofocus: autofocus,
+///         focusColor: Colors.red,
+///         onPressed: () => _handleOnPressed(),
+///         child: Text(name),
+///       ),
+///     );
+///   }
+/// }
+/// ```
+///
+/// ```dart
+/// Widget build(BuildContext context) {
+///   return FocusTraversalGroup(
+///     policy: OrderedTraversalPolicy(),
+///     child: Column(
+///       mainAxisAlignment: MainAxisAlignment.center,
+///       children: <Widget>[
+///         Row(
+///           mainAxisAlignment: MainAxisAlignment.center,
+///           children: const <Widget>[
+///             DemoButton(name: 'Six', order: 6),
+///           ],
+///         ),
+///         Row(
+///           mainAxisAlignment: MainAxisAlignment.center,
+///           children: const <Widget>[
+///             DemoButton(name: 'Five', order: 5),
+///             DemoButton(name: 'Four', order: 4),
+///           ],
+///         ),
+///         Row(
+///           mainAxisAlignment: MainAxisAlignment.center,
+///           children: const <Widget>[
+///             DemoButton(name: 'Three', order: 3),
+///             DemoButton(name: 'Two', order: 2),
+///             DemoButton(name: 'One', order: 1, autofocus: true),
+///           ],
+///         ),
+///       ],
+///     ),
+///   );
+/// }
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
+///    creation order to describe the order of traversal.
+///  * [ReadingOrderTraversalPolicy], a policy that describes the order as the
+///    natural "reading order" for the current [Directionality].
+///  * [NumericFocusOrder], a focus order that assigns a numeric traversal order
+///    to a [FocusTraversalOrder] widget.
+///  * [LexicalFocusOrder], a focus order that assigns a string-based lexical
+///    traversal order to a [FocusTraversalOrder] widget.
+///  * [FocusOrder], an abstract base class for all types of focus traversal
+///    orderings.
+class OrderedTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
+  /// Constructs a traversal policy that orders widgets for keyboard traversal
+  /// based on an explicit order.
+  ///
+  /// If [secondary] is null, it will default to [ReadingOrderTraversalPolicy].
+  OrderedTraversalPolicy({this.secondary});
+
+  /// This is the policy that is used when a node doesn't have an order
+  /// assigned, or when multiple nodes have orders which are identical.
+  ///
+  /// If not set, this defaults to [ReadingOrderTraversalPolicy].
+  ///
+  /// This policy determines the secondary sorting order of nodes which evaluate
+  /// as having an identical order (including those with no order specified).
+  ///
+  /// Nodes with no order specified will be sorted after nodes with an explicit
+  /// order.
+  final FocusTraversalPolicy secondary;
+
+  @override
+  Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants) {
+    final FocusTraversalPolicy secondaryPolicy = secondary ?? ReadingOrderTraversalPolicy();
+    final Iterable<FocusNode> sortedDescendants = secondaryPolicy.sortDescendants(descendants);
+    final List<FocusNode> unordered = <FocusNode>[];
+    final List<_OrderedFocusInfo> ordered = <_OrderedFocusInfo>[];
+    for (final FocusNode node in sortedDescendants) {
+      final FocusOrder order = FocusTraversalOrder.of(node.context, nullOk: true);
+      if (order != null) {
+        ordered.add(_OrderedFocusInfo(node: node, order: order));
+      } else {
+        unordered.add(node);
+      }
+    }
+    mergeSort<_OrderedFocusInfo>(ordered, compare: (_OrderedFocusInfo a, _OrderedFocusInfo b) {
+      assert(
+        a.order.runtimeType == b.order.runtimeType,
+        'When sorting nodes for determining focus order, the order (${a.order}) of '
+        "node ${a.node}, isn't the same type as the order (${b.order}) of ${b.node}. "
+        "Incompatible order types can't be compared.  Use a FocusTraversalGroup to group "
+        'similar orders together.',
+      );
+      return a.order.compareTo(b.order);
+    });
+    return ordered.map<FocusNode>((_OrderedFocusInfo info) => info.node).followedBy(unordered);
+  }
+}
+
+/// An inherited widget that describes the order in which its child subtree
+/// should be traversed.
+///
+/// {@macro flutter.widgets.focusorder.comparable}
+///
+/// The order for a widget is determined by the [FocusOrder] returned by
+/// [FocusTraversalOrder.of] for a particular context.
+class FocusTraversalOrder extends InheritedWidget {
+  /// A const constructor so that subclasses can be const.
+  const FocusTraversalOrder({Key key, this.order, Widget child}) : super(key: key, child: child);
+
+  /// The order for the widget descendants of this [FocusTraversalOrder].
+  final FocusOrder order;
+
+  /// Finds the [FocusOrder] in the nearest ancestor [FocusTraversalOrder] widget.
+  ///
+  /// It does not create a rebuild dependency because changing the traversal
+  /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
+  /// result of an order change.
+  static FocusOrder of(BuildContext context, {bool nullOk = false}) {
+    assert(context != null);
+    assert(nullOk != null);
+    final FocusTraversalOrder marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder;
+    final FocusOrder order = marker?.order;
+    if (order == null && !nullOk) {
+      throw FlutterError('FocusTraversalOrder.of() was called with a context that '
+          'does not contain a TraversalOrder widget. No TraversalOrder widget '
+          'ancestor could be found starting from the context that was passed to '
+          'FocusTraversalOrder.of().\n'
+          'The context used was:\n'
+          '  $context');
+    }
+    return order;
+  }
+
+  // Since the order of traversal doesn't affect display of anything, we don't
+  // need to force a rebuild of anything that depends upon it.
+  @override
+  bool updateShouldNotify(InheritedWidget oldWidget) => false;
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<FocusOrder>('order', order));
+  }
+}
+
+/// A widget that describes the inherited focus policy for focus traversal for
+/// its descendants, grouping them into a separate traversal group.
+///
+/// A traversal group is treated as one entity when sorted by the traversal
+/// algorithm, so it can be used to segregate different parts of the widget tree
+/// that need to be sorted using different algorithms and/or sort orders when
+/// using an [OrderedTraversalPolicy].
+///
+/// Within the group, it will use the given [policy] to order the elements. The
+/// group itself will be ordered using the parent group's policy.
+///
+/// By default, traverses in reading order using [ReadingOrderTraversalPolicy].
 ///
 /// See also:
 ///
 ///  * [FocusNode], for a description of the focus system.
-///  * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
+///  * [WidgetOrderTraversalPolicy], a policy that relies on the widget
 ///    creation order to describe the order of traversal.
 ///  * [ReadingOrderTraversalPolicy], a policy that describes the order as the
 ///    natural "reading order" for the current [Directionality].
 ///  * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
 ///    focus traversal in a direction.
+class FocusTraversalGroup extends StatefulWidget {
+  /// Creates a [FocusTraversalGroup] object.
+  ///
+  /// The [child] argument must not be null.
+  FocusTraversalGroup({
+    Key key,
+    FocusTraversalPolicy policy,
+    @required this.child,
+  })  : policy = policy ?? ReadingOrderTraversalPolicy(),
+        super(key: key);
+
+  /// The child widget of this [FocusTraversalGroup].
+  ///
+  /// {@macro flutter.widgets.child}
+  final Widget child;
+
+  /// The policy used to move the focus from one focus node to another when
+  /// traversing them using a keyboard.
+  ///
+  /// If not specified, traverses in reading order using
+  /// [ReadingOrderTraversalPolicy].
+  ///
+  /// See also:
+  ///
+  ///  * [FocusTraversalPolicy] for the API used to impose traversal order
+  ///    policy.
+  ///  * [WidgetOrderTraversalPolicy] for a traversal policy that traverses
+  ///    nodes in the order they are added to the widget tree.
+  ///  * [ReadingOrderTraversalPolicy] for a traversal policy that traverses
+  ///    nodes in the reading order defined in the widget tree, and then top to
+  ///    bottom.
+  final FocusTraversalPolicy policy;
+
+  /// Returns the focus policy set by the [FocusTraversalGroup] that most
+  /// tightly encloses the given [BuildContext].
+  ///
+  /// It does not create a rebuild dependency because changing the traversal
+  /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
+  /// result of an order change.
+  ///
+  /// Will assert if no [FocusTraversalGroup] ancestor is found, and `nullOk` is false.
+  ///
+  /// If `nullOk` is true, then it will return null if it doesn't find a
+  /// [FocusTraversalGroup] ancestor.
+  static FocusTraversalPolicy of(BuildContext context, {bool nullOk = false}) {
+    assert(context != null);
+    final _FocusTraversalGroupMarker inherited = context?.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupMarker>();
+    assert(() {
+      if (nullOk) {
+        return true;
+      }
+      if (inherited == null) {
+        throw FlutterError(
+          'Unable to find a FocusTraversalGroup widget in the context.\n'
+          'FocusTraversalGroup.of() was called with a context that does not contain a '
+          'FocusTraversalGroup.\n'
+          'No FocusTraversalGroup ancestor could be found starting from the context that was '
+          'passed to FocusTraversalGroup.of(). This can happen because there is not a '
+          'WidgetsApp or MaterialApp widget (those widgets introduce a FocusTraversalGroup), '
+          'or it can happen if the context comes from a widget above those widgets.\n'
+          'The context used was:\n'
+          '  $context',
+        );
+      }
+      return true;
+    }());
+    return inherited?.policy;
+  }
+
+  @override
+  _FocusTraversalGroupState createState() => _FocusTraversalGroupState();
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<FocusTraversalPolicy>('policy', policy));
+  }
+}
+
+class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
+  // The internal focus node used to collect the children of this node into a
+  // group, and to provide a context for the traversal algorithm to sort the
+  // group with.
+  FocusNode focusNode;
+
+  @override
+  void initState() {
+    super.initState();
+    focusNode = FocusNode(
+      canRequestFocus: false,
+      skipTraversal: true,
+      debugLabel: 'FocusTraversalGroup',
+    );
+  }
+
+  @override
+  void dispose() {
+    focusNode?.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return _FocusTraversalGroupMarker(
+      policy: widget.policy,
+      focusNode: focusNode,
+      child: Focus(
+        focusNode: focusNode,
+        canRequestFocus: false,
+        skipTraversal: true,
+        includeSemantics: false,
+        child: widget.child,
+      ),
+    );
+  }
+}
+
+// A "marker" inherited widget to make the group faster to find.
+class _FocusTraversalGroupMarker extends InheritedWidget {
+  const _FocusTraversalGroupMarker({
+    @required this.policy,
+    @required this.focusNode,
+    Widget child,
+  })  : assert(policy != null),
+        assert(focusNode != null),
+        super(child: child);
+
+  final FocusTraversalPolicy policy;
+  final FocusNode focusNode;
+
+  @override
+  bool updateShouldNotify(InheritedWidget oldWidget) => false;
+}
+
+/// A deprecated widget that describes the inherited focus policy for focus
+/// traversal for its descendants.
+///
+/// _This widget has been deprecated: use [FocusTraversalGroup] instead._
+@Deprecated(
+  'Use FocusTraversalGroup as a replacement for DefaultFocusTraversal. Be aware that FocusTraversalGroup does add an (unfocusable) Focus widget to the hierarchy that DefaultFocusTraversal does not. Use FocusTraversalGroup.of(context) as a replacement for DefaultFocusTraversal.of(context). '
+  'This feature was deprecated after v1.14.3.'
+)
 class DefaultFocusTraversal extends InheritedWidget {
   /// Creates a [DefaultFocusTraversal] object.
   ///
@@ -846,7 +1578,10 @@
     @required Widget child,
   }) : super(key: key, child: child);
 
-  /// The policy used to move the focus from one focus node to another.
+  /// The policy used to move the focus from one focus node to another when
+  /// traversing them using a keyboard.
+  ///
+  /// _This widget has been deprecated: use [FocusTraversalGroup] instead._
   ///
   /// If not specified, traverses in reading order using
   /// [ReadingOrderTraversalPolicy].
@@ -855,7 +1590,7 @@
   ///
   ///  * [FocusTraversalPolicy] for the API used to impose traversal order
   ///    policy.
-  ///  * [WidgetOrderFocusTraversalPolicy] for a traversal policy that traverses
+  ///  * [WidgetOrderTraversalPolicy] for a traversal policy that traverses
   ///    nodes in the order they are added to the widget tree.
   ///  * [ReadingOrderTraversalPolicy] for a traversal policy that traverses
   ///    nodes in the reading order defined in the widget tree, and then top to
@@ -865,24 +1600,37 @@
   /// Returns the [FocusTraversalPolicy] that most tightly encloses the given
   /// [BuildContext].
   ///
+  /// _This method has been deprecated: use `FocusTraversalGroup.of(context)` instead._
+  ///
+  /// It does not create a rebuild dependency because changing the traversal
+  /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
+  /// result of an order change.
+  ///
   /// The [context] argument must not be null.
-  static FocusTraversalPolicy of(BuildContext context, { bool nullOk = false }) {
-    assert(context != null);
-    final DefaultFocusTraversal inherited = context.dependOnInheritedWidgetOfExactType<DefaultFocusTraversal>();
+  static FocusTraversalPolicy of(BuildContext context, {bool nullOk = false}) {
+    final DefaultFocusTraversal inherited = context.getElementForInheritedWidgetOfExactType<DefaultFocusTraversal>()?.widget as DefaultFocusTraversal;
     assert(() {
       if (nullOk) {
         return true;
       }
+      if (context == null) {
+        throw FlutterError(
+          'The context given to DefaultFocusTraversal.of was null, so '
+          'consequently no FocusTraversalGroup ancestor can be found.',
+        );
+      }
       if (inherited == null) {
-        throw FlutterError('Unable to find a DefaultFocusTraversal widget in the context.\n'
-            'DefaultFocusTraversal.of() was called with a context that does not contain a '
-            'DefaultFocusTraversal.\n'
-            'No DefaultFocusTraversal ancestor could be found starting from the context that was '
-            'passed to DefaultFocusTraversal.of(). This can happen because there is not a '
-            'WidgetsApp or MaterialApp widget (those widgets introduce a DefaultFocusTraversal), '
-            'or it can happen if the context comes from a widget above those widgets.\n'
-            'The context used was:\n'
-            '  $context');
+        throw FlutterError(
+          'Unable to find a DefaultFocusTraversal widget in the context.\n'
+          'DefaultFocusTraversal.of() was called with a context that does not contain a '
+          'DefaultFocusTraversal.\n'
+          'No DefaultFocusTraversal ancestor could be found starting from the context that was '
+          'passed to DefaultFocusTraversal.of(). This can happen because there is not a '
+          'WidgetsApp or MaterialApp widget (those widgets introduce a DefaultFocusTraversal), '
+          'or it can happen if the context comes from a widget above those widgets.\n'
+          'The context used was:\n'
+          '  $context',
+        );
       }
       return true;
     }());
@@ -890,7 +1638,7 @@
   }
 
   @override
-  bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
+  bool updateShouldNotify(DefaultFocusTraversal oldWidget) => false;
 }
 
 // A base class for all of the default actions that request focus for a node.
@@ -988,7 +1736,8 @@
   /// Creates a [DirectionalFocusIntent] with a fixed [key], and the given
   /// [direction].
   const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true})
-      : assert(ignoreTextFields != null), super(DirectionalFocusAction.key);
+      : assert(ignoreTextFields != null),
+        super(DirectionalFocusAction.key);
 
   /// The direction in which to look for the next focusable node when the
   /// associated [DirectionalFocusAction] is invoked.
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 892927f..78ed8e1 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -2191,6 +2191,8 @@
   /// Obtains the element corresponding to the nearest widget of the given type [T],
   /// which must be the type of a concrete [InheritedWidget] subclass.
   ///
+  /// Returns null if no such element is found.
+  ///
   /// Calling this method is O(1) with a small constant factor.
   ///
   /// This method does not establish a relationship with the target in the way
diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart
index dc4c85d..3b60092 100644
--- a/packages/flutter/test/material/checkbox_test.dart
+++ b/packages/flutter/test/material/checkbox_test.dart
@@ -68,7 +68,7 @@
       ),
     ));
 
-    expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
+    expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
       hasCheckedState: true,
       hasEnabledState: true,
       isEnabled: true,
@@ -83,7 +83,7 @@
       ),
     ));
 
-    expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
+    expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
       hasCheckedState: true,
       hasEnabledState: true,
       isChecked: true,
@@ -99,7 +99,7 @@
       ),
     ));
 
-    expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
+    expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics(
       hasCheckedState: true,
       hasEnabledState: true,
     ));
@@ -111,7 +111,7 @@
       ),
     ));
 
-    expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
+    expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics(
       hasCheckedState: true,
       hasEnabledState: true,
       isChecked: true,
diff --git a/packages/flutter/test/material/debug_test.dart b/packages/flutter/test/material/debug_test.dart
index ce29aa4..1b1d7ab 100644
--- a/packages/flutter/test/material/debug_test.dart
+++ b/packages/flutter/test/material/debug_test.dart
@@ -116,7 +116,7 @@
       '   The ancestors of this widget were:\n'
       '     Semantics\n'
       '     Builder\n'
-      '     RepaintBoundary-[GlobalKey#2d465]\n'
+      '     RepaintBoundary-[GlobalKey#00000]\n'
       '     IgnorePointer\n'
       '     AnimatedBuilder\n'
       '     FadeTransition\n'
@@ -131,19 +131,19 @@
       '     PageStorage\n'
       '     Offstage\n'
       '     _ModalScopeStatus\n'
-      '     _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#969b7]\n'
+      '     _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
       '     _EffectiveTickerMode\n'
       '     TickerMode\n'
-      '     _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n'
+      '     _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
       '     _Theatre\n'
-      '     Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n'
+      '     Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
       '     _FocusMarker\n'
       '     Semantics\n'
       '     FocusScope\n'
       '     AbsorbPointer\n'
       '     _PointerListener\n'
       '     Listener\n'
-      '     Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#10579]\n'
+      '     Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#00000]\n'
       '     IconTheme\n'
       '     IconTheme\n'
       '     _InheritedCupertinoTheme\n'
@@ -158,19 +158,22 @@
       '     CheckedModeBanner\n'
       '     Title\n'
       '     Directionality\n'
-      '     _LocalizationsScope-[GlobalKey#a51e3]\n'
+      '     _LocalizationsScope-[GlobalKey#00000]\n'
       '     Semantics\n'
       '     Localizations\n'
       '     MediaQuery\n'
       '     _MediaQueryFromWindow\n'
-      '     DefaultFocusTraversal\n'
+      '     _FocusMarker\n'
+      '     Focus\n'
+      '     _FocusTraversalGroupMarker\n'
+      '     FocusTraversalGroup\n'
       '     Actions\n'
       '     _ShortcutsMarker\n'
       '     Semantics\n'
       '     _FocusMarker\n'
       '     Focus\n'
       '     Shortcuts\n'
-      '     WidgetsApp-[GlobalObjectKey _MaterialAppState#38e79]\n'
+      '     WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n'
       '     ScrollConfiguration\n'
       '     MaterialApp\n'
       '     [root]\n'
diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart
index 094c950..39b011a 100644
--- a/packages/flutter/test/material/icon_button_test.dart
+++ b/packages/flutter/test/material/icon_button_test.dart
@@ -570,7 +570,7 @@
 }
 
 Widget wrap({ Widget child }) {
-  return DefaultFocusTraversal(
+  return FocusTraversalGroup(
     policy: ReadingOrderTraversalPolicy(),
     child: Directionality(
       textDirection: TextDirection.ltr,
diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart
index d97280c..101b87c 100644
--- a/packages/flutter/test/widgets/focus_scope_test.dart
+++ b/packages/flutter/test/widgets/focus_scope_test.dart
@@ -598,7 +598,7 @@
       // This checks both FocusScopes that have their own nodes, as well as those
       // that use external nodes.
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           child: Column(
             children: <Widget>[
               FocusScope(
@@ -661,7 +661,7 @@
       final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
 
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           child: Column(
             children: <Widget>[
               FocusScope(
@@ -711,7 +711,7 @@
       expect(find.text('b'), findsOneWidget);
 
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           child: Column(
             children: <Widget>[
               FocusScope(
@@ -746,7 +746,7 @@
       final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
 
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           child: Column(
             children: <Widget>[
               FocusScope(
@@ -794,7 +794,7 @@
       expect(find.text('b'), findsOneWidget);
 
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           child: Column(
             children: <Widget>[
               FocusScope(
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index 4f286d8..6517a68 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -11,16 +11,18 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 
+import 'semantics_tester.dart';
+
 void main() {
-  group(WidgetOrderFocusTraversalPolicy, () {
+  group(WidgetOrderTraversalPolicy, () {
     testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async {
       final GlobalKey key1 = GlobalKey(debugLabel: '1');
       final GlobalKey key2 = GlobalKey(debugLabel: '2');
       final GlobalKey key3 = GlobalKey(debugLabel: '3');
       final GlobalKey key4 = GlobalKey(debugLabel: '4');
       final GlobalKey key5 = GlobalKey(debugLabel: '5');
-      await tester.pumpWidget(DefaultFocusTraversal(
-        policy: WidgetOrderFocusTraversalPolicy(),
+      await tester.pumpWidget(FocusTraversalGroup(
+        policy: WidgetOrderTraversalPolicy(),
         child: FocusScope(
           key: key1,
           child: Column(
@@ -64,8 +66,8 @@
       bool focus3;
       bool focus5;
       await tester.pumpWidget(
-        DefaultFocusTraversal(
-          policy: WidgetOrderFocusTraversalPolicy(),
+        FocusTraversalGroup(
+          policy: WidgetOrderTraversalPolicy(),
           child: FocusScope(
             debugLabel: 'key1',
             key: key1,
@@ -177,8 +179,8 @@
       final GlobalKey key5 = GlobalKey(debugLabel: '5');
       final GlobalKey key6 = GlobalKey(debugLabel: '6');
       await tester.pumpWidget(
-        DefaultFocusTraversal(
-          policy: WidgetOrderFocusTraversalPolicy(),
+        FocusTraversalGroup(
+          policy: WidgetOrderTraversalPolicy(),
           child: FocusScope(
             key: key1,
             child: Column(
@@ -250,8 +252,8 @@
       final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
       await tester.pumpWidget(
         MaterialApp(
-          home: DefaultFocusTraversal(
-            policy: WidgetOrderFocusTraversalPolicy(),
+          home: FocusTraversalGroup(
+            policy: WidgetOrderTraversalPolicy(),
             child: Center(
               child: Builder(builder: (BuildContext context) {
                 return MaterialButton(
@@ -317,7 +319,7 @@
       final GlobalKey key3 = GlobalKey(debugLabel: '3');
       final GlobalKey key4 = GlobalKey(debugLabel: '4');
       final GlobalKey key5 = GlobalKey(debugLabel: '5');
-      await tester.pumpWidget(DefaultFocusTraversal(
+      await tester.pumpWidget(FocusTraversalGroup(
         policy: ReadingOrderTraversalPolicy(),
         child: FocusScope(
           key: key1,
@@ -364,7 +366,7 @@
       await tester.pumpWidget(
         Directionality(
           textDirection: TextDirection.ltr,
-          child: DefaultFocusTraversal(
+          child: FocusTraversalGroup(
             policy: ReadingOrderTraversalPolicy(),
             child: FocusScope(
               debugLabel: 'key1',
@@ -473,7 +475,7 @@
       final GlobalKey key5 = GlobalKey(debugLabel: '5');
       final GlobalKey key6 = GlobalKey(debugLabel: '6');
       await tester.pumpWidget(
-        DefaultFocusTraversal(
+        FocusTraversalGroup(
           policy: ReadingOrderTraversalPolicy(),
           child: FocusScope(
             key: key1,
@@ -538,6 +540,579 @@
       expect(secondFocusNode.hasFocus, isFalse);
       expect(scope.hasFocus, isTrue);
     });
+
+    testWidgets('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+      final FocusScopeNode scopeNode = FocusScopeNode();
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
+      Widget buildTest(TextDirection topDirection) {
+        return Directionality(
+          textDirection: topDirection,
+          child: FocusTraversalGroup(
+            policy: ReadingOrderTraversalPolicy(),
+            child: FocusScope(
+              node: scopeNode,
+              child: Column(
+                children: <Widget>[
+                  Directionality(
+                    textDirection: TextDirection.ltr,
+                    child: Row(children: <Widget>[
+                      Focus(
+                        focusNode: nodes[0],
+                        child: Container(width: 10, height: 10),
+                      ),
+                      Focus(
+                        focusNode: nodes[1],
+                        child: Container(width: 10, height: 10),
+                      ),
+                      Focus(
+                        focusNode: nodes[2],
+                        child: Container(width: 10, height: 10),
+                      ),
+                    ]),
+                  ),
+                  Directionality(
+                    textDirection: TextDirection.ltr,
+                    child: Row(children: <Widget>[
+                      Directionality(
+                        textDirection: TextDirection.rtl,
+                        child: Focus(
+                          focusNode: nodes[3],
+                          child: Container(width: 10, height: 10),
+                        ),
+                      ),
+                      Directionality(
+                        textDirection: TextDirection.rtl,
+                        child: Focus(
+                          focusNode: nodes[4],
+                          child: Container(width: 10, height: 10),
+                        ),
+                      ),
+                      Directionality(
+                        textDirection: TextDirection.ltr,
+                        child: Focus(
+                          focusNode: nodes[5],
+                          child: Container(width: 10, height: 10),
+                        ),
+                      ),
+                    ]),
+                  ),
+                  Row(children: <Widget>[
+                    Directionality(
+                      textDirection: TextDirection.ltr,
+                      child: Focus(
+                        focusNode: nodes[6],
+                        child: Container(width: 10, height: 10),
+                      ),
+                    ),
+                    Directionality(
+                      textDirection: TextDirection.rtl,
+                      child: Focus(
+                        focusNode: nodes[7],
+                        child: Container(width: 10, height: 10),
+                      ),
+                    ),
+                    Directionality(
+                      textDirection: TextDirection.rtl,
+                      child: Focus(
+                        focusNode: nodes[8],
+                        child: Container(width: 10, height: 10),
+                      ),
+                    ),
+                    Directionality(
+                      textDirection: TextDirection.ltr,
+                      child: Focus(
+                        focusNode: nodes[9],
+                        child: Container(width: 10, height: 10),
+                      ),
+                    ),
+                  ]),
+                ],
+              ),
+            ),
+          ),
+        );
+      }
+      await tester.pumpWidget(buildTest(TextDirection.rtl));
+
+      // The last four *are* correct: the Row is sensitive to the directionality
+      // too, so it swaps the positions of 7 and 8.
+      final List<int> order = <int>[];
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 7, 8, 9]));
+
+      await tester.pumpWidget(buildTest(TextDirection.ltr));
+
+      order.clear();
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 8, 7, 9]));
+    });
+
+    testWidgets('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.rtl,
+          child: FocusTraversalGroup(
+            policy: ReadingOrderTraversalPolicy(),
+            child: Stack(
+              alignment: const Alignment(-1, -1),
+              children: List<Widget>.generate(nodeCount, (int index) {
+                // Boxes that all have the same upper left origin corner.
+                return Focus(
+                  focusNode: nodes[index],
+                  child: Container(width: 10.0 * (index + 1), height: 10.0 * (index + 1)),
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+
+      final List<int> order = <int>[];
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(<int>[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
+
+      // Concentric boxes.
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.rtl,
+          child: FocusTraversalGroup(
+            policy: ReadingOrderTraversalPolicy(),
+            child: Stack(
+              alignment: const Alignment(0, 0),
+              children: List<Widget>.generate(nodeCount, (int index) {
+                return Focus(
+                  focusNode: nodes[index],
+                  child: Container(width: 10.0 * (index + 1), height: 10.0 * (index + 1)),
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+
+      order.clear();
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(<int>[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
+
+      // Stacked (vertically) and centered (horizontally, on each other)
+      // widgets, not overlapping.
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.rtl,
+          child: FocusTraversalGroup(
+            policy: ReadingOrderTraversalPolicy(),
+            child: Stack(
+              alignment: const Alignment(0, 0),
+              children: List<Widget>.generate(nodeCount, (int index) {
+                return Positioned(
+                  top: 5.0 * index * (index + 1),
+                  left: 5.0 * (9 - index),
+                  child: Focus(
+                    focusNode: nodes[index],
+                    child: Container(
+                      decoration: BoxDecoration(border: Border.all()),
+                      width: 10.0 * (index + 1),
+                      height: 10.0 * (index + 1),
+                    ),
+                  ),
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+
+      order.clear();
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]));
+    });
+  });
+
+  group(OrderedTraversalPolicy, () {
+    testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async {
+      final GlobalKey key1 = GlobalKey(debugLabel: '1');
+      final GlobalKey key2 = GlobalKey(debugLabel: '2');
+      await tester.pumpWidget(FocusTraversalGroup(
+        policy: OrderedTraversalPolicy(secondary: ReadingOrderTraversalPolicy()),
+        child: FocusScope(
+          child: Column(
+            children: <Widget>[
+              FocusTraversalOrder(
+                order: const NumericFocusOrder(2),
+                child: Focus(
+                  child: Container(key: key1, width: 100, height: 100),
+                ),
+              ),
+              FocusTraversalOrder(
+                order: const NumericFocusOrder(1),
+                child: Focus(
+                  child: Container(key: key2, width: 100, height: 100),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ));
+
+      final Element firstChild = tester.element(find.byKey(key1));
+      final Element secondChild = tester.element(find.byKey(key2));
+      final FocusNode firstFocusNode = Focus.of(firstChild);
+      final FocusNode secondFocusNode = Focus.of(secondChild);
+      final FocusNode scope = Focus.of(firstChild).enclosingScope;
+      secondFocusNode.nextFocus();
+
+      await tester.pump();
+
+      expect(firstFocusNode.hasFocus, isFalse);
+      expect(secondFocusNode.hasFocus, isTrue);
+      expect(scope.hasFocus, isTrue);
+    });
+
+    testWidgets('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.rtl,
+          child: FocusTraversalGroup(
+            policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+            child: FocusScope(
+              child: Row(
+                children: List<Widget>.generate(
+                  nodeCount,
+                  (int index) => Focus(
+                    focusNode: nodes[index],
+                    child: Container(width: 10, height: 10),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // Because it should be using widget order, this shouldn't be affected by
+      // the directionality.
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
+      }
+
+      // Now check backwards.
+      for (int i = nodeCount - 1; i > 0; --i) {
+        nodes.first.previousFocus();
+        await tester.pump();
+        expect(nodes[i - 1].hasPrimaryFocus, isTrue, reason: "node ${i - 1} doesn't have focus, but should");
+      }
+    });
+
+    testWidgets('Move focus to next/previous node using numerical order.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: FocusTraversalGroup(
+            policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+            child: FocusScope(
+              child: Row(
+                children: List<Widget>.generate(
+                  nodeCount,
+                  (int index) => FocusTraversalOrder(
+                    order: NumericFocusOrder(nodeCount - index.toDouble()),
+                    child: Focus(
+                      focusNode: nodes[index],
+                      child: Container(width: 10, height: 10),
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // The orders are assigned to be backwards from normal, so should go backwards.
+      for (int i = nodeCount - 1; i >= 0; --i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
+      }
+
+      // Now check backwards.
+      for (int i = 1; i < nodeCount; ++i) {
+        nodes.first.previousFocus();
+        await tester.pump();
+        expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
+      }
+    });
+
+    testWidgets('Move focus to next/previous node using lexical order.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+
+      /// Generate ['J' ... 'A'];
+      final List<String> keys = List<String>.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1));
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node ${keys[index]}'));
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: FocusTraversalGroup(
+            policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+            child: FocusScope(
+              child: Row(
+                children: List<Widget>.generate(
+                  nodeCount,
+                  (int index) => FocusTraversalOrder(
+                    order: LexicalFocusOrder(keys[index]),
+                    child: Focus(
+                      focusNode: nodes[index],
+                      child: Container(width: 10, height: 10),
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // The orders are assigned to be backwards from normal, so should go backwards.
+      for (int i = nodeCount - 1; i >= 0; --i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
+      }
+
+      // Now check backwards.
+      for (int i = 1; i < nodeCount; ++i) {
+        nodes.first.previousFocus();
+        await tester.pump();
+        expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
+      }
+    });
+
+    testWidgets('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async {
+      const int nodeCount = 10;
+      final FocusScopeNode scopeNode = FocusScopeNode();
+      final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: FocusTraversalGroup(
+            policy: WidgetOrderTraversalPolicy(),
+            child: FocusScope(
+              node: scopeNode,
+              child: FocusTraversalGroup(
+                policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+                child: Row(
+                  children: <Widget>[
+                    FocusTraversalOrder(
+                      order: const NumericFocusOrder(0),
+                      child: FocusTraversalGroup(
+                        policy: WidgetOrderTraversalPolicy(),
+                        child: Row(children: <Widget>[
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(9),
+                            child: Focus(
+                              focusNode: nodes[9],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(8),
+                            child: Focus(
+                              focusNode: nodes[8],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(7),
+                            child: Focus(
+                              focusNode: nodes[7],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                        ]),
+                      ),
+                    ),
+                    FocusTraversalOrder(
+                      order: const NumericFocusOrder(1),
+                      child: FocusTraversalGroup(
+                        policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+                        child: Row(children: <Widget>[
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(4),
+                            child: Focus(
+                              focusNode: nodes[4],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(5),
+                            child: Focus(
+                              focusNode: nodes[5],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const NumericFocusOrder(6),
+                            child: Focus(
+                              focusNode: nodes[6],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                        ]),
+                      ),
+                    ),
+                    FocusTraversalOrder(
+                      order: const NumericFocusOrder(2),
+                      child: FocusTraversalGroup(
+                        policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+                        child: Row(children: <Widget>[
+                          FocusTraversalOrder(
+                            order: const LexicalFocusOrder('D'),
+                            child: Focus(
+                              focusNode: nodes[3],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const LexicalFocusOrder('C'),
+                            child: Focus(
+                              focusNode: nodes[2],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const LexicalFocusOrder('B'),
+                            child: Focus(
+                              focusNode: nodes[1],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                          FocusTraversalOrder(
+                            order: const LexicalFocusOrder('A'),
+                            child: Focus(
+                              focusNode: nodes[0],
+                              child: Container(width: 10, height: 10),
+                            ),
+                          ),
+                        ]),
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      final List<int> expectedOrder = <int>[9, 8, 7, 4, 5, 6, 0, 1, 2, 3];
+      final List<int> order = <int>[];
+      for (int i = 0; i < nodeCount; ++i) {
+        nodes.first.nextFocus();
+        await tester.pump();
+        order.add(nodes.indexOf(primaryFocus));
+      }
+      expect(order, orderedEquals(expectedOrder));
+    });
+
+    testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async {
+      final GlobalKey key1 = GlobalKey(debugLabel: '1');
+      final GlobalKey key2 = GlobalKey(debugLabel: '2');
+      final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node');
+      final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
+      await tester.pumpWidget(
+        MaterialApp(
+          home: FocusTraversalGroup(
+            policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
+            child: Center(
+              child: Builder(builder: (BuildContext context) {
+                return FocusTraversalOrder(
+                  order: const NumericFocusOrder(0),
+                  child: MaterialButton(
+                    key: key1,
+                    focusNode: testNode1,
+                    autofocus: true,
+                    onPressed: () {
+                      Navigator.of(context).push<void>(
+                        MaterialPageRoute<void>(
+                          builder: (BuildContext context) {
+                            return Center(
+                              child: FocusTraversalOrder(
+                                order: const NumericFocusOrder(0),
+                                child: MaterialButton(
+                                  key: key2,
+                                  focusNode: testNode2,
+                                  autofocus: true,
+                                  onPressed: () {
+                                    Navigator.of(context).pop();
+                                  },
+                                  child: const Text('Go Back'),
+                                ),
+                              ),
+                            );
+                          },
+                        ),
+                      );
+                    },
+                    child: const Text('Go Forward'),
+                  ),
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+
+      final Element firstChild = tester.element(find.text('Go Forward'));
+      final FocusNode firstFocusNode = Focus.of(firstChild);
+      final FocusNode scope = Focus.of(firstChild).enclosingScope;
+      await tester.pump();
+
+      expect(firstFocusNode.hasFocus, isTrue);
+      expect(scope.hasFocus, isTrue);
+
+      await tester.tap(find.text('Go Forward'));
+      await tester.pumpAndSettle();
+
+      final Element secondChild = tester.element(find.text('Go Back'));
+      final FocusNode secondFocusNode = Focus.of(secondChild);
+
+      expect(firstFocusNode.hasFocus, isFalse);
+      expect(secondFocusNode.hasFocus, isTrue);
+
+      await tester.tap(find.text('Go Back'));
+      await tester.pumpAndSettle();
+
+      expect(firstFocusNode.hasFocus, isTrue);
+      expect(scope.hasFocus, isTrue);
+    });
   });
 
   group(DirectionalFocusTraversalPolicyMixin, () {
@@ -553,8 +1128,8 @@
       await tester.pumpWidget(
         Directionality(
           textDirection: TextDirection.ltr,
-          child: DefaultFocusTraversal(
-            policy: WidgetOrderFocusTraversalPolicy(),
+          child: FocusTraversalGroup(
+            policy: WidgetOrderTraversalPolicy(),
             child: FocusScope(
               debugLabel: 'Scope',
               child: Column(
@@ -706,8 +1281,8 @@
       await tester.pumpWidget(
         Directionality(
           textDirection: TextDirection.ltr,
-          child: DefaultFocusTraversal(
-            policy: WidgetOrderFocusTraversalPolicy(),
+          child: FocusTraversalGroup(
+            policy: WidgetOrderTraversalPolicy(),
             child: FocusScope(
               debugLabel: 'Scope',
               child: Column(
@@ -827,8 +1402,8 @@
       await tester.pumpWidget(
         Directionality(
           textDirection: TextDirection.ltr,
-          child: DefaultFocusTraversal(
-            policy: WidgetOrderFocusTraversalPolicy(),
+          child: FocusTraversalGroup(
+            policy: WidgetOrderTraversalPolicy(),
             child: FocusScope(
               debugLabel: 'scope',
               child: Column(
@@ -871,7 +1446,7 @@
 
       await tester.pump();
 
-      final FocusTraversalPolicy policy = DefaultFocusTraversal.of(upperLeftKey.currentContext);
+      final FocusTraversalPolicy policy = FocusTraversalGroup.of(upperLeftKey.currentContext);
 
       expect(policy.findFirstFocusInDirection(scope, TraversalDirection.up), equals(lowerLeftNode));
       expect(policy.findFirstFocusInDirection(scope, TraversalDirection.down), equals(upperLeftNode));
@@ -885,7 +1460,7 @@
       final FocusNode focusBottom = FocusNode(debugLabel: 'bottom');
 
       final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy();
-      await tester.pumpWidget(DefaultFocusTraversal(
+      await tester.pumpWidget(FocusTraversalGroup(
         policy: policy,
         child: FocusScope(
           debugLabel: 'Scope',
@@ -909,7 +1484,7 @@
       expect(focusBottom.hasFocus, isTrue);
 
       // Remove center focus node.
-      await tester.pumpWidget(DefaultFocusTraversal(
+      await tester.pumpWidget(FocusTraversalGroup(
         policy: policy,
         child: FocusScope(
           debugLabel: 'Scope',
@@ -1380,6 +1955,14 @@
       expect(events.length, 2);
     });
   });
+  group(FocusTraversalGroup, () {
+    testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async {
+      final SemanticsTester semantics = SemanticsTester(tester);
+      await tester.pumpWidget(FocusTraversalGroup(child: Container()));
+      final TestSemantics expectedSemantics = TestSemantics.root();
+      expect(semantics, hasSemantics(expectedSemantics));
+    });
+  });
 }
 
 class TestRoute extends PageRouteBuilder<void> {