Prevent keyboard event from bubbling (propagating to parents) in Flutter - flutter

I am using RawKeyboardListener to detect the escape key to close (pop) windows, but I can't use the event and prevent it from bubbling (propagating) to parent windows, so all parent windows will receive escape and will close!
I tried using Focus element and it's onKey too, but not difference.
return Scaffold(
body: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}},
autofocus: true,
child: Container(
child: Text("blah blah")
),
),
),
);

you can attach the key listener to focus Node this listener will return a KeyEventResult enum that determines that the key event is handled or not
var focus = FocusNode(onKey: (FocusNode node, RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape)
{
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
});
Also here is the KeyEventResult description :
/// An enum that describes how to handle a key event handled by a
/// [FocusOnKeyCallback].
enum KeyEventResult {
/// The key event has been handled, and the event should not be propagated to
/// other key event handlers.
handled,
/// The key event has not been handled, and the event should continue to be
/// propagated to other key event handlers, even non-Flutter ones.
ignored,
/// The key event has not been handled, but the key event should not be
/// propagated to other key event handlers.
///
/// It will be returned to the platform embedding to be propagated to text
/// fields and non-Flutter key event handlers on the platform.
skipRemainingHandlers,
}

Related

Flutter FocusNode onKeyEvent detect multiple keys

Using this for Flutter Desktop. I used to use onKey of FocusNode. The doc says it will be deprecated:
/// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
/// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
/// in the future.
This is how I used onKey for detecting shift + enter:
FocusNode(
onKey: (node, event)
{
if (event.isKeyPressed(LogicalKeyboardKey.enter))
{
if (event.isShiftPressed)
return KeyEventResult.ignored;
sendAction();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
How do you detect multiple keys being pressed at the same time with onKeyEvent?
Edit: I'm looking for a way to detect multiple keys being pressed WITHOUT the legacy approach. The event parameter in onKeyEvent is not
RawKeyEvent type, therefore isKeyPressed() is not available there. The only thing available is event.logicalKey.keyLabel which can't be used for multiple key detection.
To handle the shortcuts Flutter has Shortcut widget. To start using it you have to specify:
Intent - describes that shortcut intent triggered
Action - describes action on triggered Intent
Let's create an intent class
class NewLineIntent extends Intent {
const NewLineIntent();
}
Add it to your widget and specify the conditions:
static const newLine = SingleActivator(LogicalKeyboardKey.enter, shift: true);
Finally, build a widget:
#override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: const {
newLine: NewLineIntent()
},
child: Actions(
actions: {
NewLineIntent: CallbackAction<NewLineIntent>(
onInvoke: (NewLineIntent intent) {
print('New line');
},
)
},
child: Focus(
autofocus: true,
child: ... ,
),
),
);
}
And when your widget will appear on the screen and you press Shift + Enter your shortcut will be triggered.

Is there any way to generate the same key for the "same" widget in Flutter without having any values to be based on

I have a widget which can have multiple input sections from the same type. When I delete the first child then it behaves weirdly like showing still the old value from that first widget. I figured out I need to use keys for the children but then my UX gets broken. Let me show you some code snippet, please:
class _ParentState extends State<ParentWidget> {
...
#override
Widget build(BuildContext context) {
return Column(
children: stateList
.mapIndexed((index, element) => ChildWidget(key: UniqueKey(), input: element, onChanged: (text) {
setState(() {
stateList[index].text = text;
});
}))
.toList(),
);
}
}
class _ChildState extends State<ChildWidget> {
final ctrl = TextEditingController();
#override
void initState() {
super.initState();
ctrl.text = widget.input.text;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: ctrl,
onChanged: (value) {
widget.onChanged(value);
}
),
...
]
);
}
}
When I wrote the user experience is broken, I was meaning that whenever the user starts to type into the text field then the focus gets loosen character-by-character since the callback is invoked and then generates a new child widget from the state because it has now "different" key... How can I refactor this? I just need a key mechanism which results the same for a child while it is not disposed yet!?
TL;DR
Solution:
remove key
by default, flutter is smart enough to determine which widget is need to rebuild or not. Thats why, key is very rarely used.
ChildWidget(input: element,
onChanged: (text) {
setState(() {
....
When you set UniquieKey() you widget will have different key every time build method called.
which is: everytime you call SetState() your apps will build NEW widget.
if you want to set key, you can use index value
ChildWidget(
key: ValueKey('$index'),
input: element,
onChanged: (text) {
what caused your "user experience broken".
it because, in your onChange function, you call setState() and also you set UniqueKey to your widget. So.. when setState() has called, it will re-execute build method, and since your ChildWidget has Unique key, it will rebuild the widget.
every time user typing, ChildWidget is rebuild. that caused lost focus

Stop event bubbling from Flutter Listener widget

I'm making an Android app in Flutter and I want to add an interactive widget that listens to raw pointer events and reacts to them. This is easy to do using the Listener widget:
class _MyWidget extends State<MyWidget> {
#override
build(BuildContext ctx) {
return Listener(
onPointerMove: (event) {
event.delta // use this to do some magic...
},
child: ...
);
}
}
However, this widget is rendered inside a PageView, which allows the user to swipe in order to change pages. This is a behavior I don't want to get rid of – except for the time when the user swipes over MyWidget. Since the MyWidget requires the user to touch-and-drag, I don't want them to accidentaly nagivate to a different page.
In a browser, this would be extremely simple to implement, I'd just call event.stopPropagation() and the event wouldn't propagate (ie. bubble) from MyWidget to its ancestor PageView. How do I stop propagation of an event in Flutter?
I could, in theory, make MyWidget set the application state, and use that to disable PageView while I'm swiping over MyWidget. However, that would go against the natural dataflow of Flutter's widgets: it would require me to add callbacks on multiple places and make all the widgets more intertwined and less reusable. I would much prefer to prevent the event from bubbling, locally.
EDIT: I've tried using AbsorbPointer, however it seems to be "blocking propagation" in the wrong direction. It blocks all children of AbsorbPointer from recieving pointer events. What I want is quite the opposite – stop pointer events on a child from propagating to its ancestors.
In Flutter, usually there is only 1 widget that would respond to user touch events, instead of everything at the touch location responding together. To determine which widget should be the one answering a touch event, Flutter uses something called "gesture arena" to determine a "winner".
Listener is a raw listening widget that does not compete in the "gesture arena", so the underlying PageView will still "win" in the "gesture arena" even though Listener is technically closer to the user (and would've won).
On the other hand, more advanced (less raw) widgets such as GestureDetector does enter the "gesture arena" competition, and will "win", thus causing the PageView to "lose" in the arena, so the page view would not move.
For example, try putting this code inside a PageView and drag on it:
GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
print('on Horizontal Drag Update');
},
onVerticalDragUpdate: (DragUpdateDetails details) {
print('on Vertical Drag Update');
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
)
You should notice that the PageView no longer scrolls.
However, depending on the scroll axis of the PageView (whether it's scrolling horizontally or vertically), removing one of the event on GestureDetector above (e.g. remove onHorizontalDragUpdate event listener on a horizontally scrolling PageView) would enable the PageView to scroll again. This is because horizontal swiping and vertical swiping are "non-conflicting events" that enter different gesture arenas.
So, go back to your original question, can you rewrite your business logic using these "more advanced" widgets, instead of dealing with the raw level data Listener? If so, the problem will be solved by the framework for you.
If you must use Listener for some reason, I have another possible workaround for you: In PageView widget, you can pass in physics: const NeverScrollableScrollPhysics() to make it stop scrolling. So you can perhaps monitor touch events, and set/clear property accordingly. Essentially, at "touch down", lock the PageView, and at "touch up", free it.
Based on my testing, Flutter sends events to the most deeply-nested widget first. You can use this to your advantage:
class MyWidget extends StatefulWidget {
// ...
}
class _MyWidgetState extends State<MyWidget> {
bool _pointerDownInner = false;
#override
Widget build(BuildContext context) {
// Outer listener
return Listener(
onPointerDown: (event) {
// If the mouse is over both listeners when the event
// happens then this flag will be true; otherwise it will
// be false.
if (!_pointerDownInner) {
// ...
}
_pointerDownInner = false;
},
// Inner listener
child: Listener(
onPointerDown: (event) {
// This is called first, so we set a flag here.
//
// There's no need to setState() here since we don't
// want the widget to re-render.
_pointerDownInner = true;
},
),
);
}
}
You can also do this across widgets:
class EventFlags {
bool pointerDownInner = false;
// ...
}
class Outer extends StatefulWidget {
const Outer({Key? key}) : super(key: key);
#override
State<Outer> createState() => _OuterState();
}
class _OuterState extends State<Outer> {
final EventFlags eventFlags = EventFlags();
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (event) {
// If the mouse is over both listeners when the event
// happens then this flag will be true; otherwise it will
// be false.
if (!eventFlags.pointerDownInner) {
// Do something...
}
eventFlags.pointerDownInner = false;
},
child: Inner(eventFlags: eventFlags),
);
}
}
class Inner extends StatelessWidget {
final EventFlags eventFlags;
const Inner({Key? key, required this.eventFlags}) : super(key: key);
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (event) {
eventFlags.pointerDownInner = true;
},
);
}
}

Flutter - Handle key event only if there is no text entry event

I'm trying to implement some shortcuts on a desktop app. I've been looking into these links:
Understanding Flutter's focus system
Focus and text fields
Using Actions and Shortcuts
I would like to do an action when the user presses on the key a (for example).
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int count = 0;
KeyEventResult onKey(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.keyA) {
setState(() {
count++;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Focus(
onKey: onKey,
child: Column(
children: [
Text('count: $count'),
const TextField(),
TextButton(
child: Text('button'),
onPressed: () {},
),
],
),
),
),
);
}
}
When the user clicks on a, the count increments.
The issue with this code is the user being unable to type a in the text field anymore because it is being handled by the focus node.
The first link states:
Key events start at the focus node with primary focus. If that node doesn’t return KeyEventResult.handled from its onKey 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.
and
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.
I would like my Focus widget to handle the key event only if the subtree didn't handle it itself including TextFields (and therefore text entry events).
I tried to always return KeyEventResult.ignored in the onKey method, but the OS triggers a sound meaning there is no action available every time the user clicks on a.
Is there a way to implement what I am trying to do? If yes, how?
You could wrap your text field in a focus that returns KeyEventResult.skipRemainingHandlers in onKeyEvent. This will prevent it affecting the other handlers while the text field is focused. Although this is inelegant and I'd love a correction with a better solution.
const unhandledKeys = [
LogicalKeyboardKey.delete,
LogicalKeyboardKey.backspace,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight
];
final focusNode = useFocusNode(onKeyEvent: (_a, _b) {
if (unhandledKeys.contains(event.logicalKey)) {
return KeyEventResult.ignored;
}
return KeyEventResult.skipRemainingHandlers;
});
return Focus(
focusNode: focusNode,
child: TextField()
);

How to detect key presses in flutter without a RawKeyboardListener

I'm trying to detect key presses like "Enter", "Delete" and "Backspace" within flutter. My issue with using a RawKeyboardListener is that it takes focus away from any child widgets.
For example
RawKeyboardListener(
focusNode: _focusNode,
onKey: handleKey,
child: TextField()
)
This makes it impossible to detect both key presses and use the Textfield at the same time.
Does anyone have a alternative way for detecting key presses.
Thanks
You can use the following from dart_html:
window.onKeyPress.listen((KeyboardEvent e) {
print(e.charCode.toString() + " " + new String.fromCharCode(e.charCode));
});
You can use dart:ui and set method window.onKeyData in initState in a stateful widget
Be careful with that method as if you have any focusable nodes on the screen you will need to pass events to them too, otherwise, for example, TextField will not work.
#override
void initState() {
window.onKeyData = (final keyData) {
if (keyData.logical == LogicalKeyboardKey.escape.keyId) {
widget.onPressed();
return true;
}
/// Let event pass to other focuses if it is not the key we looking for
return false;
};
super.initState();
}
#override
void dispose() {
window.onKeyData = null;
super.dispose();
}