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 another FocusScope
- to go to another
FocusScope the user will need to either manually/click-focus the other FocusScope, or its descendant nodes must requestFocus
- using
FocusScope instead of FocusTraversalGroup above, would limit node traversal to one Column, whichever got focus. It would not jump from one Column to the next when reaching last TextField
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