Style part of text in TextField - flutter

I'm implementing a custom text field and I would like to style certain keywords (namely hashtags) differently than the rest of the text as the user type them in.
Kind of like this:
Is there a way to do that in Flutter ?

This question is very similar to How to change color of particular text in a text field dynamically?
I answered it there in: https://stackoverflow.com/a/57846261/5280562
In short: you can extend EditableText widget including its EditableTextState class and override buildTextSpan method.
Below is a working example called AnnotatedEditableText that I use in my app.
You need to supply a list of Annotation objects which describe which ranges of text need to be highlighted and what style to use.
import 'package:flutter/widgets.dart';
class Annotation extends Comparable<Annotation> {
Annotation({#required this.range, this.style});
final TextRange range;
final TextStyle style;
#override
int compareTo(Annotation other) {
return range.start.compareTo(other.range.start);
}
#override
String toString() {
return 'Annotation(range:$range, style:$style)';
}
}
class AnnotatedEditableText extends EditableText {
AnnotatedEditableText({
Key key,
FocusNode focusNode,
TextEditingController controller,
TextStyle style,
ValueChanged<String> onChanged,
ValueChanged<String> onSubmitted,
Color cursorColor,
Color selectionColor,
TextSelectionControls selectionControls,
this.annotations,
}) : super(
key: key,
focusNode: focusNode,
controller: controller,
cursorColor: cursorColor,
style: style,
keyboardType: TextInputType.text,
autocorrect: true,
autofocus: true,
selectionColor: selectionColor,
selectionControls: selectionControls,
onChanged: onChanged,
onSubmitted: onSubmitted,
);
final List<Annotation> annotations;
#override
AnnotatedEditableTextState createState() => new AnnotatedEditableTextState();
}
class AnnotatedEditableTextState extends EditableTextState {
#override
AnnotatedEditableText get widget => super.widget;
List<Annotation> getRanges() {
var source = widget.annotations;
source.sort();
var result = new List<Annotation>();
Annotation prev;
for (var item in source) {
if (prev == null) {
// First item, check if we need one before it.
if (item.range.start > 0) {
result.add(new Annotation(
range: TextRange(start: 0, end: item.range.start),
));
}
result.add(item);
prev = item;
continue;
} else {
// Consequent item, check if there is a gap between.
if (prev.range.end > item.range.start) {
// Invalid ranges
throw new StateError(
'Invalid (intersecting) ranges for annotated field');
} else if (prev.range.end < item.range.start) {
result.add(Annotation(
range: TextRange(start: prev.range.end, end: item.range.start),
));
}
// Also add current annotation
result.add(item);
prev = item;
}
}
// Also check for trailing range
final String text = textEditingValue.text;
if (result.last.range.end < text.length) {
result.add(Annotation(
range: TextRange(start: result.last.range.end, end: text.length),
));
}
return result;
}
#override
TextSpan buildTextSpan() {
final String text = textEditingValue.text;
if (widget.annotations != null) {
var items = getRanges();
var children = <TextSpan>[];
for (var item in items) {
children.add(
TextSpan(style: item.style, text: item.range.textInside(text)),
);
}
return new TextSpan(style: widget.style, children: children);
}
return new TextSpan(style: widget.style, text: text);
}
}
It's also available in this Gist: https://gist.github.com/pulyaevskiy/d7af7217c2e71f31dfb78699f91dfbb5

I actually had the same problem and found the AnnotatedEditbleText which helped me a lot.
I published the helpful package to solve this kind of problem.
https://pub.dev/packages/hashtagable

The TextField does not provide that functionality.
https://pub.dartlang.org/packages/zefyr can do that though.

I think there are some more ''hard'' ways to do this
The first one:
Make a row widget, add a part of the String until the word you want to highlight, add the special word, style it and add the rest of your string.
Or, you could try RichText
Günter post it about the zefyr package, I didn't use it yet, but if suits you, I'll be glad that helped

Related

Highlight parts of text selected by user in flutter

So, i'm working on an annotation app with flutter. The user selects a range of text in a selectableText Field and that text has to be highlighted. I'm using selectableText widget to get selected text.Is there a way?
This is a screenshot of what i'm trying to do.
Every highlighted part of text is a text that the user already selected and assigned a tag to.
[1]: https://i.stack.imgur.com/33qCL.png
So far, i've found this code that allows to highlight one selection :
import 'package:flutter/material.dart';
class HighlightText extends StatelessWidget {
final String text;
final String highlight;
final TextStyle style;
final TextStyle highlightStyle;
final Color highlightColor;
final bool ignoreCase;
HighlightText({
Key key,
this.text,
this.highlight,
this.style,
this.highlightColor,
TextStyle highlightStyle,
this.ignoreCase: false,
}) : assert(
highlightColor == null || highlightStyle == null,
'highlightColor and highlightStyle cannot be provided at same time.',
),
highlightStyle = highlightStyle ?? style?.copyWith(color: highlightColor) ?? TextStyle(color: highlightColor),
super(key: key);
#override
Widget build(BuildContext context) {
final text = this.text ?? '';
if ((highlight?.isEmpty ?? true) || text.isEmpty) {
return Text(text, style: style);
}
var sourceText = ignoreCase ? text.toLowerCase() : text;
var targetHighlight = ignoreCase ? highlight.toLowerCase() : highlight;
List<TextSpan> spans = [];
int start = 0;
int indexOfHighlight;
do {
indexOfHighlight = sourceText.indexOf(targetHighlight, start);
if (indexOfHighlight < 0) {
// no highlight
spans.add(_normalSpan(text.substring(start)));
break;
}
if (indexOfHighlight > start) {
// normal text before highlight
spans.add(_normalSpan(text.substring(start, indexOfHighlight)));
}
start = indexOfHighlight + highlight.length;
spans.add(_highlightSpan(text.substring(indexOfHighlight, start)));
} while (true);
return Text.rich(TextSpan(children: spans));
}
TextSpan _highlightSpan(String content) {
return TextSpan(text: content, style: highlightStyle);
}
TextSpan _normalSpan(String content) {
return TextSpan(text: content, style: style);
}
}
And i'm trying to adapt it so that i can highlight multiple parts.

Flutter: TextFiled become clear when add new number

In my application i have a text field.I create a TextInputFormatter to add as a inputFormatters to my TextField:
class CurrencyTextInputFormatter extends TextInputFormatter {
#override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text.isEmpty) {
return newValue.copyWith(text: '');
} else if (newValue.text.compareTo(oldValue.text) != 0) {
var selectionIndexFromTheRight =
newValue.text.length - newValue.selection.end;
final f =
NumberFormat.currency(locale: 'en', decimalDigits: 0, symbol: '');
var num = int.parse(newValue.text.replaceAll(RegExp('[^0-9]'), ''));
final newString = f.format(num).trim();
return TextEditingValue(
text: newString,
selection: TextSelection.collapsed(
offset: newString.length - selectionIndexFromTheRight),
);
} else {
return newValue;
}
}
}
everything is ok and when i add a price in TextField, the price is separated with , from right to left.
TextField(
textAlignVertical: TextAlignVertical.center,
controller: amountController,
inputFormatters: [
CurrencyTextInputFormatter(),
LengthLimitingTextInputFormatter(15),
],
In the parent of TextField in initState i initialized text field controller :
#override
void initState() {
super.initState();
amountController.text =
commaFormatter.format(widget.provider.totalUnpaidAmount);
}
commaFormatter use NumberFormat from intl to add comma between numbers:
final commaFormatter = NumberFormat("#,###");
The problem is here:
When number added to textfield by it's controller, when i want to delete this number by softkeyboard delete key, the number is not deleted ?But when i add a a new number, the old number completely deleted !!!!
How can i fix this problem?
I want to when number is added to this textfiled, Be editable !!!

What Output field to choose in calculator build with flutter

I am making a calculator app. I want to ask which widget can I use to display numbers. If I use Text then I can't edit it with cursor and if I use TextField keyboard pops up.
I found an implementation of a EditableText that might work for you. Just drop in this code:
class NoKeyboardEditableText extends EditableText {
NoKeyboardEditableText({
#required TextEditingController controller,
#required TextStyle style,
#required Color cursorColor,
bool autofocus = false,
Color selectionColor
}):super(
controller: controller,
focusNode: NoKeyboardEditableTextFocusNode(),
style: style,
cursorColor: cursorColor,
autofocus: autofocus,
selectionColor: selectionColor,
backgroundCursorColor: Colors.black
);
#override
EditableTextState createState() {
return NoKeyboardEditableTextState();
}
}
class NoKeyboardEditableTextState extends EditableTextState {
#override
void requestKeyboard() {
super.requestKeyboard();
//hide keyboard
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
}
class NoKeyboardEditableTextFocusNode extends FocusNode {
#override
bool consumeKeyboardToken() {
// prevents keyboard from showing on first focus
return false;
}
}
This will create a EditableText that does not open the keyboard, but you can still move the cursor as normal. To add text, I would use a combination of the text(), value(), selection() methods for the TextEditingController. First get the value in the field, then get the position of the cursor with selection(), input whatever text you want at the selection index, and finally set the text for the controller to the resulting string.
Let me know if this helps!
So, you can try this one
TextField(showCursor: true, readOnly: true);

Flutter: How to insert text in middle of text field text

I have a text field in flutter and an emoji picker button. On selecting an emoji I need to insert it at the current cursor position.
How can I achieve this?
Currently using TextEditingController I'm only able to append the emoji. I'm not able to get the current cursor offset.
emojiPicker() {
return EmojiPicker(
rows: 3,
columns: 7,
recommendKeywords: null,
indicatorColor: flujoAccentColor,
onEmojiSelected: (emoji, category) {
print(emoji);
_messageInputController.text =
_messageInputController.text + emoji.emoji;
}
);
}
Use _txtController.selection to get the selection (or cursor position).
replace the selection with selected emoji.
then fix the selection(or cursor position) of the controller
import 'package:emoji_picker/emoji_picker.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: HomePage()));
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController _messageInputController;
#override
void initState() {
_messageInputController = TextEditingController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: SafeArea(
child: Column(
children: <Widget>[
EmojiPicker(
rows: 3,
columns: 7,
recommendKeywords: null,
indicatorColor: Colors.red,
onEmojiSelected: (emoji, category) {
String text = _messageInputController.text;
TextSelection textSelection = _messageInputController.selection;
String newText = text.replaceRange(
textSelection.start, textSelection.end, emoji.emoji);
final emojiLength = emoji.emoji.length;
_messageInputController.text = newText;
_messageInputController.selection = textSelection.copyWith(
baseOffset: textSelection.start + emojiLength,
extentOffset: textSelection.start + emojiLength,
);
},
),
TextField(
controller: _messageInputController,
),
],
),
),
);
}
}
with this you can not only insert the selected emoji at cursor position, but also can replace some selected text
This is a slight modification to CrazyLazyCat's answer.
void _insertText(String inserted) {
final text = _controller.text;
final selection = _controller.selection;
final newText = text.replaceRange(selection.start, selection.end, inserted);
_controller.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: selection.baseOffset + inserted.length),
);
}
Notes:
_controller is a TextEditingController.
If you are changing both the text and the selection then you should use a TextEditingValue rather than changing them individually (since they each trigger an update).
Generally if you insert something you want the cursor to appear after then insert, thus the TextSelection.collapsed with the adjusted index.
i have another solution beside text.replaceRange.
All you need is :
TextEditingController _tec;
String youWillAddtoTEC = "your emoji or your clipboard data or else";
String beforeCursorPositionAtTEC = tec.selection.textBefore(tec.text);
String afterCursorPositionAtTEC = tec.selection.textAfter(tec.text);
String result = beforeCursorPositionAtTEC + youWillAddtoTEC + afterCursorPositionAtTEC;
and then add result to tec, or any widget your need:
tec.text = result;
for the selection or cursor position is same with above, but if you need place cursor after the "youWillAddToTEC" you can do like this:
tec.selection = TextSelection.collapsed(offset: tec.selection.start + youWillAddtoTEC.lenght);
If you want to replace the selection of text field with a new string, I found the method below is useful.
void replaceTextSelectionWith(TextEditingController textEditingController, Function(String selection) getReplaceString)
{
final text = textEditingController.text;
final selection = textEditingController.selection;
final replaceText = getReplaceString(selection.textInside(text)) as String;
final newText = text.replaceRange(selection.start, selection.end, replaceText);
textEditingController.value =
TextEditingValue(text: newText, selection: TextSelection.collapsed(offset: selection.start + replaceText.length));
}
And use it like this
replaceTextSelectionWith(textEditingController, (selection) => '**$selection**');

How to change color of particular text in a text field dynamically?

Consider bellow image, I want to dynamically change the text color of part of the text based on the user input text (not the whole text) in a text field. How can i do that in flutter?
For this example we actually don't need a full blown rich-text editor.
I had a similar goal in my app to highlight tags (#flutter) or date references (next week, on Friday, etc) and I was able to implement this by extending built-in EditableText widget and posted my example as a Gist here: https://gist.github.com/pulyaevskiy/d7af7217c2e71f31dfb78699f91dfbb5
Below is full implementation of this widget which I called AnnotatedEditableText.
There is new property annotations which describes ranges of text that need to be highlighted and their style.
import 'package:flutter/widgets.dart';
class Annotation extends Comparable<Annotation> {
Annotation({#required this.range, this.style});
final TextRange range;
final TextStyle style;
#override
int compareTo(Annotation other) {
return range.start.compareTo(other.range.start);
}
#override
String toString() {
return 'Annotation(range:$range, style:$style)';
}
}
class AnnotatedEditableText extends EditableText {
AnnotatedEditableText({
Key key,
FocusNode focusNode,
TextEditingController controller,
TextStyle style,
ValueChanged<String> onChanged,
ValueChanged<String> onSubmitted,
Color cursorColor,
Color selectionColor,
TextSelectionControls selectionControls,
this.annotations,
}) : super(
key: key,
focusNode: focusNode,
controller: controller,
cursorColor: cursorColor,
style: style,
keyboardType: TextInputType.text,
autocorrect: true,
autofocus: true,
selectionColor: selectionColor,
selectionControls: selectionControls,
onChanged: onChanged,
onSubmitted: onSubmitted,
);
final List<Annotation> annotations;
#override
AnnotatedEditableTextState createState() => new AnnotatedEditableTextState();
}
class AnnotatedEditableTextState extends EditableTextState {
#override
AnnotatedEditableText get widget => super.widget;
List<Annotation> getRanges() {
var source = widget.annotations;
source.sort();
var result = new List<Annotation>();
Annotation prev;
for (var item in source) {
if (prev == null) {
// First item, check if we need one before it.
if (item.range.start > 0) {
result.add(new Annotation(
range: TextRange(start: 0, end: item.range.start),
));
}
result.add(item);
prev = item;
continue;
} else {
// Consequent item, check if there is a gap between.
if (prev.range.end > item.range.start) {
// Invalid ranges
throw new StateError(
'Invalid (intersecting) ranges for annotated field');
} else if (prev.range.end < item.range.start) {
result.add(Annotation(
range: TextRange(start: prev.range.end, end: item.range.start),
));
}
// Also add current annotation
result.add(item);
prev = item;
}
}
// Also check for trailing range
final String text = textEditingValue.text;
if (result.last.range.end < text.length) {
result.add(Annotation(
range: TextRange(start: result.last.range.end, end: text.length),
));
}
return result;
}
#override
TextSpan buildTextSpan() {
final String text = textEditingValue.text;
if (widget.annotations != null) {
var items = getRanges();
var children = <TextSpan>[];
for (var item in items) {
children.add(
TextSpan(style: item.style, text: item.range.textInside(text)),
);
}
return new TextSpan(style: widget.style, children: children);
}
return new TextSpan(style: widget.style, text: text);
}
}
Rich text controller works fine!
See more on https://pub.dev/packages/rich_text_controller
First you choose your RegExp
RichTextController _controller;
Map<RegExp, TextStyle> patternUser = {
RegExp(r"\B#[a-zA-Z0-9]+\b"):
TextStyle(color: Colors.amber, fontWeight: FontWeight.bold)
};
on initState()
_controller = RichTextController(
patternMap: patternUser,
);
Add controller on your TextFormField
TextFormField(
controller: _controller,
style: TextStyle(color: Colors.white),
)