@@ -82,9 +82,6 @@ enum TraversalDirection {
8282 /// This direction is unaffected by the [Directionality] of the current
8383 /// context.
8484 left,
85-
86- // TODO(gspencer): Add diagonal traversal directions used by TV remotes and
87- // game controllers when we support them.
8885}
8986
9087/// An object used to specify a focus traversal policy used for configuring a
@@ -547,6 +544,46 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
547544 return null ;
548545 }
549546
547+ static int _verticalCompare (Offset target, Offset a, Offset b) {
548+ return (a.dy - target.dy).abs ().compareTo ((b.dy - target.dy).abs ());
549+ }
550+
551+ static int _horizontalCompare (Offset target, Offset a, Offset b) {
552+ return (a.dx - target.dx).abs ().compareTo ((b.dx - target.dx).abs ());
553+ }
554+
555+ // Sort the ones that are closest to target vertically first, and if two are
556+ // the same vertical distance, pick the one that is closest horizontally.
557+ static Iterable <FocusNode > _sortByDistancePreferVertical (Offset target, Iterable <FocusNode > nodes) {
558+ final List <FocusNode > sorted = nodes.toList ();
559+ mergeSort <FocusNode >(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
560+ final Offset a = nodeA.rect.center;
561+ final Offset b = nodeB.rect.center;
562+ final int vertical = _verticalCompare (target, a, b);
563+ if (vertical == 0 ) {
564+ return _horizontalCompare (target, a, b);
565+ }
566+ return vertical;
567+ });
568+ return sorted;
569+ }
570+
571+ // Sort the ones that are closest horizontally first, and if two are the same
572+ // horizontal distance, pick the one that is closest vertically.
573+ static Iterable <FocusNode > _sortByDistancePreferHorizontal (Offset target, Iterable <FocusNode > nodes) {
574+ final List <FocusNode > sorted = nodes.toList ();
575+ mergeSort <FocusNode >(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
576+ final Offset a = nodeA.rect.center;
577+ final Offset b = nodeB.rect.center;
578+ final int horizontal = _horizontalCompare (target, a, b);
579+ if (horizontal == 0 ) {
580+ return _verticalCompare (target, a, b);
581+ }
582+ return horizontal;
583+ });
584+ return sorted;
585+ }
586+
550587 // Sorts nodes from left to right horizontally, and removes nodes that are
551588 // either to the right of the left side of the target node if we're going
552589 // left, or to the left of the right side of the target node if we're going
@@ -555,52 +592,54 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
555592 // This doesn't need to take into account directionality because it is
556593 // typically intending to actually go left or right, not in a reading
557594 // direction.
558- Iterable <FocusNode >? _sortAndFilterHorizontally (
595+ Iterable <FocusNode > _sortAndFilterHorizontally (
559596 TraversalDirection direction,
560597 Rect target,
561- FocusNode nearestScope ,
598+ Iterable < FocusNode > nodes ,
562599 ) {
563600 assert (direction == TraversalDirection .left || direction == TraversalDirection .right);
564- final Iterable <FocusNode > nodes = nearestScope.traversalDescendants;
565- assert (! nodes.contains (nearestScope));
566- final List <FocusNode > sorted = nodes.toList ();
567- mergeSort <FocusNode >(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo (b.rect.center.dx));
568- Iterable <FocusNode >? result;
601+ final Iterable <FocusNode > filtered;
569602 switch (direction) {
570603 case TraversalDirection .left:
571- result = sorted .where ((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left);
604+ filtered = nodes .where ((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left);
572605 break ;
573606 case TraversalDirection .right:
574- result = sorted .where ((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right);
607+ filtered = nodes .where ((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right);
575608 break ;
576609 case TraversalDirection .up:
577610 case TraversalDirection .down:
578- break ;
611+ throw ArgumentError ( 'Invalid direction $ direction ' ) ;
579612 }
580- return result;
613+ final List <FocusNode > sorted = filtered.toList ();
614+ // Sort all nodes from left to right.
615+ mergeSort <FocusNode >(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo (b.rect.center.dx));
616+ return sorted;
581617 }
582618
583619 // Sorts nodes from top to bottom vertically, and removes nodes that are
584620 // either below the top of the target node if we're going up, or above the
585621 // bottom of the target node if we're going down.
586- Iterable <FocusNode >? _sortAndFilterVertically (
622+ Iterable <FocusNode > _sortAndFilterVertically (
587623 TraversalDirection direction,
588624 Rect target,
589625 Iterable <FocusNode > nodes,
590626 ) {
591- final List < FocusNode > sorted = nodes. toList ( );
592- mergeSort <FocusNode >(sorted, compare : ( FocusNode a, FocusNode b) => a.rect.center.dy. compareTo (b.rect.center.dy)) ;
627+ assert (direction == TraversalDirection .up || direction == TraversalDirection .down );
628+ final Iterable <FocusNode > filtered ;
593629 switch (direction) {
594630 case TraversalDirection .up:
595- return sorted.where ((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
631+ filtered = nodes.where ((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
632+ break ;
596633 case TraversalDirection .down:
597- return sorted.where ((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom);
634+ filtered = nodes.where ((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom);
635+ break ;
598636 case TraversalDirection .left:
599637 case TraversalDirection .right:
600- break ;
638+ throw ArgumentError ( 'Invalid direction $ direction ' ) ;
601639 }
602- assert (direction == TraversalDirection .up || direction == TraversalDirection .down);
603- return null ;
640+ final List <FocusNode > sorted = filtered.toList ();
641+ mergeSort <FocusNode >(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dy.compareTo (b.rect.center.dy));
642+ return sorted;
604643 }
605644
606645 // Updates the policy data to keep the previously visited node so that we can
@@ -745,71 +784,55 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
745784 switch (direction) {
746785 case TraversalDirection .down:
747786 case TraversalDirection .up:
748- Iterable <FocusNode >? eligibleNodes = _sortAndFilterVertically (
749- direction,
750- focusedChild.rect,
751- nearestScope.traversalDescendants,
752- );
787+ Iterable <FocusNode > eligibleNodes = _sortAndFilterVertically (direction, focusedChild.rect, nearestScope.traversalDescendants);
788+ if (eligibleNodes.isEmpty) {
789+ break ;
790+ }
753791 if (focusedScrollable != null && ! focusedScrollable.position.atEdge) {
754- final Iterable <FocusNode > filteredEligibleNodes = eligibleNodes! .where ((FocusNode node) => Scrollable .maybeOf (node.context! ) == focusedScrollable);
792+ final Iterable <FocusNode > filteredEligibleNodes = eligibleNodes.where ((FocusNode node) => Scrollable .maybeOf (node.context! ) == focusedScrollable);
755793 if (filteredEligibleNodes.isNotEmpty) {
756794 eligibleNodes = filteredEligibleNodes;
757795 }
758796 }
759- if (eligibleNodes! .isEmpty) {
760- break ;
761- }
762- List <FocusNode > sorted = eligibleNodes.toList ();
763797 if (direction == TraversalDirection .up) {
764- sorted = sorted.reversed. toList ();
798+ eligibleNodes = eligibleNodes. toList ().reversed ;
765799 }
766800 // Find any nodes that intersect the band of the focused child.
767801 final Rect band = Rect .fromLTRB (focusedChild.rect.left, - double .infinity, focusedChild.rect.right, double .infinity);
768- final Iterable <FocusNode > inBand = sorted .where ((FocusNode node) => ! node.rect.intersect (band).isEmpty);
802+ final Iterable <FocusNode > inBand = eligibleNodes .where ((FocusNode node) => ! node.rect.intersect (band).isEmpty);
769803 if (inBand.isNotEmpty) {
770- // The inBand list is already sorted by horizontal distance, so pick
771- // the closest one.
772- found = inBand.first;
804+ found = _sortByDistancePreferVertical (focusedChild.rect.center, inBand).first;
773805 break ;
774806 }
775- // Only out-of-band targets remain, so pick the one that is closest the
776- // to the center line horizontally.
777- mergeSort <FocusNode >(sorted, compare: (FocusNode a, FocusNode b) {
778- return (a.rect.center.dx - focusedChild.rect.center.dx).abs ().compareTo ((b.rect.center.dx - focusedChild.rect.center.dx).abs ());
779- });
780- found = sorted.first;
807+ // Only out-of-band targets are eligible, so pick the one that is
808+ // closest the to the center line horizontally.
809+ found = _sortByDistancePreferHorizontal (focusedChild.rect.center, eligibleNodes).first;
781810 break ;
782811 case TraversalDirection .right:
783812 case TraversalDirection .left:
784- Iterable <FocusNode >? eligibleNodes = _sortAndFilterHorizontally (direction, focusedChild.rect, nearestScope);
813+ Iterable <FocusNode > eligibleNodes = _sortAndFilterHorizontally (direction, focusedChild.rect, nearestScope.traversalDescendants);
814+ if (eligibleNodes.isEmpty) {
815+ break ;
816+ }
785817 if (focusedScrollable != null && ! focusedScrollable.position.atEdge) {
786- final Iterable <FocusNode > filteredEligibleNodes = eligibleNodes! .where ((FocusNode node) => Scrollable .maybeOf (node.context! ) == focusedScrollable);
818+ final Iterable <FocusNode > filteredEligibleNodes = eligibleNodes.where ((FocusNode node) => Scrollable .maybeOf (node.context! ) == focusedScrollable);
787819 if (filteredEligibleNodes.isNotEmpty) {
788820 eligibleNodes = filteredEligibleNodes;
789821 }
790822 }
791- if (eligibleNodes! .isEmpty) {
792- break ;
793- }
794- List <FocusNode > sorted = eligibleNodes.toList ();
795823 if (direction == TraversalDirection .left) {
796- sorted = sorted.reversed. toList ();
824+ eligibleNodes = eligibleNodes. toList ().reversed ;
797825 }
798826 // Find any nodes that intersect the band of the focused child.
799827 final Rect band = Rect .fromLTRB (- double .infinity, focusedChild.rect.top, double .infinity, focusedChild.rect.bottom);
800- final Iterable <FocusNode > inBand = sorted .where ((FocusNode node) => ! node.rect.intersect (band).isEmpty);
828+ final Iterable <FocusNode > inBand = eligibleNodes .where ((FocusNode node) => ! node.rect.intersect (band).isEmpty);
801829 if (inBand.isNotEmpty) {
802- // The inBand list is already sorted by vertical distance, so pick the
803- // closest one.
804- found = inBand.first;
830+ found = _sortByDistancePreferHorizontal (focusedChild.rect.center, inBand).first;
805831 break ;
806832 }
807- // Only out-of-band targets remain , so pick the one that is closest the
833+ // Only out-of-band targets are eligible , so pick the one that is
808834 // to the center line vertically.
809- mergeSort <FocusNode >(sorted, compare: (FocusNode a, FocusNode b) {
810- return (a.rect.center.dy - focusedChild.rect.center.dy).abs ().compareTo ((b.rect.center.dy - focusedChild.rect.center.dy).abs ());
811- });
812- found = sorted.first;
835+ found = _sortByDistancePreferVertical (focusedChild.rect.center, eligibleNodes).first;
813836 break ;
814837 }
815838 if (found != null ) {
@@ -892,8 +915,8 @@ class _ReadingOrderSortData with Diagnosticable {
892915 }
893916 if (common! .isEmpty) {
894917 // If there is no common ancestor, then arbitrarily pick the
895- // directionality of the first group, which is the equivalent of the "first
896- // strongly typed" item in a bidi algorithm.
918+ // directionality of the first group, which is the equivalent of the
919+ // "first strongly typed" item in a bidirectional algorithm.
897920 return list.first.directionality;
898921 }
899922 // Find the closest common ancestor. The memberAncestors list contains the
0 commit comments