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

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

Related

Flutter ValueListenableBuilder result updates state to show/ hide widget

I am trying to use my ValueListenableBuilder, which generates Yes/ No buttons, to determine whether or not a textformfield will be visible. I.e. user selects the "Yes" button and the state of the app changes to allow the hidden textformfield to be displayed. I am extremely puzzled as to how I can either use setState, NotifyListeners, or ChangeNotifier to accomplish this task.
I am trying to avoid using either a radio button or making buttons outside of the ValueListenableBuilder because my ValueListenableBuilder is designed to generate a lot of my other buttons and I was hoping to incorporate one more function into them. Thanks in advance!
ValueListenableBuilder
ValueListenableBuilder<Option>(
valueListenable: yesNo,
builder: (context, option, _) => MakeButtons(
num0: 0,
num1: 1,
makeButtonWidth: MediaQuery.of(context).size.width * 0.20,
selected: option,
onChanged: (newOption) =>
yesNo.option = newOption,
ifSelected: (newOption) {
setState(() {
yesNo.option = newOption;
yesNo;
});
},
),
),
Make Buttons
enum Option {
option0,
option1,
}
class MakeButtons extends StatefulWidget {
MakeButtons({
this.num0,
this.num1,
this.selected,
this.onChanged,
this.ifSelected,
this.makeButtonWidth,
});
final int num0;
final int num1;
final double makeButtonWidth;
final Option selected;
final Function ifSelected;
final ValueChanged<Option> onChanged;
#override
_MakeButtonsState createState() => _MakeButtonsState();
}
class _MakeButtonsState extends State<MakeButtons> {
List<Widget> makeButtons(int num0, int num1, List<Widget> children,
List<Color> colors, List<Function> onPresses) {
List<Widget> buttons = new List();
for (int i = num0; i < num1; i++) {
buttons.add(Container(
constraints: BoxConstraints(
minWidth: widget.makeButtonWidth,
),
child: RectButton(
buttonChild: children[i],
bgColor: colors[i],
onPress: onPresses[i]),
));
}
return buttons;
}
Option selectedOption;
#override
Widget build(BuildContext context) {
List<Widget> children = [
AutoSizeText(
'Yes',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white),
),
AutoSizeText(
'No',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white),
),
];
List<Color> colors = [
selectedOption == Option.option0
? kActiveButtonColor
: kInactiveButtonColor,
selectedOption == Option.option1
? kActiveButtonColor
: kInactiveButtonColor,
];
List<Function> onPresses = [
() {
setState(() {
selectedOption = Option.option0;
});
return widget.onChanged(Option.option0);
},
() {
setState(() {
selectedOption = Option.option1;
});
return widget.onChanged(Option.option1);
},
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
makeButtons(widget.num0, widget.num1, children, colors, onPresses),
);
}
}
Visibility
visible: yesNo.title == 'A' ||
yesNo == 'Yes',
child: InputRow(
myUnit: defaultUnit,
inputParameter: 'Units',
textField: unitController,
colour: kEmoryDBlue,
),
),

Show counter to number of elements hidden when overflow occurs in flutter row widget

Can anyone please help to implement this feature of Gmail that shows the counter to number of emails hidden when the email list becomes large ? I want to implement this in row widget where instead of being scrollable extra elements count is shown when overflow occurs.Gmail shows +15 counter for hidden emails
I was Curious to give a try to achieve the same effect, as asked.
Just in case, If anyone want a start for writing a custom one, then below code may help.
Here is my Code, Feel free to give any suggestions,
(For Now delete button in chips is not working bcoz of some logic problem, I will make it work another day)
import 'package:flutter/material.dart';
class Demo3 extends StatefulWidget {
#override
_Demo3State createState() => _Demo3State();
}
class _Demo3State extends State<Demo3> {
String temp = "";
bool showChips = false;
List<Widget> chipsList = new List();
TextEditingController textEditingController = new TextEditingController();
final _focusNode = FocusNode();
int countChipsToDeleteLater = 0;
#override
void initState() {
super.initState();
_focusNode.addListener(() {
print("Has focus: ${_focusNode.hasFocus}");
if (!_focusNode.hasFocus) {
showChips = false;
setState(() {});
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: new Container(
height: 500,
child: new Center(
child: Container(
width: 300,
child: !showChips
? Row(
children: [
buildTextField(),
showNumberWidgetIfAny(),
],
)
: Center(
child: Wrap(
children: [
Wrap(
children: buildChips(),
),
buildTextField(),
],
),
),
),
),
),
),
);
}
buildChips() {
return chipsList;
}
buildTextField() {
return Container(
width: 200,
child: new TextField(
showCursor: true,
focusNode: _focusNode,
autofocus: true,
cursorColor: Colors.black,
style: new TextStyle(fontSize: 22.0, color: Colors.black),
controller: textEditingController,
// decoration: InputDecoration.collapsed(
// hintText: "",
// ),
onChanged: (value) {
if (value.contains(" ")) {
checkWhatToStoreInChips(value, countChipsToDeleteLater);
textEditingController.clear();
setState(() {
showChips = true;
});
countChipsToDeleteLater++;
}
},
),
);
}
checkWhatToStoreInChips(String val, int chipsIndex) {
temp = "";
for (int i = 0; i < val.length; i++) {
if (val[i] == " ") {
break;
}
temp = temp + val[i];
}
addToChips(temp, chipsIndex);
}
addToChips(String tmp, int chipsIndex) {
chipsList.add(Chip(
// onDeleted: () {
// if (chipsList.length == 0) {
// countChipsToDeleteLater = 0;
// }
// chipsList.removeAt(chipsIndex);
// print(chipsList.length);
// print(chipsIndex);
// setState(() {});
// },
avatar: CircleAvatar(
backgroundColor: Colors.grey.shade800,
child: Text(tmp[0]),
),
label: Text(temp),
));
}
showNumberWidgetIfAny() {
int len = chipsList.length;
if (len >= 1) {
return GestureDetector(
onTap: () {
showChips = true;
setState(() {});
},
child: new Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: new Text(
"${chipsList.length.toString()} ",
style: new TextStyle(color: Colors.white, fontSize: 22),
),
),
),
);
}
return Container();
}
}
How it works:
Write something in text field, then press space, showChips boolean will become true
onChanged will detect the space and will send the string to a function.
That function will extract the string before space and then will add the string to a chip,
Finally the chip will be added to a chipslist.
We will have a boolean variable to check if the textfield is in focus and when to show the textfield and numberwidget (a widget which will keep count of the total chips, same like you asked in your question) or when to show the chipslist and textfield wraped in a wrap widget.
You can play around by changing the decoration of textfield to collapsed, to it look like the same as gmail.
Check this package, if you want to use custom package for ease.
I was facing a similar issue. I found a way to implement the Overflow count text.
Sample image
You basically have to paint the overflow text, and get its width like below
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
textScaleFactor: WidgetsBinding.instance.window.textScaleFactor,
)..layout();
var textSize = textPainter.size;
textSize.width;
Then subtract that from the width available. Lets call it x.
Then create a sum of width for each row item(using TextPainter.layout() method mentioned above), till its value is less than x.
This way you'll know how many items can be shown in the row.
I have created a Flutter library to help with this.

How to access a widgets state from a stateful widget class Flutter

Sorry if this has been already answered somewhere else but I am new to Flutter. I have a toString method in my widget below that needs to access the state of the widget to output the string. The widget is a card that contains a text field and other text-related operations. To store information on what a user types into the card I need to get all the data into one string which toString returns.
class TextCard extends StatefulWidget {
_TextCardState cardState = _TextCardState();
TextCard({String text = ""}) {
cardState.textController.text = text;
}
#override
_TextCardState createState() => cardState = new _TextCardState();
String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
return delimiter2 +
"TextCard" +delimiter3 +
cardState.getText() +
delimiter3 +
(cardState.center.toString()) +
delimiter3 +
cardState.bold.toString() +
delimiter3 +
cardState.italic.toString() +
delimiter3 +
cardState.size.toString() +
delimiter2;
}
}
The widget also takes in a string value to set the initial value of a text field in the state below
class _TextCardState extends State<TextCard> {
double size = 18;
bool bold = false;
bool italic = false;
bool center = false;
var textController = TextEditingController();
#override
Widget build(BuildContext context) {
return Container(
height: _cardSizeY,
width: _cardSizeX,
child: Card(
elevation: _elevation,
child: Center(
child: Column(children: [
ListTile(leading: Icon(Icons.text_fields)),
ButtonBar(children: [
IconButton(
icon: Icon(Icons.format_bold),
onPressed: () {
updateText(size, !bold, italic, center);
},
),
IconButton(
icon: Icon(Icons.format_italic),
onPressed: () {
updateText(size, bold, !italic, center);
},
),
Slider(
value: size,
max: 80,
min: 1,
onChanged: (size) {
updateText(size, bold, italic, center);
})
]),
TextField(
maxLines: null,
style: TextStyle(
fontWeight: (bold) ? FontWeight.bold : FontWeight.normal,
fontStyle: (italic) ? FontStyle.italic : FontStyle.normal,
fontSize: size),
textAlign: (center) ? TextAlign.center : TextAlign.start,
controller: textController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(Radius.circular(10)))))
]))));
}
void updateText(double size, bool bold, bool italic, bool center) {
setState(() {
this.size = size;
this.bold = bold;
this.italic = italic;
this.center = center;
});
}
String getText() {
return textController.value.text;
}
}
When I run this code I get the error the create state function returned an old invalid state instance.
I have looked into putting the text controller into the _TextCardState() class but I would not be able to change the initial value of the TextField.
So I see what you are trying to do here but there are better ways to access the value of a textfield from outside of the class.
Instead of access your toString method from outside, which relies on a values from the private state class, I suggest a state management solution that will make this way easier and cleaner. You'll also have easier access to all those variables you need.
What you're doing here is not something that's meant to be done, which is why you're getting those state errors.
_TextCardState cardState = _TextCardState();
Here's a way to do it using GetX.
All your data will live in a GetX Controller class below and will be used in your now stateless TextCard widget.
class Controller extends GetxController {
var textController = TextEditingController();
String textfieldString = '';
double size = 18;
bool bold = false;
bool italic = false;
bool center = false;
#override
void onInit() {
super.onInit();
// updates the value of textfieldString anytime the user types
textController.addListener(() {
textfieldString = textController.text;
debugPrint(textController.text);
});
}
// this method lives in this class and is accessible from anywhere. The
// only thing not clear is what delimier2 is and where it comes from
// toString is not a good name because that is an overridden method that lives
// in most Dart classes
String buildString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
return delimiter2 +
"TextCard" +delimiter3 +
textfieldString +
delimiter3 +
(center.toString()) +
delimiter3 +
bold.toString() +
delimiter3 +
italic.toString() +
delimiter3 +
size.toString() +
delimiter2;
}
// single responsibility methods as opposed to firing one big function
// multiple times when its only affecting one variable
void toggleBold() {
bold = !bold;
update();
}
void toggleItalic() {
italic = !italic;
update();
}
void toggleCenter() {
center = !center;
update();
}
void updateSize(double sliderValue) {
size = sliderValue;
update();
}
}
Put this in your main before running your app. Can be done anywhere as long as its before you try and access the controller.
Get.put(Controller());
And here is your TextCard widget
class TextCard extends StatelessWidget {
#override
Widget build(BuildContext context) {
final controller =
Get.find<Controller>(); // finding the initalized controller
return Container(
height: _cardSizeY,
width: _cardSizeX,
child: Card(
elevation: 20,
child: Center(
child: Column(
children: [
ListTile(leading: Icon(Icons.text_fields)),
ButtonBar(children: [
IconButton(
icon: Icon(Icons.format_bold),
onPressed: () {
controller.toggleBold();
},
),
IconButton(
icon: Icon(Icons.format_italic),
onPressed: () {
controller.toggleItalic(); // accessing method via controller
},
),
// GetBuilder rebuilds children when value of controller variable changes
GetBuilder<Controller>(
builder: (_) {
return Slider(
value: controller
.size, // accessing size in other class via controller
max: 80,
min: 1,
onChanged: (value) {
controller.updateSize(value);
});
},
)
]),
GetBuilder<Controller>(
builder: (_) {
return TextField(
maxLines: null,
style: TextStyle(
fontWeight: (controller.bold)
? FontWeight.bold
: FontWeight.normal,
fontStyle: (controller.italic)
? FontStyle.italic
: FontStyle.normal,
fontSize: controller.size),
textAlign: (controller.center)
? TextAlign.center
: TextAlign.start,
controller: controller.textController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
);
},
)
],
),
),
),
);
}
}
So where ever you are in your app where you need that function, find the controller and get your value.
final controller = Get.find<Controller>():
final newString = controller.buildString();
This will be easier and use less memory because TextCard is now stateless.

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.