blob: e063d81a0ed662efd48cb3d17a0691c3ccdaa1a7 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'basic.dart';
import 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart';
/// A direction along either the horizontal or vertical axes.
///
/// This is used by the [DirectionalFocusTraversalPolicyMixin] to indicate which
/// direction to traverse in.
enum TraversalDirection {
/// Indicates a direction above the currently focused widget.
up,
/// Indicates a direction to the right of the currently focused widget.
///
/// This direction is unaffected by the [Directionality] of the current
/// context.
right,
/// Indicates a direction below the currently focused widget.
down,
/// Indicates a direction to the left of the currently focused widget.
///
/// This direction is unaffected by the [Directionality] of the current
/// context.
left,
// TODO(gspencer): Add diagonal traversal directions used by TV remotes and
// game controllers when we support them.
}
/// An object used to specify a focus traversal policy used for configuring a
/// [DefaultFocusTraversal] widget.
///
/// The focus traversal policy is what determines which widget is "next",
/// "previous", or in a direction from the currently focused [FocusNode].
///
/// One of the pre-defined subclasses may be used, or define a custom policy to
/// create a unique focus order.
///
/// See also:
///
/// * [FocusNode], for a description of the focus system.
/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
/// [Focus] nodes below it in the widget hierarchy.
/// * [FocusNode], which is affected by the traversal policy.
/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
/// creation order to describe the order of traversal.
/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
/// natural "reading order" for the current [Directionality].
/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
/// focus traversal in a direction.
abstract class FocusTraversalPolicy {
/// Returns the node that should receive focus if there is no current focus
/// in the [FocusScopeNode] that [currentNode] belongs to.
///
/// This is used by [next]/[previous]/[inDirection] to determine which node to
/// focus if they are called, but 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
/// for next/previous, and if direction is non-null, should find the
/// appropriate first node in that direction.
///
/// The [currentNode] argument must not be null.
FocusNode findFirstFocus(FocusNode currentNode);
/// Returns the node in the given [direction] that should receive focus if
/// there is no current focus in the scope to which the [currentNode] belongs.
///
/// This is typically used by [inDirection] to determine which node to focus
/// if it is called, but no node is currently focused.
///
/// All arguments must not be null.
FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction);
/// Clears the data associated with the given [FocusScopeNode] for this object.
///
/// This is used to indicate that the focus policy has changed its mode, and
/// so any cached policy data should be invalidated. For example, changing the
/// direction in which focus is moving, or changing from directional to
/// next/previous navigation modes.
///
/// The default implementation does nothing.
@mustCallSuper
@protected
void invalidateScopeData(FocusScopeNode node) {}
/// This is called whenever the given [node] is reparented into a new scope,
/// so that the policy has a chance to update or invalidate any cached data
/// that it maintains per scope about the node.
///
/// The [oldScope] is the previous scope that this node belonged to, if any.
///
/// The default implementation does nothing.
@mustCallSuper
void changedScope({FocusNode node, FocusScopeNode oldScope}) {}
/// Focuses the next widget in the focus scope that contains the given
/// [currentNode].
///
/// This should determine what the next node to receive focus should be by
/// inspecting the node tree, and then calling [FocusNode.requestFocus] on
/// the node that has been selected.
///
/// Returns true if it successfully found a node and requested focus.
///
/// The [currentNode] argument must not be null.
bool next(FocusNode currentNode);
/// Focuses the previous widget in the focus scope that contains the given
/// [currentNode].
///
/// This should determine what the previous node to receive focus should be by
/// inspecting the node tree, and then calling [FocusNode.requestFocus] on
/// the node that has been selected.
///
/// Returns true if it successfully found a node and requested focus.
///
/// The [currentNode] argument must not be null.
bool previous(FocusNode currentNode);
/// Focuses the next widget in the given [direction] in the focus scope that
/// contains the given [currentNode].
///
/// This should determine what the next node to receive focus in the given
/// [direction] should be by inspecting the node tree, and then calling
/// [FocusNode.requestFocus] on the node that has been selected.
///
/// Returns true if it successfully found a node and requested focus.
///
/// All arguments must not be null.
bool inDirection(FocusNode currentNode, TraversalDirection direction);
}
/// A policy data object for use by the [DirectionalFocusTraversalPolicyMixin]
class _DirectionalPolicyDataEntry {
const _DirectionalPolicyDataEntry({@required this.direction, @required this.node})
: assert(direction != null),
assert(node != null);
final TraversalDirection direction;
final FocusNode node;
}
class _DirectionalPolicyData {
const _DirectionalPolicyData({@required this.history}) : assert(history != null);
/// A queue of entries that describe the path taken to the current node.
final List<_DirectionalPolicyDataEntry> history;
}
/// A mixin class that provides an implementation for finding a node in a
/// particular direction.
///
/// This can be mixed in to other [FocusTraversalPolicy] implementations that
/// only want to implement new next/previous policies.
///
/// Since hysteresis in the navigation order is undesirable, this implementation
/// maintains a stack of previous locations that have been visited on the
/// [policyData] for the affected [FocusScopeNode]. If the previous direction
/// was the opposite of the current direction, then the this policy will request
/// focus on the previously focused node. Change to another direction other than
/// the current one or its opposite will clear the stack.
///
/// For instance, if the focus moves down, down, down, and then up, up, up, it
/// will follow the same path through the widgets in both directions. However,
/// if it moves down, down, down, left, right, and then up, up, up, it may not
/// follow the same path on the way up as it did on the way down.
///
/// See also:
///
/// * [FocusNode], for a description of the focus system.
/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
/// [Focus] nodes below it in the widget hierarchy.
/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
/// creation order to describe the order of traversal.
/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
/// natural "reading order" for the current [Directionality].
mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
final Map<FocusScopeNode, _DirectionalPolicyData> _policyData = <FocusScopeNode, _DirectionalPolicyData>{};
@override
void invalidateScopeData(FocusScopeNode node) {
super.invalidateScopeData(node);
_policyData.remove(node);
}
@override
void changedScope({FocusNode node, FocusScopeNode oldScope}) {
super.changedScope(node: node, oldScope: oldScope);
if (oldScope != null) {
_policyData[oldScope]?.history?.removeWhere((_DirectionalPolicyDataEntry entry) {
return entry.node == node;
});
}
}
@override
FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) {
assert(direction != null);
assert(currentNode != null);
switch (direction) {
case TraversalDirection.up:
// Find the bottom-most node so we can go up from there.
return _sortAndFindInitial(currentNode, vertical: true, first: false);
case TraversalDirection.down:
// Find the top-most node so we can go down from there.
return _sortAndFindInitial(currentNode, vertical: true, first: true);
case TraversalDirection.left:
// Find the right-most node so we can go left from there.
return _sortAndFindInitial(currentNode, vertical: false, first: false);
case TraversalDirection.right:
// Find the left-most node so we can go right from there.
return _sortAndFindInitial(currentNode, vertical: false, first: true);
}
return null;
}
FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) {
final Iterable<FocusNode> nodes = currentNode.nearestScope.traversalDescendants;
final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) {
if (vertical) {
if (first) {
return a.rect.top.compareTo(b.rect.top);
} else {
return b.rect.bottom.compareTo(a.rect.bottom);
}
} else {
if (first) {
return a.rect.left.compareTo(b.rect.left);
} else {
return b.rect.right.compareTo(a.rect.right);
}
}
});
return sorted.first;
}
// Sorts nodes from left to right horizontally, and removes nodes that are
// either to the right of the left side of the target node if we're going
// left, or to the left of the right side of the target node if we're going
// right.
//
// This doesn't need to take into account directionality because it is
// typically intending to actually go left or right, not in a reading
// direction.
Iterable<FocusNode> _sortAndFilterHorizontally(
TraversalDirection direction,
Rect target,
FocusNode nearestScope,
) {
assert(direction == TraversalDirection.left || direction == TraversalDirection.right);
final Iterable<FocusNode> nodes = nearestScope.traversalDescendants;
assert(!nodes.contains(nearestScope));
final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
Iterable<FocusNode> result;
switch (direction) {
case TraversalDirection.left:
result = sorted.where((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left);
break;
case TraversalDirection.right:
result = sorted.where((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right);
break;
case TraversalDirection.up:
case TraversalDirection.down:
break;
}
return result;
}
// Sorts nodes from top to bottom vertically, and removes nodes that are
// either below the top of the target node if we're going up, or above the
// bottom of the target node if we're going down.
Iterable<FocusNode> _sortAndFilterVertically(
TraversalDirection direction,
Rect target,
Iterable<FocusNode> nodes,
) {
final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy));
switch (direction) {
case TraversalDirection.up:
return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
case TraversalDirection.down:
return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom);
case TraversalDirection.left:
case TraversalDirection.right:
break;
}
assert(direction == TraversalDirection.up || direction == TraversalDirection.down);
return null;
}
// Updates the policy data to keep the previously visited node so that we can
// avoid hysteresis when we change directions in navigation.
//
// Returns true if focus was requested on a previous node.
bool _popPolicyDataIfNeeded(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) {
final _DirectionalPolicyData policyData = _policyData[nearestScope];
if (policyData != null && policyData.history.isNotEmpty && policyData.history.first.direction != direction) {
if (policyData.history.last.node.parent == null) {
// If a node has been removed from the tree, then we should stop
// referencing it and reset the scope data so that we don't try and
// request focus on it. This can happen in slivers where the rendered node
// has been unmounted. This has the side effect that hysteresis might not
// be avoided when items that go off screen get unmounted.
invalidateScopeData(nearestScope);
return false;
}
switch (direction) {
case TraversalDirection.down:
case TraversalDirection.up:
switch (policyData.history.first.direction) {
case TraversalDirection.left:
case TraversalDirection.right:
// Reset the policy data if we change directions.
invalidateScopeData(nearestScope);
break;
case TraversalDirection.up:
case TraversalDirection.down:
policyData.history.removeLast().node.requestFocus();
return true;
}
break;
case TraversalDirection.left:
case TraversalDirection.right:
switch (policyData.history.first.direction) {
case TraversalDirection.left:
case TraversalDirection.right:
policyData.history.removeLast().node.requestFocus();
return true;
case TraversalDirection.up:
case TraversalDirection.down:
// Reset the policy data if we change directions.
invalidateScopeData(nearestScope);
break;
}
}
}
if (policyData != null && policyData.history.isEmpty) {
// Reset the policy data if we change directions.
invalidateScopeData(nearestScope);
}
return false;
}
void _pushPolicyData(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) {
final _DirectionalPolicyData policyData = _policyData[nearestScope];
if (policyData != null && policyData is! _DirectionalPolicyData) {
return;
}
final _DirectionalPolicyDataEntry newEntry = _DirectionalPolicyDataEntry(node: focusedChild, direction: direction);
if (policyData != null) {
policyData.history.add(newEntry);
} else {
_policyData[nearestScope] = _DirectionalPolicyData(history: <_DirectionalPolicyDataEntry>[newEntry]);
}
}
/// Focuses the next widget in the given [direction] in the [FocusScope] that
/// contains the [currentNode].
///
/// This determines what the next node to receive focus in the given
/// [direction] will be by inspecting the node tree, and then calling
/// [FocusNode.requestFocus] on it.
///
/// Returns true if it successfully found a node and requested focus.
///
/// Maintains a stack of previous locations that have been visited on the
/// [policyData] for the affected [FocusScopeNode]. If the previous direction
/// was the opposite of the current direction, then the this policy will
/// request focus on the previously focused node. Change to another direction
/// other than the current one or its opposite will clear the stack.
///
/// If this function returns true when called by a subclass, then the subclass
/// should return true and not request focus from any node.
@mustCallSuper
@override
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
final FocusScopeNode nearestScope = currentNode.nearestScope;
final FocusNode focusedChild = nearestScope.focusedChild;
if (focusedChild == null) {
final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction);
(firstFocus ?? currentNode).requestFocus();
return true;
}
if (_popPolicyDataIfNeeded(direction, nearestScope, focusedChild)) {
return true;
}
FocusNode found;
switch (direction) {
case TraversalDirection.down:
case TraversalDirection.up:
final Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically(
direction,
focusedChild.rect,
nearestScope.traversalDescendants,
);
if (eligibleNodes.isEmpty) {
break;
}
List<FocusNode> sorted = eligibleNodes.toList();
if (direction == TraversalDirection.up) {
sorted = sorted.reversed.toList();
}
// Find any nodes that intersect the band of the focused child.
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);
if (inBand.isNotEmpty) {
// The inBand list is already sorted by horizontal distance, so pick the closest one.
found = inBand.first;
break;
}
// Only out-of-band targets remain, so pick the one that is closest the to the center line horizontally.
sorted.sort((FocusNode a, FocusNode b) {
return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs());
});
found = sorted.first;
break;
case TraversalDirection.right:
case TraversalDirection.left:
final Iterable<FocusNode> eligibleNodes = _sortAndFilterHorizontally(direction, focusedChild.rect, nearestScope);
if (eligibleNodes.isEmpty) {
break;
}
List<FocusNode> sorted = eligibleNodes.toList();
if (direction == TraversalDirection.left) {
sorted = sorted.reversed.toList();
}
// Find any nodes that intersect the band of the focused child.
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);
if (inBand.isNotEmpty) {
// The inBand list is already sorted by vertical distance, so pick the closest one.
found = inBand.first;
break;
}
// Only out-of-band targets remain, so pick the one that is closest the to the center line vertically.
sorted.sort((FocusNode a, FocusNode b) {
return (a.rect.center.dy - focusedChild.rect.center.dy).abs().compareTo((b.rect.center.dy - focusedChild.rect.center.dy).abs());
});
found = sorted.first;
break;
}
if (found != null) {
_pushPolicyData(direction, nearestScope, focusedChild);
found.requestFocus();
return true;
}
return false;
}
}
/// A [FocusTraversalPolicy] that traverses the focus order in widget hierarchy
/// order.
///
/// This policy is used when the order desired is the order in which widgets are
/// created in the widget hierarchy.
///
/// See also:
///
/// * [FocusNode], for a description of the focus system.
/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
/// [Focus] nodes below it in the widget hierarchy.
/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
/// natural "reading order" for the current [Directionality].
/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
/// focus traversal in a direction.
class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
/// Creates a const [WidgetOrderFocusTraversalPolicy].
WidgetOrderFocusTraversalPolicy();
@override
FocusNode findFirstFocus(FocusNode currentNode) {
assert(currentNode != null);
final FocusScopeNode scope = currentNode.nearestScope;
// Start with the candidate focus as the focused child of this scope, if
// there is one. Otherwise start with this node itself. Keep going down
// through scopes until an ultimately focusable item is found, a scope
// doesn't have a focusedChild, or a non-scope is encountered.
FocusNode candidate = scope.focusedChild;
if (candidate == null) {
if (scope.traversalChildren.isNotEmpty) {
candidate = scope.traversalChildren.first;
} else {
candidate = currentNode;
}
}
while (candidate is FocusScopeNode && candidate.focusedChild != null) {
final FocusScopeNode candidateScope = candidate;
candidate = candidateScope.focusedChild;
}
return candidate;
}
// Moves the focus to the next or previous node, depending on whether forward
// is true or not.
bool _move(FocusNode currentNode, {@required bool forward}) {
if (currentNode == null) {
return false;
}
final FocusScopeNode nearestScope = currentNode.nearestScope;
invalidateScopeData(nearestScope);
final FocusNode focusedChild = nearestScope.focusedChild;
if (focusedChild == null) {
final FocusNode firstFocus = findFirstFocus(currentNode);
if (firstFocus != null) {
firstFocus.requestFocus();
return true;
}
}
FocusNode previousNode;
FocusNode firstNode;
FocusNode lastNode;
bool visit(FocusNode node) {
for (FocusNode visited in node.traversalChildren) {
firstNode ??= visited;
if (!visit(visited)) {
return false;
}
if (forward) {
if (previousNode == focusedChild) {
visited.requestFocus();
return false; // short circuit the traversal.
}
} else {
if (previousNode != null && visited == focusedChild) {
previousNode.requestFocus();
return false; // short circuit the traversal.
}
}
previousNode = visited;
lastNode = visited;
}
return true; // continue traversal
}
if (visit(nearestScope)) {
if (forward) {
if (firstNode != null) {
firstNode.requestFocus();
return true;
}
} else {
if (lastNode != null) {
lastNode.requestFocus();
return true;
}
}
return false;
}
return true;
}
@override
bool next(FocusNode currentNode) => _move(currentNode, forward: true);
@override
bool previous(FocusNode currentNode) => _move(currentNode, forward: false);
}
class _SortData {
_SortData(this.node) : rect = node.rect;
final Rect rect;
final FocusNode node;
}
/// Traverses the focus order in "reading order".
///
/// By default, reading order traversal goes in the reading direction, and then
/// down, using this algorithm:
///
/// 1. Find the node rectangle that has the highest `top` on the screen.
/// 2. Find any other nodes that intersect the infinite horizontal band defined
/// by the highest rectangle's top and bottom edges.
/// 3. Pick the closest to the beginning of the reading order from among the
/// nodes discovered above.
///
/// It uses the ambient directionality in the context for the enclosing scope to
/// determine which direction is "reading order".
///
/// See also:
///
/// * [FocusNode], for a description of the focus system.
/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the
/// [Focus] nodes below it in the widget hierarchy.
/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
/// creation order to describe the order of traversal.
/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
/// focus traversal in a direction.
class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
@override
FocusNode findFirstFocus(FocusNode currentNode) {
assert(currentNode != null);
final FocusScopeNode scope = currentNode.nearestScope;
FocusNode candidate = scope.focusedChild;
if (candidate == null && scope.traversalChildren.isNotEmpty) {
candidate = _sortByGeometry(scope).first;
}
// If we still didn't find any candidate, use the current node as a
// fallback.
candidate ??= currentNode;
candidate ??= WidgetsBinding.instance.focusManager.rootScope;
return candidate;
}
// Sorts the list of nodes based on their geometry into the desired reading
// order based on the directionality of the context for each node.
Iterable<FocusNode> _sortByGeometry(FocusScopeNode scope) {
final Iterable<FocusNode> nodes = scope.traversalDescendants;
if (nodes.length <= 1) {
return nodes;
}
Iterable<_SortData> inBand(_SortData current, Iterable<_SortData> candidates) {
final Rect wide = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom);
return candidates.where((_SortData item) {
return !item.rect.intersect(wide).isEmpty;
});
}
final TextDirection textDirection = scope.context == null ? TextDirection.ltr : Directionality.of(scope.context);
_SortData pickFirst(List<_SortData> candidates) {
int compareBeginningSide(_SortData a, _SortData b) {
return textDirection == TextDirection.ltr ? a.rect.left.compareTo(b.rect.left) : -a.rect.right.compareTo(b.rect.right);
}
int compareTopSide(_SortData a, _SortData b) {
return a.rect.top.compareTo(b.rect.top);
}
// Get the topmost
candidates.sort(compareTopSide);
final _SortData topmost = candidates.first;
// If there are any others in the band of the topmost, then pick the
// leftmost one.
final List<_SortData> inBandOfTop = inBand(topmost, candidates).toList();
inBandOfTop.sort(compareBeginningSide);
if (inBandOfTop.isNotEmpty) {
return inBandOfTop.first;
}
return topmost;
}
final List<_SortData> data = <_SortData>[
for (FocusNode node in nodes) _SortData(node),
];
// Pick the initial widget as the one that is leftmost in the band of the
// topmost, or the topmost, if there are no others in its band.
final List<_SortData> sortedList = <_SortData>[];
final List<_SortData> unplaced = data.toList();
_SortData current = pickFirst(unplaced);
sortedList.add(current);
unplaced.remove(current);
while (unplaced.isNotEmpty) {
final _SortData next = pickFirst(unplaced);
current = next;
sortedList.add(current);
unplaced.remove(current);
}
return sortedList.map((_SortData item) => item.node);
}
// Moves the focus forward or backward in reading order, depending on the
// value of the forward argument.
bool _move(FocusNode currentNode, {@required bool forward}) {
final FocusScopeNode nearestScope = currentNode.nearestScope;
invalidateScopeData(nearestScope);
final FocusNode focusedChild = nearestScope.focusedChild;
if (focusedChild == null) {
final FocusNode firstFocus = findFirstFocus(currentNode);
if (firstFocus != null) {
firstFocus.requestFocus();
return true;
}
}
final List<FocusNode> sortedNodes = _sortByGeometry(nearestScope).toList();
if (forward && focusedChild == sortedNodes.last) {
sortedNodes.first.requestFocus();
return true;
}
if (!forward && focusedChild == sortedNodes.first) {
sortedNodes.last.requestFocus();
return true;
}
final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
FocusNode previousNode;
for (FocusNode node in maybeFlipped) {
if (previousNode == focusedChild) {
node.requestFocus();
return true;
}
previousNode = node;
}
return false;
}
@override
bool next(FocusNode currentNode) => _move(currentNode, forward: true);
@override
bool previous(FocusNode currentNode) => _move(currentNode, forward: false);
}
/// A widget that describes the inherited focus policy for focus traversal.
///
/// By default, traverses in widget order using
/// [ReadingOrderFocusTraversalPolicy].
///
/// See also:
///
/// * [FocusNode], for a description of the focus system.
/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget
/// creation order to describe the order of traversal.
/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
/// natural "reading order" for the current [Directionality].
/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
/// focus traversal in a direction.
class DefaultFocusTraversal extends InheritedWidget {
/// Creates a [DefaultFocusTraversal] object.
///
/// The [child] argument must not be null.
const DefaultFocusTraversal({
Key key,
this.policy,
@required Widget child,
}) : super(key: key, child: child);
/// The policy used to move the focus from one focus node to another.
///
/// If not specified, traverses in reading order using
/// [ReadingOrderTraversalPolicy].
///
/// See also:
///
/// * [FocusTraversalPolicy] for the API used to impose traversal order
/// policy.
/// * [WidgetOrderFocusTraversalPolicy] for a traversal policy that traverses
/// nodes in the order they are added to the widget tree.
/// * [ReadingOrderTraversalPolicy] for a traversal policy that traverses
/// nodes in the reading order defined in the widget tree, and then top to
/// bottom.
final FocusTraversalPolicy policy;
/// Returns the [FocusTraversalPolicy] that most tightly encloses the given
/// [BuildContext].
///
/// The [context] argument must not be null.
static FocusTraversalPolicy of(BuildContext context, { bool nullOk = false }) {
assert(context != null);
final DefaultFocusTraversal inherited = context.inheritFromWidgetOfExactType(DefaultFocusTraversal);
assert(() {
if (nullOk) {
return true;
}
if (inherited == null) {
throw FlutterError('Unable to find a DefaultFocusTraversal widget in the context.\n'
'DefaultFocusTraversal.of() was called with a context that does not contain a '
'DefaultFocusTraversal.\n'
'No DefaultFocusTraversal ancestor could be found starting from the context that was '
'passed to DefaultFocusTraversal.of(). This can happen because there is not a '
'WidgetsApp or MaterialApp widget (those widgets introduce a DefaultFocusTraversal), '
'or it can happen if the context comes from a widget above those widgets.\n'
'The context used was:\n'
' $context');
}
return true;
}());
return inherited?.policy ?? ReadingOrderTraversalPolicy();
}
@override
bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
}