How to add Zero Width Character - flutter

After reviewing this article from this Flutter issue (regarding to identify when back key is pressed to work with a pin alike widget)
I've been trying to add the \u200b special character as suggested in the article, but it is always taking it as a String with of length 6.
How can I work with it to place a \u200b Unicode character in the TextEditingController and make it work?
This is what the article suggests:
Take whatever content you want in your TextEditingController and place a \u200b unicode character at the front of that text. When the user deletes all of your content, the text field looks empty, but there is single remaining \u200b character. When the user presses “delete” one more time, your TextEditingController receives a change event, and the text in your TextEditingController goes from a length of 1 to a length of 0. Now you know that the user pressed “delete” in an “empty” text field and you can take whatever action you want as a developer.

For adding the zero-width-space to the TextEditingController it was enough to provide "\u200b" in the initializer and when updating the value of the controller.
As for how to make this detect delete presses, you can try the code below, it have a callback onEmptyTextField which triggers when the TextEditingController's text turns empty or already is empty.
import 'package:flutter/material.dart';
const zeroWidthSpace = "\u200b";
class DetectDeleteTextField extends StatelessWidget {
const DetectDeleteTextField({
Key? key,
required this.onEmptyTextField,
required this.controller,
}) : super(key: key);
/// Triggered when the TextField turns/is empty, such as when the zero-width-space ("\u200b")
/// or another character is deleted, or delete is pressed repeatedly.
final Function() onEmptyTextField;
final TextEditingController controller;
#override
Widget build(BuildContext context) {
return TextField(
controller: controller,
onChanged: (text) {
print("onChanged - Length: ${text.length}, Text runes:${text.runes.toString()}");
// Remove the zero-width-space character, handle cursor position
// correctly, and ensure no zero-width-space is present if text is pasted.
if (text.length >= 2 && text.contains(zeroWidthSpace)) {
String trimmedText = text.replaceAll(zeroWidthSpace, "");
controller.value = TextEditingValue(
text: trimmedText,
selection: TextSelection.fromPosition(TextPosition(offset: trimmedText.length)),
);
print("Trimming - Length: ${controller.text.length}, Text runes:${controller.text.runes.toString()}");
}
// Trigger the callback and reset the text to the zero-width-space character.
// The selection is needed for detecting backspace when only the
// zero-width-space exist in the text.
if (text.isEmpty) {
onEmptyTextField();
controller.value = const TextEditingValue(text: zeroWidthSpace);
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 1);
print("After delete - Length: ${controller.text.length}, Text runes:${controller.text.runes.toString()}");
}
},
);
}
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DetectDeleteTextField(
controller: TextEditingController(text: zeroWidthSpace),
onEmptyTextField: () => print("Delete triggered!")
),
],
),
),
);
}
}
void main() => runApp(const App());
Tested on Android and Windows.

Related

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()
);

Flutter: How to insert text properly in a TextField/TextFormField

Let's say there is an empty TextFormField. After entering 2 characters manually I would like to insert a new one programmatically. So if length equals with 2 than insert a new one. It sounds really simple but strange behaviors appeared while I tried to achieve this. For example: The cursor continuously jumps back to the start and may cause this debug log:
Text selection index was clamped (-1->0) to remain in bounds. This may not be your fault, as some keyboards may select outside of bounds.
Or if I try to deal with the TextEditingController's value or selection property to place cursor at the end of the text, it causes more strange behaviors.
Can you provide me an example by using a TextField or a TextFormField with a TextEditingController and on onChanged() if text length is equals with 2 than insert a new character at the end and place back the cursor also at the end.
I tried these solutions but in this case they did not work:
How do you change the value inside of a textfield flutter?
Thank you!
EDIT: Example code:
void main() => runApp(MyApp());
/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'example',
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final TextEditingController controller = TextEditingController(text: '');
#override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
onChanged: (value) {
if (controller.text != null && controller.text.length == 2) {
controller.text = '$value/';
controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
setState(() {});
}
},
);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
}
The problem: If I replace the TextFormField to a TextField, it works as expected. I guess it is a bug that should be fixed.
I also found a link that in flutter version 1.20.1 and later this is an issue with TextFormFields.
https://github.com/flutter/flutter/issues/62654
TextFormField is a FormField that contains a TextField. While a Form isn't required to be its parent, using a Form makes it easier to manage multiple fields at once. You can stick with TextField if it works better and meets the requirements.

Get text before/after cursor, on currently edited line, in TextField

I can print out all text before and after the cursor using this code:
controller = TextEditingController();
final selection = controller.value.selection;
final text = controller.value.text;
print("\nBEFORE\n${selection.textBefore(text)}");
print("\nAFTER\n${selection.textAfter(text)}\n");
How can I use the TextEditingController to find only the text before/after the cursor on the same line as the cursor?
For example, I want to use the TextEditingController to query the text before/after the current position of the cursor so the text before is "lin" and the text after the cursor is "e2 contents" (in the image above).
Full app code that gets all text before/after the cursor (not on the same line as required):
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: TextFieldApp()));
class TextFieldApp extends StatefulWidget {
#override
_TextFieldAppState createState() => _TextFieldAppState();
}
class _TextFieldAppState extends State<TextFieldApp> {
TextEditingController controller;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: TextField(
keyboardType: TextInputType.multiline,
controller: controller,
maxLines: 10,
maxLength: null,
),
);
}
#override
void dispose() {
super.dispose();
}
#override
void initState() {
controller = TextEditingController();
controller.text = 'line 1\nline2 contents\nline3';
controller.addListener(() {
final selection = controller.value.selection;
final text = controller.value.text;
print("\nBEFORE\n${selection.textBefore(text)}");
print("\nAFTER\n${selection.textAfter(text)}\n");
});
super.initState();
}
}
You can split the text by a \n then take the last value from the array.
Similarly the first entry for the text after.
final selection = controller.value.selection;
final text = controller.value.text;
final before = selection.textBefore(text);
final after = selection.textAfter(text);
print(before.split('\n').last);
print(after.split('\n').first);
Note:
If the users selects cont in a line with content line2 contents they would be
line2 //before
ents //after
If you need the text 'after' to also have the 'selected text',
you may add the text 'inside' to the text 'after'.
selection.textInside(text) + after.split('\n').first;

Flutter Keyboard listen on hide and show

How can I listen whether the keyboard is showing up or hiding?
I tried this example
How to listen to keyboard on screen Flutter?
void _listener(){
if(_myNode.hasFocus){
// keyboard appeared
}else{
// keyboard dismissed
}
}
FocusNode _myNode = new FocusNode()..addListener(_listner);
TextField _myTextField = new TextField(
focusNode: _mynNode,
...
...
);
But unfortunately it doesn't work. Any ideas how it could be possible to listen to the keyboard change?
It seems like it works when I press "Done" on the keyboard. But if i press back on my phone it won't go to "keyboard dismissed" because the focus still exists.. Any help?
KeyboardVisibilityBuilder
Listening for keyboard show/hide events can be achieved with WidgetsBindingObserver mixin. I prepared KeyboardVisibilityBuilder widget that handles the behavior for you. The usage is quite similar to AnimatedBuilder:
return KeyboardVisibilityBuilder(
builder: (context, child, isKeyboardVisible) {
if (isKeyboardVisible) {
// build layout for visible keyboard
} else {
// build layout for invisible keyboard
}
},
child: child, // this widget goes to the builder's child property. Made for better performance.
);
KeyboardVisibilityBuilder implementation:
/// Calls `builder` on keyboard close/open.
/// https://stackoverflow.com/a/63241409/1321917
class KeyboardVisibilityBuilder extends StatefulWidget {
final Widget child;
final Widget Function(
BuildContext context,
Widget child,
bool isKeyboardVisible,
) builder;
const KeyboardVisibilityBuilder({
Key key,
this.child,
#required this.builder,
}) : super(key: key);
#override
_KeyboardVisibilityBuilderState createState() => _KeyboardVisibilityBuilderState();
}
class _KeyboardVisibilityBuilderState extends State<KeyboardVisibilityBuilder>
with WidgetsBindingObserver {
var _isKeyboardVisible = false;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
final bottomInset = WidgetsBinding.instance.window.viewInsets.bottom;
final newValue = bottomInset > 0.0;
if (newValue != _isKeyboardVisible) {
setState(() {
_isKeyboardVisible = newValue;
});
}
}
#override
Widget build(BuildContext context) => widget.builder(
context,
widget.child,
_isKeyboardVisible,
);
}
Not sure how reliable this is, but there's this property on MediaQueryData:
/// The number of physical pixels on each side of the display rectangle into
/// which the application can render, but over which the operating system
/// will likely place system UI, such as the keyboard, that fully obscures
/// any content.
final EdgeInsets viewInsets;
Checking if viewInsets.vertical is greater than zero in the build() method gave me correct results:
#override
Widget build(BuildContext context) {
bool isKeyboardShowing = MediaQuery.of(context).viewInsets.vertical > 0;
return SafeArea(
child: Scaffold(
body: Column(
children: <Widget>[
Text(isKeyboardShowing ? 'YES!' : 'NO!'),
TextField(),
],
),
),
);
}
It's probably a good idea to combine this with other checks (e.g. input focus), to avoid false positives.
There is a package which mets your requirement completely. Please check that: flutter_keyboard_visibility
In there, by using StreamSubscription, you can listen whether the keyboard becomes goes up/down and react based on that.
what I need is a listener so I convert Andrey Gordeev code to
import 'package:flutter/cupertino.dart';
class KeyboardVisibilityListener extends StatefulWidget {
final Widget child;
final void Function(
bool isKeyboardVisible,
) listener;
const KeyboardVisibilityListener({
Key? key,
required this.child,
required this.listener,
}) : super(key: key);
#override
_KeyboardVisibilityListenerState createState() =>
_KeyboardVisibilityListenerState();
}
class _KeyboardVisibilityListenerState extends State<KeyboardVisibilityListener>
with WidgetsBindingObserver {
var _isKeyboardVisible = false;
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
final bottomInset = WidgetsBinding.instance!.window.viewInsets.bottom;
final newValue = bottomInset > 0.0;
if (newValue != _isKeyboardVisible) {
_isKeyboardVisible = newValue;
widget.listener(_isKeyboardVisible);
}
}
#override
Widget build(BuildContext context) => widget.child;
}
and
I used it to unfocus the input field when hiding the keyboard
home: KeyboardVisibilityListener(
listener: (isKeyboardVisible) {
if (!isKeyboardVisible) {
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Scaffold(
key: scaffoldKey,
body: layoutp.currentItem.page,
),
),
if (MediaQuery.of(context).viewInsets.bottom > 0.0) {
// keyboard on the screen
}
Simple explanation: MediaQuery to learn the size of the current media. This class use as MediaQueryData media = MediaQuery.of(context);. If any view appears on the screen MediaQuery.of(context).viewInsetsgive some value of the height of that view. As keyboard appears from the bottom on the screen so I use MediaQuery.of(context).viewInsets.bottom and this gives me the height of the keyboard taken on my screen. When the keyboard doesn't appear this value is 0.And this solution definitely works.
Did you pick up the spelling mistake?
FocusNode _myNode = new FocusNode()..addListener(_listner);
Should be:
FocusNode _myNode = new FocusNode()..addListener(_listener);
A widget that calls a callback whenever the user presses or releases a key on a keyboard.
A RawKeyboardListener is useful for listening to raw key events and hardware buttons that are represented as keys. Typically used by games and other apps that use keyboards for purposes other than text entry.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
const RawKeyboardListener({
Key key,
#required FocusNode focusNode,
#required ValueChanged<RawKeyEvent> onKey,
#required Widget child
})
Creates a widget that receives raw keyboard events.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
Implementation
const RawKeyboardListener({
Key key,
#required this.focusNode,
#required this.onKey,
#required this.child,
}) : assert(focusNode != null),
assert(child != null),
super(key: key);
You can use this library
keyboard_visibility_pro
KeyboardVisibility(
// it will notify
onChanged: (bool visible) {
print(visible);
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[TextField()],
),
),
),

How to listen to keyboard on screen Flutter?

I am building a mobile app, I want to remove a widget when the keyboard appears on the screen, i.e when the input text field is on focus.
I have tried to use RawKeyboardListener but that doesn't seem to work, my code is as below:
new Container(
child: new RawKeyboardListener(
focusNode: new FocusNode(),
onKey: (input) => debugPrint("*****KEY PRESSED"),
child: new TextField(
controller: new TextEditingController(),
),
),
);
You can use this simple check:
MediaQuery.of(context).viewInsets.bottom == 0
The keyboard is closed when this returns true, otherwise it's open.
Be aware to take the context of the whole screen (Scaffold for example) and not only from one widget.
This is how you integrate that check to your code:
Visibility(
child: Icon(Icons.add),
visible: MediaQuery.of(context).viewInsets.bottom == 0,
)
The keyboard will automatically appear when the text field is focused. So you can add a listner to the focusnode to listen the focus change and hide respective widget.
Example:
void _listener(){
if(_myNode.hasFocus){
// keyboard appeared
}else{
// keyboard dismissed
}
}
FocusNode _myNode = new FocusNode()..addListener(_listner);
TextField _myTextField = new TextField(
focusNode: _mynNode,
...
...
);
new Container(
child: _myTextField
);
I used the package keyboard_visibility
Then I wrapped my TextField with a KeyboardListener implemented as follows:
class KeyboardListener extends StatefulWidget {
final Widget child;
final void Function(bool) onChange;
KeyboardListener({#required this.child, #required this.onChange});
#override
_KeyboardListenerState createState() => _KeyboardListenerState();
}
class _KeyboardListenerState extends State<KeyboardListener> {
int _sId;
KeyboardVisibilityNotification _kvn;
#override
void initState() {
super.initState();
_kvn = KeyboardVisibilityNotification();
_sId = _kvn.addNewListener(
onChange: widget.onChange,
);
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void dispose() {
_kvn.removeListener(_sId);
super.dispose();
}
}
You can use this library keyboard_visibility: ^0.5.6 at :
https://pub.dev/packages/keyboard_visibility
For execute your code, insert this in the initState()
KeyboardVisibilityNotification.addNewListener(
onChange: (bool visible) {
print(visible);
this.setState(() {
keyboardIsOpen = visible;
});
},
);
Whenever keyboard is open or closed, the library calls onChange method with the visibility boolean.
A widget that calls a callback whenever the user presses or releases a key on a keyboard.
A RawKeyboardListener is useful for listening to raw key events and hardware buttons that are represented as keys. Typically used by games and other apps that use keyboards for purposes other than text entry.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
const RawKeyboardListener({
Key key,
#required FocusNode focusNode,
#required ValueChanged<RawKeyEvent> onKey,
#required Widget child
})
Creates a widget that receives raw keyboard events.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
Implementation
const RawKeyboardListener({
Key key,
#required this.focusNode,
#required this.onKey,
#required this.child,
}) : assert(focusNode != null),
assert(child != null),
super(key: key);