@@ -496,29 +496,43 @@ class _DirectionalPolicyData {
496
496
/// only want to implement new next/previous policies.
497
497
///
498
498
/// 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.
504
504
///
505
505
/// For instance, if the focus moves down, down, down, and then up, up, up, it
506
506
/// will follow the same path through the widgets in both directions. However,
507
507
/// if it moves down, down, down, left, right, and then up, up, up, it may not
508
508
/// follow the same path on the way up as it did on the way down, since changing
509
509
/// the axis of motion resets the history.
510
510
///
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
+ ///
511
525
/// See also:
512
526
///
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.
522
536
mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
523
537
final Map <FocusScopeNode , _DirectionalPolicyData > _policyData = < FocusScopeNode , _DirectionalPolicyData > {};
524
538
@@ -622,6 +636,54 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
622
636
return sorted;
623
637
}
624
638
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
+
625
687
// Sorts nodes from left to right horizontally, and removes nodes that are
626
688
// either to the right of the left side of the target node if we're going
627
689
// 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 {
843
905
break ;
844
906
}
845
907
// 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;
848
911
break ;
849
912
case TraversalDirection .right:
850
913
case TraversalDirection .left:
@@ -869,8 +932,9 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
869
932
break ;
870
933
}
871
934
// 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;
874
938
break ;
875
939
}
876
940
if (found != null ) {
0 commit comments