Unverified Commit 8ef5e2f0 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add OrderedFocusTraversalPolicy and FocusTraversalGroup to all… (#49235)

This change adds a way to provide explicit focus order for a part of the widget tree.

It adds FocusTraversalPolicyGroup, which in many ways is similar to DefaultFocusTraversal, except that it groups a widget subtree together so that those nodes are traversed as a group. DefaultFocusTraversal doesn't work as one would expect: If there is more than one DefaultFocusTraversal inside of a focus scope, the policy can change depending on which node was asked to move "next", which can cause unexpected behavior. The new grouping mechanism doesn't have that problem. I deprecate DefaultFocusTraversal in this PR.

It also adds OrderedFocusTraversalPolicy, which is a policy that can be supplied to FocusTraversalPolicyGroup to set the policy for a sub-tree. It looks for FocusTraversalOrder inherited widgets, which use a FocusOrder to do the sorting. FocusOrder has two subclasses: NumericalFocusOrder (which sorts based on a double), and LexicalFocusOrder, which sorts based on a String.

As part of doing this, I refactored the way FocusTraversalPolicy is implemented so that it has more default implementation methods, and exposes a new protected member: sortDescendants, which makes it easier for developers to make their own policy subclasses: they only need to implement sortDescendants to get a new ordering behavior, but can also still override any of the default implementation behaviors if they need different behavior.

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