How do I switch focus groups (via keyboard) immediately?
Preface: I'm not an expert on Focus
and find it quite convoluted myself.
But the below works for what you're after, I believe, traversing from one column to the next, without focusing "invisible" items. At least on mobile devices. Web platform... that's a whole different ballgame (and I doubt it works the same).
I got rid of the FocusableActionDetector
(which acts as a FocusNode
itself) and wrapped each Column in a FocusTraversalGroup
. I believe Flutter tries to go from TraversalGroup to TraversalGroup when it can.
The FocusScope
wrapping the TraversalGroups prevents the Back button and any other clickable items from getting focus (once the FocusScope
has gained focus).
class MyHomePage2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
width: double.infinity,
child: FocusScope( // LIMIT FOCUS TO DESCENDANTS
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (var i = 0; i < 2; i++)
FocusTraversalGroup( // CREATE GROUPS HERE
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < 3; i++)
SizedBox(
width: 150,
child: TextField(),
),
],
),
),
],
),
),
),
),
);
}
}
Summary
(to the best of my knowledge, which is fuzzy)
FocusScope
limits node traversal to its direct descendant nodes- focus will not traverse from one
FocusScope
to anotherFocusScope
- to go to another
FocusScope
the user will need to either manually/click-focus the otherFocusScope
, or its descendant nodes mustrequestFocus
- using
FocusScope
instead ofFocusTraversalGroup
above, would limit node traversal to oneColumn
, whichever got focus. It would not jump from oneColumn
to the next when reaching lastTextField
- focus will not traverse from one
FocusTraversalGroup
collects descendants into a group for traversal- but focus will leave this group for another group
- jumps when at final child node and another
FocusTraversalGroup
is available - can adjust traversal order within its group
Debug
Dumping the focus tree using debugDumpFocusTree
(a static function available everywhere) can be helpful in debugging.
I sometimes add it to the AppBar for easy, on-demand access:
return Scaffold(
appBar: AppBar(
title: Text('Focus Tab Page'),
actions: [
IconButton(icon: Icon(Icons.info_outline), onPressed: debugDumpFocusTree)
],
),
I've copied to relevant part below.
└─Child 2: FocusScopeNode#c83f0(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#7d536([IN FOCUS PATH])
│
├─Child 1: FocusScopeNode#7d536([IN FOCUS PATH])
│ │ context: FocusScope
│ │ IN FOCUS PATH
│ │ focusedChildren: FocusNode#1b886([PRIMARY FOCUS]),
│ │ FocusNode#72c3b, FocusNode#34b25, FocusNode#3b410,
│ │ FocusNode#02fac, FocusNode#61cd5
│ │
│ ├─Child 1: FocusNode#1d51e(FocusTraversalGroup)
│ │ │ context: Focus
│ │ │ NOT FOCUSABLE
│ │ │
│ │ ├─Child 1: FocusNode#3b410
│ │ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#70699]
│ │ │
│ │ ├─Child 2: FocusNode#34b25
│ │ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#7c822]
│ │ │
│ │ └─Child 3: FocusNode#72c3b
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#f00bc]
│ │
│ └─Child 2: FocusNode#bdb17(FocusTraversalGroup [IN FOCUS PATH])
│ │ context: Focus
│ │ NOT FOCUSABLE
│ │ IN FOCUS PATH
│ │
│ ├─Child 1: FocusNode#1b886([PRIMARY FOCUS])
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#c5a56]
│ │ PRIMARY FOCUS
│ │
│ ├─Child 2: FocusNode#61cd5
│ │ context: EditableText-[LabeledGlobalKey<EditableTextState>#a4dd8]
│ │
│ └─Child 3: FocusNode#02fac
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#fe12d]
│
├─Child 2: FocusNode#4886e
│ context: Focus
│
└─Child 3: FocusNode#9241b
context: Focus
Merritt
Updated on December 30, 2022Comments
-
Merritt over 1 year
When traversing from one focus group to the next (with tab on keyboard) I expect the focus to move to the first field in the next group, but it seems to focus on nothing - and then another tab moves into that group.
class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SizedBox( width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for (var i = 0; i < 2; i++) FocusableActionDetector( onFocusChange: (focused) { if (!focused) { print('Have left focus group'); } }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var i = 0; i < 3; i++) SizedBox( width: 150, child: TextField(), ), ], ), ), ], ), ), ), ); } }
I expect that after the last field in the first group a press of
Tab
should move focus immediately to the first item in the next focus group.I have tried all manner of
FocusNode
,FocusScope
,FocusScope.of(context).___
, however I am finding the focus management in Flutter a bit confusing. -
Merritt almost 3 yearsPerfect answer, nice bonus about using
debugDumpFocusTree
!FocusTraversalGroup
was primarily what I needed, and you helped me understand how it works much better. In addition for my use case I need to set the selection of theTextField
when focused, so I wrap the TextFields with aFocus
withskipTraversal: true
and set the controller selection insideonFocusChange
. Thank you for the excellent answer, much appreciated! <3