@@ -496,29 +496,43 @@ class _DirectionalPolicyData {
496496/// only want to implement new next/previous policies.
497497///
498498/// Since hysteresis in the navigation order is undesirable, this implementation
499- /// maintains a stack of previous locations that have been visited on the
500- /// policy data for the affected [FocusScopeNode] . If the previous direction
501- /// was the opposite of the current direction, then the this policy will request
502- /// focus on the previously focused node. Change to another direction other than
503- /// the current one or its opposite will clear the stack.
499+ /// maintains a stack of previous locations that have been visited on the policy
500+ /// data for the affected [FocusScopeNode] . If the previous direction was the
501+ /// opposite of the current direction, then the this policy will request focus
502+ /// on the previously focused node. Change to another direction other than the
503+ /// current one or its opposite will clear the stack.
504504///
505505/// For instance, if the focus moves down, down, down, and then up, up, up, it
506506/// will follow the same path through the widgets in both directions. However,
507507/// if it moves down, down, down, left, right, and then up, up, up, it may not
508508/// follow the same path on the way up as it did on the way down, since changing
509509/// the axis of motion resets the history.
510510///
511+ /// This class implements an algorithm that considers an infinite band extending
512+ /// along the direction of movement, the width or height (depending on
513+ /// direction) of the currently focused widget, and finds the closest widget in
514+ /// that band along the direction of movement. If nothing is found in that band,
515+ /// then it picks the widget with an edge closest to the band in the
516+ /// perpendicular direction. If two out-of-band widgets are the same distance
517+ /// from the band, then it picks the one closest along the direction of
518+ /// movement.
519+ ///
520+ /// The goal of this algorithm is to pick a widget that (to the user) doesn't
521+ /// appear to traverse along the wrong axis, as it might if it only sorted
522+ /// widgets by distance along one axis, but also jumps to the next logical
523+ /// widget in a direction without skipping over widgets.
524+ ///
511525/// See also:
512526///
513- /// * [FocusNode] , for a description of the focus system.
514- /// * [FocusTraversalGroup] , a widget that groups together and imposes a
515- /// traversal policy on the [Focus] nodes below it in the widget hierarchy.
516- /// * [WidgetOrderTraversalPolicy] , a policy that relies on the widget
517- /// creation order to describe the order of traversal.
518- /// * [ReadingOrderTraversalPolicy] , a policy that describes the order as the
519- /// natural "reading order" for the current [Directionality].
520- /// * [OrderedTraversalPolicy] , a policy that describes the order
521- /// explicitly using [FocusTraversalOrder] widgets.
527+ /// * [FocusNode] , for a description of the focus system.
528+ /// * [FocusTraversalGroup] , a widget that groups together and imposes a
529+ /// traversal policy on the [Focus] nodes below it in the widget hierarchy.
530+ /// * [WidgetOrderTraversalPolicy] , a policy that relies on the widget creation
531+ /// order to describe the order of traversal.
532+ /// * [ReadingOrderTraversalPolicy] , a policy that describes the order as the
533+ /// natural "reading order" for the current [Directionality] .
534+ /// * [OrderedTraversalPolicy] , a policy that describes the order explicitly
535+ /// using [FocusTraversalOrder] widgets.
522536mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
523537 final Map <FocusScopeNode , _DirectionalPolicyData > _policyData = < FocusScopeNode , _DirectionalPolicyData > {};
524538
@@ -622,6 +636,54 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
622636 return sorted;
623637 }
624638
639+ static int _verticalCompareClosestEdge (Offset target, Rect a, Rect b) {
640+ // Find which edge is closest to the target for each.
641+ final double aCoord = (a.top - target.dy).abs () < (a.bottom - target.dy).abs () ? a.top : a.bottom;
642+ final double bCoord = (b.top - target.dy).abs () < (b.bottom - target.dy).abs () ? b.top : b.bottom;
643+ return (aCoord - target.dy).abs ().compareTo ((bCoord - target.dy).abs ());
644+ }
645+
646+ static int _horizontalCompareClosestEdge (Offset target, Rect a, Rect b) {
647+ // Find which edge is closest to the target for each.
648+ final double aCoord = (a.left - target.dx).abs () < (a.right - target.dx).abs () ? a.left : a.right;
649+ final double bCoord = (b.left - target.dx).abs () < (b.right - target.dx).abs () ? b.left : b.right;
650+ return (aCoord - target.dx).abs ().compareTo ((bCoord - target.dx).abs ());
651+ }
652+
653+ // Sort the ones that have edges that are closest horizontally first, and if
654+ // two are the same horizontal distance, pick the one that is closest
655+ // vertically.
656+ static Iterable <FocusNode > _sortClosestEdgesByDistancePreferHorizontal (Offset target, Iterable <FocusNode > nodes) {
657+ final List <FocusNode > sorted = nodes.toList ();
658+ mergeSort <FocusNode >(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
659+ final int horizontal = _horizontalCompareClosestEdge (target, nodeA.rect, nodeB.rect);
660+ if (horizontal == 0 ) {
661+ // If they're the same distance horizontally, pick the closest one
662+ // vertically.
663+ return _verticalCompare (target, nodeA.rect.center, nodeB.rect.center);
664+ }
665+ return horizontal;
666+ });
667+ return sorted;
668+ }
669+
670+ // Sort the ones that have edges that are closest vertically first, and if
671+ // two are the same vertical distance, pick the one that is closest
672+ // horizontally.
673+ static Iterable <FocusNode > _sortClosestEdgesByDistancePreferVertical (Offset target, Iterable <FocusNode > nodes) {
674+ final List <FocusNode > sorted = nodes.toList ();
675+ mergeSort <FocusNode >(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
676+ final int vertical = _verticalCompareClosestEdge (target, nodeA.rect, nodeB.rect);
677+ if (vertical == 0 ) {
678+ // If they're the same distance vertically, pick the closest one
679+ // horizontally.
680+ return _horizontalCompare (target, nodeA.rect.center, nodeB.rect.center);
681+ }
682+ return vertical;
683+ });
684+ return sorted;
685+ }
686+
625687 // Sorts nodes from left to right horizontally, and removes nodes that are
626688 // either to the right of the left side of the target node if we're going
627689 // left, or to the left of the right side of the target node if we're going
@@ -843,8 +905,9 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
843905 break ;
844906 }
845907 // Only out-of-band targets are eligible, so pick the one that is
846- // closest the to the center line horizontally.
847- found = _sortByDistancePreferHorizontal (focusedChild.rect.center, eligibleNodes).first;
908+ // closest to the center line horizontally, and if any are the same
909+ // distance horizontally, pick the closest one of those vertically.
910+ found = _sortClosestEdgesByDistancePreferHorizontal (focusedChild.rect.center, eligibleNodes).first;
848911 break ;
849912 case TraversalDirection .right:
850913 case TraversalDirection .left:
@@ -869,8 +932,9 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
869932 break ;
870933 }
871934 // Only out-of-band targets are eligible, so pick the one that is
872- // to the center line vertically.
873- found = _sortByDistancePreferVertical (focusedChild.rect.center, eligibleNodes).first;
935+ // closest to the center line vertically, and if any are the same
936+ // distance vertically, pick the closest one of those horizontally.
937+ found = _sortClosestEdgesByDistancePreferVertical (focusedChild.rect.center, eligibleNodes).first;
874938 break ;
875939 }
876940 if (found != null ) {
0 commit comments