diff --git a/dev/manual_tests/lib/actions.dart b/dev/manual_tests/lib/actions.dart index 02839ffee51..1ee61508a80 100644 --- a/dev/manual_tests/lib/actions.dart +++ b/dev/manual_tests/lib/actions.dart @@ -435,7 +435,7 @@ class _FocusDemoState extends State { kUndoActionKey: () => kUndoAction, kRedoActionKey: () => kRedoAction, }, - child: FocusTraversalGroup( + child: DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), child: Shortcuts( shortcuts: { diff --git a/dev/manual_tests/lib/focus.dart b/dev/manual_tests/lib/focus.dart index 00e5c2db9c3..361988e7ff9 100644 --- a/dev/manual_tests/lib/focus.dart +++ b/dev/manual_tests/lib/focus.dart @@ -142,7 +142,7 @@ class _FocusDemoState extends State { Widget build(BuildContext context) { final TextTheme textTheme = Theme.of(context).textTheme; - return FocusTraversalGroup( + return DefaultFocusTraversal( 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 d4bdee62f4f..11e645c0673 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -1395,7 +1395,7 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { debugLabel: '', child: Actions( actions: widget.actions ?? WidgetsApp.defaultActions, - child: FocusTraversalGroup( + child: DefaultFocusTraversal( 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 6c94532cb9a..b11b1306628 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -230,12 +230,12 @@ class FocusAttachment { /// particular direction, is determined by the [FocusTraversalPolicy] in force. /// /// The ambient policy is determined by looking up the widget hierarchy for a -/// [FocusTraversalGroup] widget, and obtaining the focus traversal policy +/// [DefaultFocusTraversal] 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 [WidgetOrderTraversalPolicy], +/// Predefined policies include [WidgetOrderFocusTraversalPolicy], /// [ReadingOrderTraversalPolicy], and [DirectionalFocusTraversalPolicyMixin], /// but custom policies can be built based upon these policies. /// @@ -361,8 +361,8 @@ class FocusAttachment { /// events to focused nodes. /// * [FocusTraversalPolicy], a class used to determine how to move the focus /// to other nodes. -/// * [FocusTraversalGroup], a widget used to group together and configure the -/// focus traversal policy for a widget subtree. +/// * [DefaultFocusTraversal], a widget used to configure the default focus +/// traversal policy for a widget subtree. class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { /// Creates a focus node. /// @@ -426,8 +426,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { /// /// See also: /// - /// * [FocusTraversalGroup], a widget used to group together and configure the - /// focus traversal policy for a widget subtree. + /// * [DefaultFocusTraversal], a widget that sets the traversal policy for + /// its descendants. /// * [FocusTraversalPolicy], a class that can be extended to describe a /// traversal policy. bool get canRequestFocus { @@ -518,8 +518,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { return _descendants; } - /// Returns all descendants which do not have the [skipTraversal] and do have - /// the [canRequestFocus] flag set. + /// Returns all descendants which do not have the [skipTraversal] flag set. Iterable get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus); /// An [Iterable] over the ancestors of this node. @@ -777,7 +776,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { _manager?.primaryFocus?._setAsFocusedChild(); } if (oldScope != null && child.context != null && child.enclosingScope != oldScope) { - FocusTraversalGroup.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope); + DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope); } if (child._requestFocusWhenReparented) { child._doRequestFocus(); @@ -916,19 +915,19 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { /// [FocusTraversalPolicy.next] method. /// /// Returns true if it successfully found a node and requested focus. - bool nextFocus() => FocusTraversalGroup.of(context).next(this); + bool nextFocus() => DefaultFocusTraversal.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() => FocusTraversalGroup.of(context).previous(this); + bool previousFocus() => DefaultFocusTraversal.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) => FocusTraversalGroup.of(context).inDirection(this, direction); + bool focusInDirection(TraversalDirection direction) => DefaultFocusTraversal.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 1a4108586ff..cb9ce512a98 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -462,10 +462,8 @@ class _FocusState extends State { return _FocusMarker( node: focusNode, child: Semantics( - // If these values are false, then just don't set them, so they don't - // eclipse values set by children. - focusable: _canRequestFocus ? true : null, - focused: _hasPrimaryFocus ? true : null, + focusable: _canRequestFocus, + focused: _hasPrimaryFocus, child: widget.child, ), ); diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart index 870908ec0bd..c88ed395531 100644 --- a/packages/flutter/lib/src/widgets/focus_traversal.dart +++ b/packages/flutter/lib/src/widgets/focus_traversal.dart @@ -4,7 +4,6 @@ import 'dart:ui'; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -12,59 +11,14 @@ import 'actions.dart'; 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 members, - }) : groupNode = marker?.focusNode, - policy = marker?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(), - members = members ?? []; - - final FocusNode groupNode; - final FocusTraversalPolicy policy; - final List members; -} - /// A direction along either the horizontal or vertical axes. /// -/// This is used by the [DirectionalFocusTraversalPolicyMixin], and -/// [Focus.focusInDirection] to indicate which direction to look in for the next -/// focus. +/// This is used by the [DirectionalFocusTraversalPolicyMixin] to indicate which +/// direction to traverse in. enum TraversalDirection { /// Indicates a direction above the currently focused widget. up, @@ -89,7 +43,7 @@ enum TraversalDirection { } /// An object used to specify a focus traversal policy used for configuring a -/// [FocusTraversalGroup] widget. +/// [DefaultFocusTraversal] widget. /// /// The focus traversal policy is what determines which widget is "next", /// "previous", or in a direction from the currently focused [FocusNode]. @@ -97,61 +51,40 @@ enum TraversalDirection { /// 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. -/// * [FocusTraversalGroup], a widget that groups together and imposes a -/// traversal policy on the [Focus] nodes below it in the widget hierarchy. +/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the +/// [Focus] nodes below it in the widget hierarchy. /// * [FocusNode], which is affected by the traversal policy. -/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget +/// * [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]. -/// * [OrderedTraversalPolicy], a policy that describes the order -/// explicitly using [FocusTraversalOrder] widgets. /// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements /// focus traversal in a direction. -@immutable -abstract class FocusTraversalPolicy extends Diagnosticable { - /// A const constructor so subclasses can be const. - const FocusTraversalPolicy(); - +abstract class FocusTraversalPolicy { /// Returns the node that should receive focus if there is no current focus - /// in the nearest [FocusScopeNode] that `currentNode` belongs to. + /// in the [FocusScopeNode] that [currentNode] belongs to. /// /// This is used by [next]/[previous]/[inDirection] to determine which node to - /// focus if they are called when no node is currently focused. + /// focus if they are called, but no node is currently focused. /// - /// The `currentNode` argument must not be null. + /// It is also used by the [FocusManager] to know which node to focus + /// initially if no nodes are focused. /// - /// 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 sorted = _sortAllDescendants(scope); - candidate = sorted.isNotEmpty ? sorted.first : 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); - // 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. + /// 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. /// /// This is typically used by [inDirection] to determine which node to focus - /// if it is called when no node is currently focused. + /// if it is called, but no node is currently focused. /// /// All arguments must not be null. FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction); @@ -188,7 +121,7 @@ abstract class FocusTraversalPolicy extends Diagnosticable { /// Returns true if it successfully found a node and requested focus. /// /// The [currentNode] argument must not be null. - bool next(FocusNode currentNode) => _moveFocus(currentNode, forward: true); + bool next(FocusNode currentNode); /// Focuses the previous widget in the focus scope that contains the given /// [currentNode]. @@ -200,7 +133,7 @@ abstract class FocusTraversalPolicy extends Diagnosticable { /// Returns true if it successfully found a node and requested focus. /// /// The [currentNode] argument must not be null. - bool previous(FocusNode currentNode) => _moveFocus(currentNode, forward: false); + bool previous(FocusNode currentNode); /// Focuses the next widget in the given [direction] in the focus scope that /// contains the given [currentNode]. @@ -213,165 +146,15 @@ abstract class FocusTraversalPolicy extends Diagnosticable { /// /// 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 sortDescendants(Iterable 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 _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 groups = {}; - 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: [], 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: [], defaultPolicy: defaultPolicy); - assert(!groups[groupNode].members.contains(node)); - groups[groupNode].members.add(node); - } - } - - // Sort the member lists using the individual policy sorts. - final Set groupKeys = groups.keys.toSet(); - for (final FocusNode key in groups.keys) { - final List 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 sortedDescendants = []; - 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 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 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; - } } -// A policy data object for use by the DirectionalFocusTraversalPolicyMixin so -// it can keep track of the traversal history. +@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] class _DirectionalPolicyDataEntry { const _DirectionalPolicyDataEntry({@required this.direction, @required this.node}) : assert(direction != null), @@ -404,20 +187,17 @@ class _DirectionalPolicyData { /// 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, since changing -/// the axis of motion resets the history. +/// follow the same path on the way up as it did on the way down. /// /// See also: /// /// * [FocusNode], for a description of the focus system. -/// * [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 +/// * [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 /// 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 _policyData = {}; @@ -458,10 +238,10 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { return null; } - FocusNode _sortAndFindInitial(FocusNode currentNode, {bool vertical, bool first}) { + FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) { final Iterable nodes = currentNode.nearestScope.traversalDescendants; final List sorted = nodes.toList(); - mergeSort(sorted, compare: (FocusNode a, FocusNode b) { + sorted.sort((FocusNode a, FocusNode b) { if (vertical) { if (first) { return a.rect.top.compareTo(b.rect.top); @@ -477,9 +257,8 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { } }); - if (sorted.isNotEmpty) { + if (sorted.isNotEmpty) return sorted.first; - } return null; } @@ -501,7 +280,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { final Iterable nodes = nearestScope.traversalDescendants; assert(!nodes.contains(nearestScope)); final List sorted = nodes.toList(); - mergeSort(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx)); + sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx)); Iterable result; switch (direction) { case TraversalDirection.left: @@ -526,7 +305,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { Iterable nodes, ) { final List sorted = nodes.toList(); - mergeSort(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy)); + sorted.sort((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); @@ -550,9 +329,9 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { 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; } @@ -565,14 +344,14 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { 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( @@ -707,14 +486,12 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity); final Iterable 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. - mergeSort(sorted, compare: (FocusNode a, FocusNode b) { + // 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) { return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs()); }); found = sorted.first; @@ -739,14 +516,12 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom); final Iterable 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. - mergeSort(sorted, compare: (FocusNode a, FocusNode b) { + // 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) { return (a.rect.center.dy - focusedChild.rect.center.dy).abs().compareTo((b.rect.center.dy - focusedChild.rect.center.dy).abs()); }); found = sorted.first; @@ -764,10 +539,10 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { break; case TraversalDirection.down: case TraversalDirection.right: - _focusAndEnsureVisible( - found, - alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, - ); + _focusAndEnsureVisible( + found, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); break; } return true; @@ -785,159 +560,115 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { /// See also: /// /// * [FocusNode], for a description of the focus system. -/// * [FocusTraversalGroup], a widget that groups together and imposes a -/// traversal policy on the [Focus] nodes below it in the widget hierarchy. +/// * [DefaultFocusTraversal], a widget that 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. -/// * [OrderedTraversalPolicy], a policy that describes the order -/// explicitly using [FocusTraversalOrder] widgets. -class WidgetOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { +class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { + /// Creates a const [WidgetOrderFocusTraversalPolicy]. + WidgetOrderFocusTraversalPolicy(); + @override - Iterable sortDescendants(Iterable descendants) => descendants; + 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); } -// 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); +class _SortData { + _SortData(this.node) : rect = node.rect; - 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()?.widget as Directionality)?.textDirection; - } - - /// Finds the common Directional ancestor of an entire list of groups. - static TextDirection commonDirectionalityOf(List<_ReadingOrderSortData> list) { - final Iterable> allAncestors = list.map>((_ReadingOrderSortData member) => member.directionalAncestors.toSet()); - Set common; - for (final Set 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 get directionalAncestors { - List getDirectionalityAncestors(BuildContext context) { - final List result = []; - InheritedElement directionalityElement = context.getElementForInheritedWidgetOfExactType(); - while (directionalityElement != null) { - result.add(directionalityElement.widget as Directionality); - directionalityElement = _getAncestor(directionalityElement)?.getElementForInheritedWidgetOfExactType(); - } - return result; - } - - _directionalAncestors ??= getDirectionalityAncestors(node.context); - return _directionalAncestors; - } - - List _directionalAncestors; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('directionality', directionality)); - properties.add(StringProperty('name', node.debugLabel, defaultValue: null)); - properties.add(DiagnosticsProperty('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((_ReadingOrderSortData data) => data.rect)) { - _rect ??= rect; - _rect = _rect.expandToInclude(rect); - } - } - return _rect; - } - - List get memberAncestors { - if (_memberAncestors == null) { - _memberAncestors = []; - for (final _ReadingOrderSortData member in members) { - _memberAncestors.addAll(member.directionalAncestors); - } - } - return _memberAncestors; - } - - List _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('directionality', directionality)); - properties.add(DiagnosticsProperty('rect', rect)); - properties.add(IterableProperty('members', members.map((_ReadingOrderSortData member) { - return '"${member.node.debugLabel}"(${member.rect})'; - }))); - } } /// Traverses the focus order in "reading order". @@ -951,622 +682,160 @@ class _ReadingOrderDirectionalGroupData extends Diagnosticable { /// 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. -/// * [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 +/// * [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 /// 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 { - // 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(); + @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; } - 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; + // If we still didn't find any candidate, use the current node as a + // fallback. + candidate ??= currentNode; + candidate ??= FocusManager.instance.rootScope; + return candidate; } // Sorts the list of nodes based on their geometry into the desired reading // order based on the directionality of the context for each node. - @override - Iterable sortDescendants(Iterable descendants) { - assert(descendants != null); - if (descendants.length <= 1) { - return descendants; + Iterable _sortByGeometry(FocusScopeNode scope) { + final Iterable nodes = scope.traversalDescendants; + if (nodes.length <= 1) { + return nodes; } - final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[ - for (final FocusNode node in descendants) _ReadingOrderSortData(node), + 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 sortedList = []; - 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); + // 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); 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 _ReadingOrderSortData next = _pickNext(unplaced); + final _SortData next = pickFirst(unplaced); current = next; - sortedList.add(current.node); + sortedList.add(current); unplaced.remove(current); } - return sortedList; - } -} - -/// Base class for all sort orders for [OrderedTraversalPolicy] traversal. -/// -/// {@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 { - /// 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); + return sortedList.map((_SortData item) => item.node); } - /// 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: [ -/// Row( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: const [ -/// DemoButton(name: 'Six', order: 6), -/// ], -/// ), -/// Row( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: const [ -/// DemoButton(name: 'Five', order: 5), -/// DemoButton(name: 'Four', order: 4), -/// ], -/// ), -/// Row( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: const [ -/// 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 sortDescendants(Iterable descendants) { - final FocusTraversalPolicy secondaryPolicy = secondary ?? ReadingOrderTraversalPolicy(); - final Iterable sortedDescendants = secondaryPolicy.sortDescendants(descendants); - final List unordered = []; - 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); + // 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; } } - 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((_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()?.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'); + final List sortedNodes = _sortByGeometry(nearestScope).toList(); + if (forward && focusedChild == sortedNodes.last) { + _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd); + return true; } - return order; + if (!forward && focusedChild == sortedNodes.first) { + _focusAndEnsureVisible(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); + return true; + } + + final Iterable 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; } - // 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; + bool next(FocusNode currentNode) => _move(currentNode, forward: true); @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('order', order)); - } + bool previous(FocusNode currentNode) => _move(currentNode, forward: false); } -/// A widget that describes the inherited focus policy for focus traversal for -/// its descendants, grouping them into a separate traversal group. +/// A widget that describes the inherited focus policy for focus traversal. /// -/// 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]. +/// By default, traverses in widget order using +/// [ReadingOrderFocusTraversalPolicy]. /// /// See also: /// /// * [FocusNode], for a description of the focus system. -/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget +/// * [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]. /// * [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('policy', policy)); - } -} - -class _FocusTraversalGroupState extends State { - // 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, - 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. /// @@ -1577,10 +846,7 @@ class DefaultFocusTraversal extends InheritedWidget { @required Widget child, }) : super(key: key, child: child); - /// 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._ + /// The policy used to move the focus from one focus node to another. /// /// If not specified, traverses in reading order using /// [ReadingOrderTraversalPolicy]. @@ -1589,7 +855,7 @@ class DefaultFocusTraversal extends InheritedWidget { /// /// * [FocusTraversalPolicy] for the API used to impose traversal order /// policy. - /// * [WidgetOrderTraversalPolicy] for a traversal policy that traverses + /// * [WidgetOrderFocusTraversalPolicy] 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 @@ -1599,37 +865,24 @@ class DefaultFocusTraversal extends InheritedWidget { /// 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}) { - final DefaultFocusTraversal inherited = context.getElementForInheritedWidgetOfExactType()?.widget as DefaultFocusTraversal; + static FocusTraversalPolicy of(BuildContext context, { bool nullOk = false }) { + assert(context != null); + final DefaultFocusTraversal inherited = context.dependOnInheritedWidgetOfExactType(); 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; }()); @@ -1637,7 +890,7 @@ class DefaultFocusTraversal extends InheritedWidget { } @override - bool updateShouldNotify(DefaultFocusTraversal oldWidget) => false; + bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy; } // A base class for all of the default actions that request focus for a node. @@ -1735,8 +988,7 @@ class DirectionalFocusIntent extends Intent { /// 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 78ed8e18ad8..892927f062f 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2191,8 +2191,6 @@ abstract class BuildContext { /// 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 3b600925d33..dc4c85d3d1d 100644 --- a/packages/flutter/test/material/checkbox_test.dart +++ b/packages/flutter/test/material/checkbox_test.dart @@ -68,7 +68,7 @@ void main() { ), )); - expect(tester.getSemantics(find.byType(Focus)), matchesSemantics( + expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics( hasCheckedState: true, hasEnabledState: true, isEnabled: true, @@ -83,7 +83,7 @@ void main() { ), )); - expect(tester.getSemantics(find.byType(Focus)), matchesSemantics( + expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics( hasCheckedState: true, hasEnabledState: true, isChecked: true, @@ -99,7 +99,7 @@ void main() { ), )); - expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( + expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics( hasCheckedState: true, hasEnabledState: true, )); @@ -111,7 +111,7 @@ void main() { ), )); - expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( + expect(tester.getSemantics(find.byType(Focus).last), 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 fc0fa225804..ce29aa4c809 100644 --- a/packages/flutter/test/material/debug_test.dart +++ b/packages/flutter/test/material/debug_test.dart @@ -116,7 +116,7 @@ void main() { ' The ancestors of this widget were:\n' ' Semantics\n' ' Builder\n' - ' RepaintBoundary-[GlobalKey#00000]\n' + ' RepaintBoundary-[GlobalKey#2d465]\n' ' IgnorePointer\n' ' AnimatedBuilder\n' ' FadeTransition\n' @@ -131,19 +131,19 @@ void main() { ' PageStorage\n' ' Offstage\n' ' _ModalScopeStatus\n' - ' _ModalScope-[LabeledGlobalKey<_ModalScopeState>#00000]\n' + ' _ModalScope-[LabeledGlobalKey<_ModalScopeState>#969b7]\n' ' _EffectiveTickerMode\n' ' TickerMode\n' - ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n' + ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n' ' _Theatre\n' - ' Overlay-[LabeledGlobalKey#00000]\n' + ' Overlay-[LabeledGlobalKey#31a52]\n' ' _FocusMarker\n' ' Semantics\n' ' FocusScope\n' ' AbsorbPointer\n' ' _PointerListener\n' ' Listener\n' - ' Navigator-[GlobalObjectKey _WidgetsAppState#00000]\n' + ' Navigator-[GlobalObjectKey _WidgetsAppState#10579]\n' ' IconTheme\n' ' IconTheme\n' ' _InheritedCupertinoTheme\n' @@ -158,23 +158,19 @@ void main() { ' CheckedModeBanner\n' ' Title\n' ' Directionality\n' - ' _LocalizationsScope-[GlobalKey#00000]\n' + ' _LocalizationsScope-[GlobalKey#a51e3]\n' ' Semantics\n' ' Localizations\n' ' MediaQuery\n' ' _MediaQueryFromWindow\n' - ' Semantics\n' - ' _FocusMarker\n' - ' Focus\n' - ' _FocusTraversalGroupMarker\n' - ' FocusTraversalGroup\n' + ' DefaultFocusTraversal\n' ' Actions\n' ' _ShortcutsMarker\n' ' Semantics\n' ' _FocusMarker\n' ' Focus\n' ' Shortcuts\n' - ' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n' + ' WidgetsApp-[GlobalObjectKey _MaterialAppState#38e79]\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 39b011ad5ed..094c9501a22 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -570,7 +570,7 @@ void main() { } Widget wrap({ Widget child }) { - return FocusTraversalGroup( + return DefaultFocusTraversal( 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 101b87c1769..d97280ce737 100644 --- a/packages/flutter/test/widgets/focus_scope_test.dart +++ b/packages/flutter/test/widgets/focus_scope_test.dart @@ -598,7 +598,7 @@ void main() { // This checks both FocusScopes that have their own nodes, as well as those // that use external nodes. await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( child: Column( children: [ FocusScope( @@ -661,7 +661,7 @@ void main() { final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( child: Column( children: [ FocusScope( @@ -711,7 +711,7 @@ void main() { expect(find.text('b'), findsOneWidget); await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( child: Column( children: [ FocusScope( @@ -746,7 +746,7 @@ void main() { final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( child: Column( children: [ FocusScope( @@ -794,7 +794,7 @@ void main() { expect(find.text('b'), findsOneWidget); await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( child: Column( children: [ FocusScope( diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart index b84331d8871..4f286d8eebf 100644 --- a/packages/flutter/test/widgets/focus_traversal_test.dart +++ b/packages/flutter/test/widgets/focus_traversal_test.dart @@ -12,15 +12,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; void main() { - group(WidgetOrderTraversalPolicy, () { + group(WidgetOrderFocusTraversalPolicy, () { 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(FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + await tester.pumpWidget(DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( key: key1, child: Column( @@ -64,8 +64,8 @@ void main() { bool focus3; bool focus5; await tester.pumpWidget( - FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( debugLabel: 'key1', key: key1, @@ -177,8 +177,8 @@ void main() { final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key6 = GlobalKey(debugLabel: '6'); await tester.pumpWidget( - FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( key: key1, child: Column( @@ -250,8 +250,8 @@ void main() { final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); await tester.pumpWidget( MaterialApp( - home: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + home: DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: Center( child: Builder(builder: (BuildContext context) { return MaterialButton( @@ -317,7 +317,7 @@ void main() { final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key5 = GlobalKey(debugLabel: '5'); - await tester.pumpWidget(FocusTraversalGroup( + await tester.pumpWidget(DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), child: FocusScope( key: key1, @@ -364,7 +364,7 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: FocusTraversalGroup( + child: DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), child: FocusScope( debugLabel: 'key1', @@ -473,7 +473,7 @@ void main() { final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key6 = GlobalKey(debugLabel: '6'); await tester.pumpWidget( - FocusTraversalGroup( + DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), child: FocusScope( key: key1, @@ -538,579 +538,6 @@ void main() { 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 nodes = List.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: [ - Directionality( - textDirection: TextDirection.ltr, - child: Row(children: [ - 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: [ - 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: [ - 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 order = []; - for (int i = 0; i < nodeCount; ++i) { - nodes.first.nextFocus(); - await tester.pump(); - order.add(nodes.indexOf(primaryFocus)); - } - expect(order, orderedEquals([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([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 nodes = List.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.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 order = []; - for (int i = 0; i < nodeCount; ++i) { - nodes.first.nextFocus(); - await tester.pump(); - order.add(nodes.indexOf(primaryFocus)); - } - expect(order, orderedEquals([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.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([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.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([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: [ - 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 nodes = List.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.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 nodes = List.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.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 keys = List.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1)); - final List nodes = List.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.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 nodes = List.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: [ - FocusTraversalOrder( - order: const NumericFocusOrder(0), - child: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), - child: Row(children: [ - 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: [ - 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: [ - 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 expectedOrder = [9, 8, 7, 4, 5, 6, 0, 1, 2, 3]; - final List order = []; - 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( - MaterialPageRoute( - 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, () { @@ -1126,8 +553,8 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + child: DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( debugLabel: 'Scope', child: Column( @@ -1279,8 +706,8 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + child: DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( debugLabel: 'Scope', child: Column( @@ -1400,8 +827,8 @@ void main() { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: FocusTraversalGroup( - policy: WidgetOrderTraversalPolicy(), + child: DefaultFocusTraversal( + policy: WidgetOrderFocusTraversalPolicy(), child: FocusScope( debugLabel: 'scope', child: Column( @@ -1444,7 +871,7 @@ void main() { await tester.pump(); - final FocusTraversalPolicy policy = FocusTraversalGroup.of(upperLeftKey.currentContext); + final FocusTraversalPolicy policy = DefaultFocusTraversal.of(upperLeftKey.currentContext); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.up), equals(lowerLeftNode)); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.down), equals(upperLeftNode)); @@ -1458,7 +885,7 @@ void main() { final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); - await tester.pumpWidget(FocusTraversalGroup( + await tester.pumpWidget(DefaultFocusTraversal( policy: policy, child: FocusScope( debugLabel: 'Scope', @@ -1482,7 +909,7 @@ void main() { expect(focusBottom.hasFocus, isTrue); // Remove center focus node. - await tester.pumpWidget(FocusTraversalGroup( + await tester.pumpWidget(DefaultFocusTraversal( policy: policy, child: FocusScope( debugLabel: 'Scope',