I'm implementing a simple rich text editor that renders text with a text editing controller that recognises basic markdown syntax, I'll link some code down below.
Everything works fine, the only problem I'm having is when a text style requires a bigger line height, for instance an # h1 that should be rendered as a title and therefore require a bigger line height overlaps over the previous line, as you can see in the screenshot below.
I've not been able so far to make the line height in a TextView variable based on the style of the text that is being displayed, is such thing even achievable in a Flutter TextView?
Here's a snippet of my text editing controller and a screenshot detailing my problem.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class AddNotePage extends StatelessWidget {
final TextEditingController _controller = MarkdownTextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Note'),
),
body: GestureDetector(
onVerticalDragDown: (_) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: TextField(
style: defaultTextStyle,
controller: _controller,
decoration: InputDecoration(
hintText: "Insert your message",
border: UnderlineInputBorder(
borderSide: BorderSide.none,
),
),
scrollPadding: EdgeInsets.all(20.0),
keyboardType: TextInputType.multiline,
maxLines: null,
),
),
],
),
),
);
}
}
const Map<String, TextStyle> defaultMarkdownStyleMap = {
r'^# .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 50,
),
r'^## .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 40,
),
r'^### .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
r'__(.*?)\__': TextStyle(fontStyle: FontStyle.italic, fontSize: 20),
r'~~(.*?)~~': TextStyle(decoration: TextDecoration.lineThrough, fontSize: 20),
r'\*\*(.*?)\*\*': TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
};
const TextStyle defaultTextStyle = TextStyle(fontSize: 20);
class MarkdownTextEditingController extends TextEditingController {
final Map<String, TextStyle> styleMap;
final Pattern pattern;
MarkdownTextEditingController({this.styleMap = defaultMarkdownStyleMap})
: pattern = RegExp(
styleMap.keys.map((key) {
return key;
}).join('|'),
multiLine: true);
#override
TextSpan buildTextSpan(
{required BuildContext context,
TextStyle? style,
required bool withComposing}) {
final List<InlineSpan> children = [];
text.splitMapJoin(
pattern,
onMatch: (Match match) {
TextStyle? markdownStyle = styleMap[styleMap.keys.firstWhere(
(e) {
return RegExp(e).hasMatch(match[0]!);
},
)];
children.add(TextSpan(
text: match[0],
style: style!.merge(markdownStyle),
));
return "";
},
onNonMatch: (String text) {
children
.add(TextSpan(text: text, style: style!.merge(defaultTextStyle)));
return "";
},
);
return TextSpan(style: style, children: children);
}
}
I've found a solution.
All I needed to do was to play around with the strutStyle property of the TextField.
As the documentation states:
The strut style used for the vertical layout.
StrutStyle is used to establish a predictable vertical layout. Since
fonts may vary depending on user input and due to font fallback,
StrutStyle.forceStrutHeight is enabled by default to lock all lines to
the height of the base TextStyle, provided by style. This ensures the
typed text fits within the allotted space.
Related
I'm using the flutter markdown package made by the flutter team here https://pub.dev/packages/flutter_markdown. I've created my own MarkdownElementBuilder based on their examples that inserts my own custom widget into the markdown and it looks like this:
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:outlit_app/constants/color_theme.dart';
import 'package:outlit_app/constants/dimension.dart';
import 'package:outlit_app/models/models.dart';
import 'package:markdown/markdown.dart' as md;
class DefinitionBuilder extends MarkdownElementBuilder {
final List<Definition> definitions;
DefinitionBuilder(this.definitions) : super();
#override
Widget visitElementAfter(md.Element element, TextStyle preferredStyle) {
final String textContent = element.textContent;
Definition definition = definitions.firstWhere(
(def) => textContent.toLowerCase().contains(def.word.toLowerCase()),
orElse: () =>
Definition(word: 'nothing found for $textContent', definition: ''),
);
return Tooltip(
margin: EdgeInsets.all(Dimensions.MARGIN_SIZE_EXTRA_LARGE),
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_DEFAULT),
decoration: BoxDecoration(
color: GetColor.gradientPurple,
borderRadius: BorderRadius.circular(8),
),
verticalOffset: -10,
triggerMode: TooltipTriggerMode.tap,
message: definition.definition.trim(),
child: Text(
textContent.trim(),
style: TextStyle(
color: GetColor.primaryColor,
fontSize: Dimensions.FONT_SIZE_OVER_LARGE,
),
),
);
}
}
class DefinitionSyntax extends md.InlineSyntax {
static final String AST_SYMBOL = 'def';
DefinitionSyntax() : super(_pattern);
static const String _pattern = r'{{(.*)}}';
#override
bool onMatch(md.InlineParser parser, Match match) {
parser.addNode(md.Element.text(AST_SYMBOL, match[1]));
return true;
}
}
It works well but the widget is always on it's own seperate line as opposed to being inline with the rest of the text. If I return a simple text widget I still get the same thing.
Any tips in the right direction would be great :)
I got it work although not perfect because the leading distribution is a little off with the text of the tooltip but the widget that gets embedded now looks like this:
return RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Container(
child: Tooltip(
margin: EdgeInsets.all(Dimensions.MARGIN_SIZE_EXTRA_LARGE),
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_DEFAULT),
decoration: BoxDecoration(
color: GetColor.gradientPurple,
borderRadius: BorderRadius.circular(8),
),
verticalOffset: -10,
triggerMode: TooltipTriggerMode.tap,
message: definition.definition.trim(),
child: Text(
textContent.trim(),
style: TextStyle(
color: GetColor.primaryColor,
fontSize: Dimensions.FONT_SIZE_OVER_LARGE,
leadingDistribution: TextLeadingDistribution.even,
height: 1,
),
),
),
))
],
),
);
I have the following function
static Widget nameAvatar({String displayName, TextStyle style, double scale}) {
final firstLetter = displayName.isNotEmpty ? displayName[0].toUpperCase() : '?';
return Center(
child: Text(
firstLetter,
style: style,
textScaleFactor: 5.0,
),
);
}
Which returns a widget. I put this in a circle avatar. It's the fallback()
#override
Widget build(BuildContext context) {
if (hasPhotoURL) precacheImage(_buildPhoto(), context);
return InkWell(
onTap: () => onTap?.call(),
child: CircleAvatar(
backgroundColor: backgroundColor,
radius: radius,
backgroundImage: _buildPhoto(),
child: ConditionalWidget(
condition: !hasPhotoURL,
builderTrue: () => fallback,
),
),
);
}
But my text seems to be scaling downwards? If I remove the textScaleFactor it is aligned perfectly in the center of the circle? It's almost as if the scaling is no being done from the center of the text. How can I get this letter in the center?
I use the following texttheme
bodyText1: const TextStyle(
fontSize: 14,
color: _primaryColorLight,
fontFamily: 'Open Sans',
)
Im trying to do chat mentions and I need to some how change the color of full name that mentioned in the TextField before send the message.
How can I do that?
Hope i'm not too late :)
I had the same issue and couldn't find any implementation in the main flutter package or any third party packages, so i hacked a little packaage and uploaded it.
it's an extension of the text editing controller that you can supply with a map of Regex patterns and corresponding Text style.
https://pub.dev/packages/rich_text_controller
https://github.com/micwaziz/rich_text_controller
In case someone stumbles across this question (like I did) but requires the textfield to render tap-able links, then here's one way to do it (simplified from our final implementation for better clarity). It's inspired by the RichTextController, but with the definitive focus to allow for taps on the links.
Regarding the regular expression: We tried using linkify, but despite trying all options, it tended to modify the links - which messed up the user input.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:url_launcher/url_launcher.dart';
class LinkedTextEditingController extends TextEditingController {
final RegExp linkRegexp;
final TextStyle linkStyle;
final Function(String match) onTap;
static RegExp _defaultRegExp =
RegExp(r'((?:(https?:\/\/)?)(www\.)?[-a-zA-Z0-9#:%._\+~#=]{2,256}\.[a-z]{2,5}\b([-a-zA-Z0-9#:%_\+.~#?&//=]*))', caseSensitive: false, dotAll: true);
static void _defaultOnLaunch(String url) async {
final re = RegExp('^https?://');
final fullUrl = re.hasMatch(url) ? url : 'http://$url';
if (await canLaunch(fullUrl)) {
await launch(fullUrl);
}
}
LinkedTextEditingController({
String text,
RegExp regexp,
this.linkStyle,
this.onTap = _defaultOnLaunch,
}) : linkRegexp = regexp ?? _defaultRegExp,
super(text: text);
LinkedTextEditingController.fromValue(
TextEditingValue value, {
RegExp regexp,
this.linkStyle,
this.onTap = _defaultOnLaunch,
}) : linkRegexp = regexp ?? _defaultRegExp,
assert(
value == null || !value.composing.isValid || value.isComposingRangeValid,
'New TextEditingValue $value has an invalid non-empty composing range '
'${value.composing}. It is recommended to use a valid composing range, '
'even for readonly text fields',
),
super.fromValue(value ?? TextEditingValue.empty);
#override
TextSpan buildTextSpan({TextStyle style, bool withComposing}) {
List<TextSpan> children = [];
text.splitMapJoin(
linkRegexp,
onMatch: (Match match) {
children.add(
TextSpan(
text: match[0],
style: linkStyle,
recognizer: onTap == null ? null : TapGestureRecognizer()
..onTap = () => onTap(match[0]),
),
);
return null;
},
onNonMatch: (String span) {
children.add(TextSpan(text: span, style: style));
return span;
},
);
return TextSpan(style: style, children: children);
}
}
as Durdu suggested, you can use RichText to achieve different color text. Just need to put multiple TextSpan with different color using TextStyle. Sample as below
Container(
margin: const EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: 10.0),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: 'By clicking sign up, you have read and agreed to our ',
style: TextStyle(color: Colors.black54),
),
TextSpan(
text: 'Terms & Conditions',
style: TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
print('Tapped on hyperlink');
},
),
TextSpan(
text: '.',
style: TextStyle(color: Colors.black54),
),
],
),
),
),
Hope this clears and solve your problem.
If you just want to change the color or font for the text in the textfield:
Text("Hello",
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900))
If you want to use multiple styles you should check, you should check RichText.
The RichText widget displays text that uses multiple different styles.
RichText(
text: TextSpan(
text: 'Hello ',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900)),
children: <TextSpan>[
TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: ' world!'),
],
),
)
I store a text string with \n and unicode literals like \u2022 in mysql, then retrieve it with http api call on flutter. When displaying it with Text widget, these escaped symbles do not show as expected. When I directly pass the string , it works. Could anyone help me out?
child: Column(
children: <Widget>[
Text(prompt.prompt_body, //This variable is from http call which does not work
textAlign: TextAlign.left,
style:TextStyle(
color: Colors.black,
fontSize: 13,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic
)),
Divider(),
Text("You live in a room in college which you share with another student.However, there are many problems with this arrangement and you find it very difficult to work.\n\nWrite a letter to the accommodation officer at the college. In the letter,\n\n \u2022 describe the situation\n \u2022 explain your problems and why it is difficult to work\n \u2022 say what kind of accommodation you would prefer", //this part works
textAlign: TextAlign.left,
style:TextStyle(
color: Colors.black,
fontSize: 13,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic
))
],
),
emulator screenshot
In response to Gunter's query, I add the following code on api call:
class PromptModel {
int id;
String prompt_body;
String prompt_image;
PromptModel(this.id, this.prompt_body, this.prompt_image);
PromptModel.fromJson(Map<String, dynamic> parsedJson) {
id = parsedJson['id'];
prompt_body = parsedJson['prompt_body'];
prompt_image = parsedJson['prompt_image'];
}
}
....
class PromptListPageState extends State<PromptListPage> {
int counter = 0;
List<PromptModel> prompts = [];
void fetchImage() async {
counter++;
var response =
await get('http://10.0.2.2:8080/TestPrompt');
var promptModel = PromptModel.fromJson(json.decode(response.body));
setState(() {
prompts.add(promptModel);
});
}
The following is the response of the api call:
{"id":1,"prompt_body":"You live in a room in college which you share with another student.However, there are many problems with this arrangement and you find it very difficult to work.\\n\\nWrite a letter to the accommodation officer at the college. In the letter,\\n\\n \\u2022 describe the situation\\n \\u2022 explain your problems and why it is difficult to work\\n \\u2022 say what kind of accommodation you would prefer","prompt_image":"http://10.0.2.2:8080/test.jpg"}
I solved the problem by inputting the string from flutter using TextFormField. directly inserting the text on database side is tricky. The code is as below:
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: myController,
maxLines: 5,
validator: (val) =>
(val == null || val.isEmpty) ? "请输入商品名称" : null,
decoration: const InputDecoration(
//icon: Icon(Icons.person),
hintText: 'add the prompt here:',
labelText: 'Prompt content',
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.teal)),
),
onSaved: (val) => this.content = val,
),
new Container(
margin: const EdgeInsets.only(top: 10.0),
child: new RaisedButton(
onPressed: _save,
child: new Text('Save'),
),
)
]),
),
appBar: AppBar(
title: Text('Add Essay Prompt'),
),
),
);
}
}
I have a TextField (not a Text) widget that must remain on one line. I want to reduce it's font size if the text entered is too large for the TextField box, ie shrink it if it overflows. How can I do this?
I have written some code like this in a stateful component
if (textLength < 32) {
newAutoTextVM.fontSize = 35.0;
} else if (textLength < 42) {
newAutoTextVM.fontSize = 25.0;
In the view
fontSize: 25.0,
but it isn't very intelligent, it doesn't cope with resizing, also, because the font size isn't monospaced (courier etc), different characters take up different amounts of space.
Use a TextPainter to calculate the width of your text. Use a GlobalKey to get the size of your widget (A LayoutBuilder might be better to handle screen rotation).
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: Home()));
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
const textFieldPadding = EdgeInsets.all(8.0);
const textFieldTextStyle = TextStyle(fontSize: 30.0);
class _HomeState extends State<Home> {
final TextEditingController _controller = TextEditingController();
final GlobalKey _textFieldKey = GlobalKey();
double _textWidth = 0.0;
double _fontSize = textFieldTextStyle.fontSize;
#override
void initState() {
super.initState();
_controller.addListener(_onTextChanged);
}
void _onTextChanged() {
// substract text field padding to get available space
final inputWidth = _textFieldKey.currentContext.size.width - textFieldPadding.horizontal;
// calculate width of text using text painter
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: _controller.text,
style: textFieldTextStyle,
),
);
textPainter.layout();
var textWidth = textPainter.width;
var fontSize = textFieldTextStyle.fontSize;
// not really efficient and doesn't find the perfect size, but you got all you need!
while (textWidth > inputWidth && fontSize > 1.0) {
fontSize -= 0.5;
textPainter.text = TextSpan(
text: _controller.text,
style: textFieldTextStyle.copyWith(fontSize: fontSize),
);
textPainter.layout();
textWidth = textPainter.width;
}
setState(() {
_textWidth = textPainter.width;
_fontSize = fontSize;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Autosize TextField'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: _textFieldKey,
controller: _controller,
decoration: InputDecoration(
border: InputBorder.none,
fillColor: Colors.orange,
filled: true,
contentPadding: textFieldPadding,
),
style: textFieldTextStyle.copyWith(fontSize: _fontSize),
),
Text('Text width:'),
Container(
padding: textFieldPadding,
color: Colors.orange,
child: Row(
children: <Widget>[
Container(width: _textWidth, height: 20.0, color: Colors.blue),
],
),
)
],
),
),
);
}
}
I have searched through the docs and found a couple of solutions that could come at your help:
L̶o̶o̶k̶ ̶a̶t̶ ̶t̶h̶e̶ ̶o̶f̶f̶i̶c̶i̶a̶l̶ ̶d̶o̶c̶s̶[̶1̶]̶,̶ ̶i̶n̶ ̶p̶a̶r̶t̶i̶c̶u̶l̶a̶r̶e̶ ̶a̶t̶ ̶t̶h̶e̶s̶e̶ ̶p̶r̶o̶p̶e̶r̶t̶i̶e̶s̶:̶ ̶ ̶m̶a̶x̶L̶i̶n̶e̶s̶,̶ ̶o̶v̶e̶r̶f̶l̶o̶w̶ ̶a̶n̶d̶ ̶s̶o̶f̶t̶W̶r̶a̶p̶ (These are TextBox properties, not TextFields)
Have a look at this thread where they suggest to wrap the TextBox/TextFeld with a Flexible Widget
Depending on the rest of your code one of these solutions could be better, try tweaking around.
Hope it helps.