Understanding Flutter's keyboard focus system
- Overview
- Glossary
- FocusNode and FocusScopeNode
- Focus widget
- FocusScope widget
- FocusableActionDetector widget
- Controlling focus traversal
- The focus manager
This article explains how to control where keyboard input is directed. If you are implementing an application that uses a physical keyboard, such as most desktop and web applications, this page is for you. If your app won't be used with a physical keyboard, you can skip this.
Overview
#Flutter comes with a focus system that directs the keyboard input to a particular part of an application. In order to do this, users "focus" the input onto that part of an application by tapping or clicking the desired UI element. Once that happens, text entered with the keyboard flows to that part of the application until the focus moves to another part of the application. Focus can also be moved by pressing a particular keyboard shortcut, which is typically bound to Tab, so it is sometimes called "tab traversal".
This page explores the APIs used to perform these operations on a Flutter
application, and how the focus system works. We have noticed that there is some
confusion among developers about how to define and use FocusNode
objects.
If that describes your experience, skip ahead to the best practices for
creating FocusNode
objects.
Focus use cases
#Some examples of situations where you might need to know how to use the focus system:
- Receiving/handling key events
- Implementing a custom component that needs to be focusable
- Receiving notifications when the focus changes
- Changing or defining the "tab order" of focus traversal in an application
- Defining groups of controls that should be traversed together
- Preventing some controls in an application from being focusable
Glossary
#Below are terms, as Flutter uses them, for elements of the focus system. The various classes that implement some of these concepts are introduced below.
- Focus tree - A tree of focus nodes that typically sparsely mirrors the widget tree, representing all the widgets that can receive focus.
- Focus node - A single node in a focus tree. This node can receive the focus, and is said to "have focus" when it is part of the focus chain. It participates in handling key events only when it has focus.
- Primary focus - The farthest focus node from the root of the focus tree that has focus. This is the focus node where key events start propagating to the primary focus node and its ancestors.
- Focus chain - An ordered list of focus nodes that starts at the primary focus node and follows the branches of the focus tree to the root of the focus tree.
- Focus scope - A special focus node whose job is to contain a group of other focus nodes, and allow only those nodes to receive focus. It contains information about which nodes were previously focused in its subtree.
- Focus traversal - The process of moving from one focusable node to another in a predictable order. This is typically seen in applications when the user presses Tab to move to the next focusable control or field.
FocusNode and FocusScopeNode
#The FocusNode
and FocusScopeNode
objects implement the
mechanics of the focus system. They are long-lived objects (longer than widgets,
similar to render objects) that hold the focus state and attributes so that they
are persistent between builds of the widget tree. Together, they form
the focus tree data structure.
They were originally intended to be developer-facing objects used to control
some aspects of the focus system, but over time they have evolved to mostly
implement details of the focus system. In order to prevent breaking existing
applications, they still contain public interfaces for their attributes. But, in
general, the thing for which they are most useful is to act as a relatively
opaque handle, passed to a descendant widget in order to call requestFocus()
on an ancestor widget, which requests that a descendant widget obtain focus.
Setting of the other attributes is best managed by a Focus
or
FocusScope
widget, unless you are not using them, or implementing your own
version of them.
Best practices for creating FocusNode objects
#Some dos and don'ts around using these objects include:
- Don't allocate a new
FocusNode
for each build. This can cause memory leaks, and occasionally causes a loss of focus when the widget rebuilds while the node has focus. - Do create
FocusNode
andFocusScopeNode
objects in a stateful widget.FocusNode
andFocusScopeNode
need to be disposed of when you're done using them, so they should only be created inside of a stateful widget's state object, where you can overridedispose
to dispose of them. - Don't use the same
FocusNode
for multiple widgets. If you do, the widgets will fight over managing the attributes of the node, and you probably won't get what you expect. - Do set the
debugLabel
of a focus node widget to help with diagnosing focus issues. - Don't set the
onKeyEvent
callback on aFocusNode
orFocusScopeNode
if they are being managed by aFocus
orFocusScope
widget. If you want anonKeyEvent
handler, then add a newFocus
widget around the widget subtree you would like to listen to, and set theonKeyEvent
attribute of the widget to your handler. SetcanRequestFocus: false
on the widget if you also don't want it to be able to take primary focus. This is because theonKeyEvent
attribute on theFocus
widget can be set to something else in a subsequent build, and if that happens, it overwrites theonKeyEvent
handler you set on the node. - Do call
requestFocus()
on a node to request that it receives the primary focus, especially from an ancestor that has passed a node it owns to a descendant where you want to focus. - Do use
focusNode.requestFocus()
. It is not necessary to callFocusScope.of(context).requestFocus(focusNode)
. ThefocusNode.requestFocus()
method is equivalent and more performant.
Unfocusing
#There is an API for telling a node to "give up the focus", named
FocusNode.unfocus()
. While it does remove focus from the node, it is important
to realize that there really is no such thing as "unfocusing" all nodes. If a
node is unfocused, then it must pass the focus somewhere else, since there is
always a primary focus. The node that receives the focus when a node calls
unfocus()
is either the nearest FocusScopeNode
, or a previously focused node
in that scope, depending upon the disposition
argument given to unfocus()
.
If you would like more control over where the focus goes when you remove it from
a node, explicitly focus another node instead of calling unfocus()
, or use the
focus traversal mechanism to find another node with the focusInDirection
,
nextFocus
, or previousFocus
methods on FocusNode
.
When calling unfocus()
, the disposition
argument allows two modes for
unfocusing: UnfocusDisposition.scope
and
UnfocusDisposition.previouslyFocusedChild
. The default is scope
, which gives
the focus to the nearest parent focus scope. This means that if the focus is
thereafter moved to the next node with FocusNode.nextFocus
, it starts with the
"first" focusable item in the scope.
The previouslyFocusedChild
disposition will search the scope to find the
previously focused child and request focus on it. If there is no previously
focused child, it is equivalent to scope
.
Focus widget
#The Focus
widget owns and manages a focus node, and is the workhorse of the
focus system. It manages the attaching and detaching of the focus node it owns
from the focus tree, manages the attributes and callbacks of the focus node, and
has static functions to enable discovery of focus nodes attached to the widget
tree.
In its simplest form, wrapping the Focus
widget around a widget subtree allows
that widget subtree to obtain focus as part of the focus traversal process, or
whenever requestFocus
is called on the FocusNode
passed to it. When combined
with a gesture detector that calls requestFocus
, it can receive focus when
tapped or clicked.
You might pass a FocusNode
object to the Focus
widget to manage, but if you
don't, it creates its own. The main reason to create your own
FocusNode
is to be able to call requestFocus()
on the node to control the focus from a parent widget. Most of the other
functionality of a FocusNode
is best accessed by changing the attributes of
the Focus
widget itself.
The Focus
widget is used in most of Flutter's own controls to implement their
focus functionality.
Here is an example showing how to use the Focus
widget to make a custom
control focusable. It creates a container with text that reacts to receiving the
focus.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Focus Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[MyCustomWidget(), MyCustomWidget()],
),
),
);
}
}
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({super.key});
@override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
Color _color = Colors.white;
String _label = 'Unfocused';
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (focused) {
setState(() {
_color = focused ? Colors.black26 : Colors.white;
_label = focused ? 'Focused' : 'Unfocused';
});
},
child: Center(
child: Container(
width: 300,
height: 50,
alignment: Alignment.center,
color: _color,
child: Text(_label),
),
),
);
}
}
Key events
#If you wish to listen for key events in a subtree,
set the onKeyEvent
attribute of the Focus
widget to
be a handler that either just listens to the key, or
handles the key and stops its propagation to other widgets.
Key events start at the focus node with primary focus.
If that node doesn't return KeyEventResult.handled
from
its onKeyEvent
handler, then its parent focus node is given the event.
If the parent doesn't handle it, it goes to its parent,
and so on, until it reaches the root of the focus tree.
If the event reaches the root of the focus tree without being handled, then
it is returned to the platform to give to
the next native control in the application
(in case the Flutter UI is part of a larger native application UI).
Events that are handled are not propagated to other Flutter widgets,
and they are also not propagated to native widgets.
Here's an example of a Focus
widget that absorbs every key that
its subtree doesn't handle, without being able to be the primary focus:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) => KeyEventResult.handled,
canRequestFocus: false,
child: child,
);
}
Focus key events are processed before text entry events, so handling a key event when the focus widget surrounds a text field prevents that key from being entered into the text field.
Here's an example of a widget that won't allow the letter "a" to be typed into the text field:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
return (event.logicalKey == LogicalKeyboardKey.keyA)
? KeyEventResult.handled
: KeyEventResult.ignored;
},
child: const TextField(),
);
}
If the intent is input validation, this example's functionality would probably
be better implemented using a TextInputFormatter
, but the technique can still
be useful: the Shortcuts
widget uses this method to handle shortcuts before
they become text input, for instance.
Controlling what gets focus
#One of the main aspects of focus is controlling what can receive focus and how.
The attributes canRequestFocus
, skipTraversal,
and descendantsAreFocusable
control how this node and its descendants participate in the focus process.
If the skipTraversal
attribute true, then this focus node doesn't participate
in focus traversal. It is still focusable if requestFocus
is called on its
focus node, but is otherwise skipped when the focus traversal system is looking
for the next thing to focus on.
The canRequestFocus
attribute, unsurprisingly, controls whether or not the
focus node that this Focus
widget manages can be used to request focus. If
this attribute is false, then calling requestFocus
on the node has no effect.
It also implies that this node is skipped for focus traversal, since it can't
request focus.
The descendantsAreFocusable
attribute controls whether the descendants of this
node can receive focus, but still allows this node to receive focus. This
attribute can be used to turn off focusability for an entire widget subtree.
This is how the ExcludeFocus
widget works: it's just a Focus
widget with
this attribute set.
Autofocus
#Setting the autofocus
attribute of a Focus
widget tells the widget to
request the focus the first time the focus scope it belongs to is focused. If
more than one widget has autofocus
set, then it is arbitrary which one
receives the focus, so try to only set it on one widget per focus scope.
The autofocus
attribute only takes effect if there isn't already a focus in
the scope that the node belongs to.
Setting the autofocus
attribute on two nodes that belong to different focus
scopes is well defined: each one becomes the focused widget when their
corresponding scopes are focused.
Change notifications
#The Focus.onFocusChanged
callback can be used to get notifications that the
focus state for a particular node has changed. It notifies if the node is added
to or removed from the focus chain, which means it gets notifications even if it
isn't the primary focus. If you only want to know if you have received the
primary focus, check and see if hasPrimaryFocus
is true on the focus node.
Obtaining the FocusNode
#Sometimes, it is useful to obtain the focus node of a Focus
widget to
interrogate its attributes.
To access the focus node from an ancestor of the Focus
widget, create and pass
in a FocusNode
as the Focus
widget's focusNode
attribute. Because it needs
to be disposed of, the focus node you pass needs to be owned by a stateful
widget, so don't just create one each time it is built.
If you need access to the focus node from the descendant of a Focus
widget,
you can call Focus.of(context)
to obtain the focus node of the nearest Focus
widget to the given context. If you need to obtain the FocusNode
of a Focus
widget within the same build function, use a Builder
to make sure you have
the correct context. This is shown in the following example:
@override
Widget build(BuildContext context) {
return Focus(
child: Builder(
builder: (context) {
final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
print('Building with primary focus: $hasPrimary');
return const SizedBox(width: 100, height: 100);
},
),
);
}
Timing
#One of the details of the focus system is that when focus is requested, it only takes effect after the current build phase completes. This means that focus changes are always delayed by one frame, because changing focus can cause arbitrary parts of the widget tree to rebuild, including ancestors of the widget currently requesting focus. Because descendants cannot dirty their ancestors, it has to happen between frames, so that any needed changes can happen on the next frame.
FocusScope widget
#The FocusScope
widget is a special version of the Focus
widget that manages
a FocusScopeNode
instead of a FocusNode
. The FocusScopeNode
is a special
node in the focus tree that serves as a grouping mechanism for the focus nodes
in a subtree. Focus traversal stays within a focus scope unless a node outside
of the scope is explicitly focused.
The focus scope also keeps track of the current focus and history of the nodes focused within its subtree. That way, if a node releases focus or is removed when it had focus, the focus can be returned to the node that had focus previously.
Focus scopes also serve as a place to return focus to if none of the descendants have focus. This allows the focus traversal code to have a starting context for finding the next (or first) focusable control to move to.
If you focus a focus scope node, it first attempts to focus the current, or most recently focused node in its subtree, or the node in its subtree that requested autofocus (if any). If there is no such node, it receives the focus itself.
FocusableActionDetector widget
#The FocusableActionDetector
is a widget that combines the functionality of
Actions
, Shortcuts
, MouseRegion
and a Focus
widget to create
a detector that defines actions and key bindings, and provides callbacks for
handling focus and hover highlights. It is what Flutter controls use to
implement all of these aspects of the controls. It is just implemented using the
constituent widgets, so if you don't need all of its functionality, you can just
use the ones you need, but it is a convenient way to build these behaviors into
your custom controls.
Controlling focus traversal
#Once an application has the ability to focus, the next thing many apps want to do is to allow the user to control the focus using the keyboard or another input device. The most common example of this is "tab traversal" where the user presses Tab to go to the "next" control. Controlling what "next" means is the subject of this section. This kind of traversal is provided by Flutter by default.
In a simple grid layout, it's fairly easy to decide which control is next. If you're not at the end of the row, then it's the one to the right (or left for right-to-left locales). If you are at the end of a row, it's the first control in the next row. Unfortunately, applications are rarely laid out in grids, so more guidance is often needed.
The default algorithm in Flutter (ReadingOrderTraversalPolicy
) for focus
traversal is pretty good: It gives the right answer for most applications.
However, there are always pathological cases, or cases where the context or
design requires a different order than the one the default ordering algorithm
arrives at. For those cases, there are other mechanisms for achieving the
desired order.
FocusTraversalGroup widget
#The FocusTraversalGroup
widget should be placed in the tree around widget
subtrees that should be fully traversed before moving on to another widget or
group of widgets. Just grouping widgets into related groups is often enough to
resolve many tab traversal ordering problems. If not, the group can also be
given a FocusTraversalPolicy
to determine the ordering within the group.
The default ReadingOrderTraversalPolicy
is usually sufficient, but in
cases where more control over ordering is needed, an
OrderedTraversalPolicy
can be used. The order
argument of the
FocusTraversalOrder
widget wrapped around the focusable components
determines the order. The order can be any subclass of FocusOrder
, but
NumericFocusOrder
and LexicalFocusOrder
are provided.
If none of the provided focus traversal policies are sufficient for your application, you could also write your own policy and use it to determine any custom ordering you want.
Here's an example of how to use the FocusTraversalOrder
widget to traverse a
row of buttons in the order TWO, ONE, THREE using NumericFocusOrder
.
class OrderedButtonRow extends StatelessWidget {
const OrderedButtonRow({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: <Widget>[
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextButton(
child: const Text('ONE'),
onPressed: () {},
),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextButton(
child: const Text('TWO'),
onPressed: () {},
),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: TextButton(
child: const Text('THREE'),
onPressed: () {},
),
),
const Spacer(),
],
),
);
}
}
FocusTraversalPolicy
#The FocusTraversalPolicy
is the object that determines which widget is next,
given a request and the current focus node. The requests (member functions) are
things like findFirstFocus
, findLastFocus
, next
, previous
, and
inDirection
.
FocusTraversalPolicy
is the abstract base class for concrete policies, like
ReadingOrderTraversalPolicy
, OrderedTraversalPolicy
and the
DirectionalFocusTraversalPolicyMixin
classes.
In order to use a FocusTraversalPolicy
, you give one to a
FocusTraversalGroup
, which determines the widget subtree in which the policy
will be effective. The member functions of the class are rarely called directly:
they are meant to be used by the focus system.
The focus manager
#The FocusManager
maintains the current primary focus for the system. It
only has a few pieces of API that are useful to users of the focus system. One
is the FocusManager.instance.primaryFocus
property, which contains the
currently focused focus node and is also accessible from the global
primaryFocus
field.
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-07-06。 查看文档源码 或者 为本页面内容提出建议。