How to knows where TextWidget will wrap? - flutter

When using Text widget with big strings, it happens that the displayed text is splitted on 2 or more lines.
My question is simple : How to know the exact character where a string is divided ?
I've digged inside Text code. Up to dart:ui's Paragraph. But I haven't found anything usable.

Actually the text in flutter is painted on canvas using a TextPainter which computes the layout size during runtime with the box constrains of its parent class. If you already know the available space, you can compute the layout width of the text and compare it with the available width to know where the text would break.
You can computer the width of the text with TextPainter like:
final TextPainter paint = new TextPainter(
text: new TextSpan(
text: "Hello World",
),
textDirection: TextDirection.ltr
);
paint.layout();
debugPrint(paint.width.toString());
You can also do it with CustomPainter like this:
import 'package:flutter/material.dart';
void main() {
String text = "Hello World";
void textShownOnUI(int size){
debugPrint(text.substring(0,size));
}
runApp(new MaterialApp(
title: "Example",
home: new Container(
width: 100.0,
padding: const EdgeInsets.all(175.0),
child: new CustomPaint(
painter: new MyTextWidget(
text: text,
style: new TextStyle(),
notifySize: textShownOnUI
),
child: new Container(),
),
),
));
}
class MyTextWidget extends CustomPainter {
Function notifySize;
String text;
TextStyle style;
TextDirection direction;
MyTextWidget({this.text,this.notifySize,this.style,this.direction});
#override
void paint(Canvas canvas, Size size){
debugPrint(size.width.toString());
final TextPainter _painterWithConstrains = new TextPainter(
text: new TextSpan(
text: text,
style: style
),
textDirection: direction??TextDirection.ltr
);
String _willBeShownOnUI = text;
int _size = text.length;
TextPainter _temp = new TextPainter(
text: new TextSpan(
text: _willBeShownOnUI,
style: style
),
textDirection: direction??TextDirection.ltr
);
_painterWithConstrains.layout(maxWidth: size.width);
_temp.layout();
while(_temp.width > _painterWithConstrains.width && _size != 0){
debugPrint(_temp.width.toString()+" "+ _size.toString());
_willBeShownOnUI = _willBeShownOnUI.substring(0, --_size);
_temp = new TextPainter(
text: new TextSpan(
text: _willBeShownOnUI,
style: style
),
textDirection: direction??TextDirection.ltr
);
_temp.layout();
}
_size = _willBeShownOnUI.split(" ")[0].length; // Get complete words that will be shown
Function.apply(notifySize,<int>[_size]);
_painterWithConstrains.paint(canvas, Offset.zero);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Hope that helped!

Related

Animate specific part of txt?By RichText in Flutter, I changed background color by tween animation How to do brush stroke type animation on highlight

I want to make an Epub Reader with text highlighting, text selections. But I tried using RichText and highlight_text package. But it is static and does not add highlighting animations. Like I want to show the highlight happening (brush stroke from right to left on a word). In Flutter, is there any way to do it?
I want to add animations to each text highlight:
`TextHighlight(
//This widget uses RichText and nested TextSpan for highlighting
//I want to add animations to the text Inside TextSpan
text: text,
words: words as LinkedHashMap<String, HighlightedWord>,
textStyle: TextStyle(
fontSize: 20.0,
color: Colors.black,
backgroundColor: Colors.transparent,
),`
Flutter package I am using highlight_text
``
library highlight_text;
import 'dart:collection';
import 'dart:core';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class HighlightedWord {
final TextStyle textStyle;
final VoidCallback onTap;
HighlightedWord({
required this.onTap,
required this.textStyle,
});
}
class TextHighlight extends StatefulWidget {
String text;
TextAlign textAlign;
TextDirection? textDirection;
bool softWrap;
TextOverflow overflow;
double textScaleFactor;
int? maxLines;
Locale? locale;
StrutStyle? strutStyle;
bool enableCaseSensitive;
TextHighlight({
required this.text,
required this.words,
this.textStyle = const TextStyle(
fontSize: 25.0,
color: Colors.black,
),
this.textAlign = TextAlign.start,
this.textDirection,
this.softWrap = true,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.maxLines,
this.locale,
this.strutStyle,
this.enableCaseSensitive = false,
});
#override
_TextHightlightState createState() => _TextHighlightState(
required this.text,
required this.words,
this.textStyle = const TextStyle(
fontSize: 25.0,
color: Colors.black,
),
this.textAlign = TextAlign.start,
this.textDirection,
this.softWrap = true,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.maxLines,
this.locale,
this.strutStyle,
this.enableCaseSensitive = false,
);
}
class _TextHighlightState extends State<TextHighlight> {
String text;
TextAlign textAlign;
TextDirection? textDirection;
bool softWrap;
TextOverflow overflow;
double textScaleFactor;
int? maxLines;
Locale? locale;
StrutStyle? strutStyle;
bool enableCaseSensitive;
#override
void initState(){
text = widget.text;
textAlign = widget.textAlign;
super.initState();
}
#override
Widget build(BuildContext context) {
List<String> _textWords = _bind();
return AnimatedDefaultTextStyle(
child: RichText(
text: _buildSpan(_textWords),
locale: locale,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
),
);
}
List<String> _bind() {
String bindedText = text;
for (String word in words.keys) {
if (enableCaseSensitive) {
bindedText = bindedText.replaceAll(
word, '<highlight>${words.keys.toList().indexOf(word)}<highlight>');
} else {
int strIndex = bindedText.toLowerCase().indexOf(word.toLowerCase());
print('$word\n');
bindedText = bindedText.replaceRange(strIndex, strIndex + word.length,
'<highlight>${words.keys.toList().indexOf(word)}<highlight>');
}
}
List<String> splitedTexts = bindedText.split("<highlight>");
splitedTexts.removeWhere((s) => s.isEmpty);
return splitedTexts;
}
TextSpan _buildSpan(List<String> bindedWords) {
if (bindedWords.isEmpty) return TextSpan();
String nextToDisplay = bindedWords.first;
bindedWords.removeAt(0);
int? index = int.tryParse(nextToDisplay);
if (index != null) {
String currentWord = words.keys.toList()[index];
return TextSpan(
text: currentWord,
style: words[currentWord]!.textStyle,
children: [
_buildSpan(bindedWords),
],
recognizer: TapGestureRecognizer()
..onTap = () => words[currentWord]!.onTap(),
);
}
return TextSpan(
text: nextToDisplay,
style: textStyle,
children: [
_buildSpan(bindedWords),
],
);
}
}
Is there any way to do animations, tweaking the package code in some way.. or any other workaround..
Baiscally I want to highlight a specific text, rendered by Text widget. In mobile and Web.

Flutter: Alternative text when truncated

In some Text Widgets we display text which is getting truncated on some devices and we want to replace the text in this cases instead of truncating it.
I.E. we display a name like "Kamala Devi Harris"
When the name is too long for displaying we want to call a method and transform it to "Kamala D. Harris" and if that is also to long then "K. D. Harris" ... "K. Harris" or even "K. H."
How can we recognize that the current text is getting truncated?
I don't know if it's the most suitable way to achieve what you're trying to do but here's how we can do it :
Make a custom Widget which will take a list of String from greater value to lower so that it can check which value fit's in the available space.
class OverflowAwareText extends StatefulWidget {
final List<String> alternativeTexts;
OverflowAwareText({this.alternativeTexts});
#override
_OverflowAwareText createState() => _OverflowAwareText();
}
class _OverflowAwareText extends State<OverflowAwareText> {
List<String> texts;
int activeTextIndex = 0;
#override
void initState() {
super.initState();
texts = widget.alternativeTexts;
}
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, size) {
var textSpan = TextSpan(
text: texts[activeTextIndex],
style: TextStyle(fontSize: 42),
);
// Use a textpainter to determine if it will exceed max lines
var textPainter = TextPainter(
maxLines: 1,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
text: textSpan,
)..layout(maxWidth: size.maxWidth);
if (textPainter.didExceedMaxLines) {
if (activeTextIndex != texts.length - 1) {
WidgetsBinding.instance.addPostFrameCallback((t) {
setState(() {
activeTextIndex += 1;
});
});
}
}
return Text.rich(
textSpan,
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
});
}
}
Our OverflowAwareText takes a parameter alternativeTexts which is List<String>. You have to provide a list of Strings with larger to smaller values.
We can now use the OverflowAwareText :
#override
Widget build(BuildContext context) {
print('Printing from build method');
return Scaffold(
body: Center(
child: Container(
alignment: Alignment.center,
height: 50,
color: Colors.amber,
child: OverflowAwareText(
alternativeTexts: [
'Kamala Devi Harris',
'K. D. Harris',
'K. Harris',
'K. H'
],
),
),
),
);
}
We've wrapped the OverflowAwareText in a Container, so that we can check if the text is being changed or not when there is less space.
RESULT :
With no width in Container
With width = 330 in Container
With width = 180 in Container
With width = 100 in Container

How can I know the string length of each line in Text?

I want to create a text widget which has a 'see more' function. I want to get the string length in third line so I can do the substring.
The following code is what I have so far:
class ExpandedTextState extends State<ExpandedTextWidget> {
String firstHalf;
String secondHalf;
bool flag = true;
#override
void initState() {
super.initState();
TextPainter textPainter = new TextPainter();
textPainter.maxLines = 3;
textPainter.text = TextSpan(text: widget.text);
textPainter.textDirection = TextDirection.ltr;
textPainter.layout(maxWidth: double.infinity , minWidth: 0.0);
if(textPainter.didExceedMaxLines){
firstHalf = widget.text.substring(0, 50); //substring here
secondHalf = widget.text.substring(50, widget.text.length);
}else{
firstHalf = widget.text;
secondHalf = "";
}
}
#override
Widget build(BuildContext context) {
return new Container(
padding: new EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: secondHalf.isEmpty
? new Text(firstHalf)
: Container(
child: new RichText(
text: TextSpan(children: [
TextSpan(
text: flag ? (firstHalf + "...") : (firstHalf + secondHalf),
style: TextStyle(fontSize: 13, color: Colors.amber),),
TextSpan(
text: flag ? "see more" : "",
style: TextStyle(fontSize: 13, color: Colors.black),
recognizer: new TapGestureRecognizer()
..onTap = () {
setState(() {
flag = !flag;
});
}),
]),
),
)
);
}
}
How can I know the string length in each line, or how can I know the last offset of the third line?
If you want a component that expands the text and starts wih only three lines i would suggest to use a simple approach , if you want to know the offset you would need to know the font dimensions, the screen dimensions, the font weight, the device orientation.
Flutter itself can't give you that, it would be kinda hard to implement..
But, if you want a text of 3 lines that expands when you click "see more" and if it's expanded "see more" dissapears, you can use this approach
class ExpandableText extends StatefulWidget {
#override
_ExpandableTextState createState() => _ExpandableTextState();
}
class _ExpandableTextState extends State<ExpandableText> {
final String text =
'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp';
bool isExpanded = false;
#override
Widget build(BuildContext context) {
return isExpanded
? Text(text + text + text,
style: TextStyle(fontSize: 13, color: Colors.amber))
: Column(children: <Widget>[
Text(text + text + text,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: Colors.amber)),
GestureDetector(
onTap: () => setState(() => isExpanded = true),
child: Text('See More...',
style: TextStyle(fontSize: 13, color: Colors.black)))
]);
}
}
in that component we use a maxlines property to tell if the text is expanded or not, if it is , it's null , if it's not, it's 3

Make Emojis bigger in text?

Any idea how to make only the fontsize of emojis larger in the Text() widget?
The issue is that the Text() widget parses the emojis automatically. Sure, I can increase the overall text size, but I want the text to stay at the same fontsize.
If you want to auto increase size of emojis in chat message you can use this method:
static final RegExp REGEX_EMOJI = RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])');
Widget _buildContent(String content) {
final Iterable<Match> matches = REGEX_EMOJI.allMatches(content);
if (matches.isEmpty)
return Text(
'${content}',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Colors.black,
),
);
return RichText(
text: TextSpan(children: [
for (var t in content.characters)
TextSpan(
text: t,
style: TextStyle(
fontSize: REGEX_EMOJI.allMatches(t).isNotEmpty ? 20.0 : 12.0,
color: Colors.black,
)),
]));
}
You can use the widget RichText
RichText(
text: TextSpan(
text: 'hello',
children: <TextSpan>[
TextSpan(text: '🙂', style: TextStyle(fontSize: 30))
]
),
),
I was facing the same issue and find beginning of solution here
I've modified the code a little to allow showing text and emoji with different font size.
import 'package:flutter/material.dart';
/// Widget to render emoji and text with different font size
class EmojisText extends StatelessWidget {
const EmojisText({
Key? key,
required this.text,
required this.color,
required this.emojiSize,
required this.textSize,
}) : super(key: key);
///The text which emoji and alpha-numeric characters
///emoji can be absent
final String text;
/// THe font size to set to emoji
final double emojiSize;
/// The font size to set to text
final double textSize;
/// the color of the text
final Color color;
#override
Widget build(BuildContext context) {
return RichText(
text: _buildText(),
);
}
TextSpan _buildText() {
final children = <TextSpan>[];
final runes = text.runes;
for (int i = 0; i < runes.length; /* empty */) {
int current = runes.elementAt(i);
// we assume that everything that is not
// in Extended-ASCII set is an emoji...
final isEmoji = current > 255;
final shouldBreak = isEmoji ? (x) => x <= 255 : (x) => x > 255;
final chunk = <int>[];
while (!shouldBreak(current)) {
chunk.add(current);
if (++i >= runes.length) break;
current = runes.elementAt(i);
}
children.add(
TextSpan(
text: String.fromCharCodes(chunk),
style: TextStyle(
fontSize: isEmoji ? emojiSize : textSize,
color: color,
),
),
);
}
return TextSpan(children: children);
}
}

Flutter TextField - how to shrink the font if the text entered overflows

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.