Related
I have a DataTable in which some cells have links. Ideally, I would like to fetch a preview about the link's content whenever hovering over the link, which I was able to achieve using the Stack widget. However, since the stacked preview is inside the DataCell, it seems like I'm not able to raise its "z-index" to be on top of the rest of the table.
Is this not possible with Flutter, or is there a way around it?
The only way I imagine this working, without something to update a global z-index, would be for the cell to update a global state and then have the thumbnail preview appear on a Stack above the DataTable level. But I wish there was a less clunkier way to do it...
3 widgets I've tried but to no avail — they might work, I don't know —:
Tooltip
Overlay
FloatingActionButton
My whole app is here, and the precise commit is 0303732. The relevant code is this ClickableLink widget:
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:url_launcher/url_launcher.dart';
import '../schema/links.dart';
#immutable
class ClickableLink extends StatefulWidget {
const ClickableLink({
Key? key,
required this.link,
this.linkText,
this.color = Colors.blue,
}) : super(key: key);
final Link link;
final String? linkText;
final Color color;
#override
State<ClickableLink> createState() => _ClickableLinkState();
}
class _ClickableLinkState extends State<ClickableLink> {
Widget hoverWidget = const SizedBox.shrink();
void _fetchPreview(PointerEvent pointerEvent) {
setState(() {
if (widget.link.host == 'online-go.com' && widget.link.prePath == 'game') {
hoverWidget = Positioned(
top: 25,
child: Image.network('https://online-go.com/api/v1/games/${widget.link.id}/png'),
);
}
});
}
void _onExit(PointerEvent pointerEvent) {
setState(() {
hoverWidget = const SizedBox.shrink();
});
}
#override
Widget build(BuildContext context) {
return MouseRegion(
onHover: _fetchPreview,
onExit: _onExit,
child: Stack(
clipBehavior: Clip.none,
children: [
SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(color: widget.color),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
hoverWidget,
],
),
);
}
}
The problem here is due to the fact that your Stack widget, defined inside ClickableLink, will be at a "lower" point (inside your app widget tree) than every other GameResultCell.
So even the higher z-index will still be behind the other GameResultCells.
To fix this I would reccomend changing your structure and define an higher point in your structure to show the preview.
Another way could be using a library to nest your preview inside a tooltip. Take a look at this one for example:
just_the_tooltip: ^0.0.11+2. With this package, you could even use a StatelessWidget.
The result here is more similar to what I suppose you were expecting.
class ClickableLink extends StatelessWidget {
#override
Widget build(BuildContext context) {
return JustTheTooltip(
content: Image.network(
'https://online-go.com/api/v1/games/${widget.link.id}/png',
),
child: SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(
color: widget.color ??
(DogempTheme.currentThemeIsLight(context)
? const Color(0xff1158c7)
: Colors.orange.withOpacity(0.85)),
),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
);
}
}
Lastly you could use a Dialog, but the resulting behaviour is a bit different.
Take a look at this code if you want to try:
class _ClickableLinkState extends State<ClickableLink> {
Widget hoverWidget = const SizedBox.shrink();
void _fetchPreview(PointerEvent pointerEvent) {
showDialog(
context: context,
builder: (context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
'https://online-go.com/api/v1/games/${widget.link.id}/png'),
const SizedBox(
height: 16.0,
),
TextButton(
onPressed: () async => launch(widget.link.completeLink),
child: const Text('Go to complete link'))
],
),
);
},
);
}
#override
Widget build(BuildContext context) {
return MouseRegion(
onHover: _fetchPreview,
child: Stack(
clipBehavior: Clip.none,
children: [
SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(
color: widget.color ??
(DogempTheme.currentThemeIsLight(context)
? const Color(0xff1158c7)
: Colors.orange.withOpacity(0.85)),
),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
],
),
);
}
}
I am trying to use CheckboxListTile in a popup widget so the user can check as many items as needed and then click OK to accept the checked items which adds them to a list of items in main widget
What happens is CheckboxListTile loses checked state when scrolling down for some items, when getting back up in the list it is not checked anymore,
here is my code for the CheckboxListTile
class ItemTileWithImage extends StatefulWidget {
final Model model;
ItemTileWithImage(this.model);
#override
_ItemTileWithImageState createState() => _ItemTileWithImageState();
}
class _ItemTileWithImageState extends State<ItemTileWithImage> {
bool _checked = false;
#override
Widget build(BuildContext context) {
Color hintColor = Theme.of(context).hintColor;
Color primaryColor = Theme.of(context).primaryColor;
return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
height: 85,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: hintColor, width: 0.5),
),
),
child: CheckboxListTile(
activeColor: primaryColor,
checkColor: Colors.white,
value: _checked,
onChanged: (val) {
setState(() {
_checked = val;
});
if (val) {
Provider.of<OrderProvider>(context, listen: false)
.insertModelItems(widget.model.code);
} else {
Provider.of<OrderProvider>(context, listen: false)
.removeModelItems(widget.model.remoteId);
}
},
secondary: widget.model.imageUrl != null
? CachedNetworkImage(
height: 80,
width: 60,
imageUrl: widget.model.imageUrl,
progressIndicatorBuilder: (context, url, downloadProgress) {
return Center(
child: Container(
width: 30,
height: 30,
child: CircularProgressIndicator(
value: downloadProgress.progress,
backgroundColor: primaryColor,
strokeWidth: 2,
),
),
);
},
errorWidget: (context, url, error) => Icon(Icons.error),
)
: Image.asset(
'assets/images/mini_logo.png',
width: 50,
height: 50,
),
title: Text(widget.model.code ?? '${widget.model.barCode}'),
subtitle: Text(
widget.model.name ?? '',
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
);
}
}
Flutter apps typically 'clean up' your off screen state. Some off-screen widgets will experience the loss of certain view related attributes. This is discussed some in the Keys section here. You can instantiate stateless widgets in a way where their variable attributes are stored off for later recall.
Here is a demo utilizing rows of CheckboxListTile widgets which keep their state even with scrolling/going off-screen. You can use a DartPad of the below code here.
// Remember CheckboxListTile status with scrolling demo.
// Currently utilizes rows in implementation.
// This can be cut down to maintain a single checkbox list tile.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(
home: Home(),
));
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
// Refresh Callback for descendant widgets (if checkboxes are generated in seperate widget)
refresh() {setState(() {});}
// Rows with checkbox title and check value
// Must update second value in entry to maintain check status while scrolling
List<List> rowData = [
['row1', true], ['row2', false], ['row3', false],
['row4', false], ['row5', false], ['row6', false],
['row7', false], ['row8', false], ['row9', false],
['row10', false], ['row11', false], ['row12', false],
['row13', false], ['row14', false], ['row15', false],
['row16', false], ['row17', false], ['row18', false],
['row19', false], ['row20', false], ['row21', false],
['row22', false], ['row23', false], ['row24', false]];
List<Row> rows = []; // Holds row objects generated from rowData (with checkboxes)
int row = 0;
int rowCount = 0;
#override
Widget build(BuildContext context) {
rows = []; // Refresh rows
for (int i=0; i<rowData.length; i++) { // Init/refresh row widgets
rows.add(Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded (
child: CheckboxListTile(
value: rowData[i][1] as bool, // Check value
title: SizedBox(
width: 100,
child: Text(rowData[i][0], style: const TextStyle(fontSize: 50))
), // Title for check box
checkColor: Colors.black,
dense: false,
onChanged: (newValue) { // On click of check box
setState(() {
print("Checkbox clicked");
rowData[i][1] = newValue; // Update value in the RowData list
});
}
),
)
]
));
}
return Scaffold(
appBar: AppBar(
title: const Text('Remember Checkboxes Demo'),
centerTitle: true,
backgroundColor: const Color(0xff66b366),
),
body: Column (
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: ListView(
children: <Widget>[for (var i in rows) i,] // Fill in rows
),
)
]
)
);
}
}
Abstracting the idea for the question's implementation, you can replace your _checked variable to be a passed in boolean from the parent of the ItemTileWithImage widget. Also, you would want to pass in a function, like the refresh() function in the demo, to call in the onChange block after updating your shared check value. This allows you to refresh the state of your parent widget to include the updated check status.
Note: I am a relative novice with Flutter so apologies if this solution is considered bad practice.
The above implementation was developed with Flutter 2.5.3 and Dart 2.14.4.
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.
When a PopupMenuButton is pressed, the currently selected value is highlighted,
but when a DropdownButton is pressed, the currently selected value is not highlighted.
Is there a way to highlight the selected value of a DropdownButton?
For reference here is some sample code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String letter = 'A';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Popup Menu Button')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 16.0),
Text('PopupMenuButton'),
buildPopupMenuButton(),
SizedBox(height: 16.0),
Text('DropdownButton'),
buildDropdownButton(),
],
),
);
}
Widget buildPopupMenuButton() {
return PopupMenuButton<String>(
padding: EdgeInsets.zero,
initialValue: letter,
onSelected: (val) => setState(() => letter = val),
child: ListTile(
title: Text('The letter $letter'),
),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem<String>(
value: 'A',
child: Text('The letter A'),
),
PopupMenuItem<String>(
value: 'B',
child: Text('The letter B'),
),
];
},
);
}
Widget buildDropdownButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<String>(
value: letter,
onChanged: (val) => setState(() => letter = val),
items: [
DropdownMenuItem<String>(
value: 'A',
child: Text('The letter A'),
),
DropdownMenuItem<String>(
value: 'B',
child: Text('The letter B'),
),
],
),
);
}
}
Here's a video that shows the issue:
The DropdownMenuItem doesn't support many custom modifications on the child element, as there's no style, background, anything actually in the DropdownMenuItem attributes to help you with that. Looking at the code, it really wasn't built for that,
Yet, there's something you could add, a simple check on the child attribute of the DropdownMenuItem, and wrap the Text child element in something else or style the Text element itself if it is checked.
One example:
Widget buildDropdownButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<String>(
value: letter,
onChanged: (val) => setState(() => letter = val),
items: [
DropdownMenuItem<String>(
value: 'A',
child: Container(
color: letter == 'A' ? Colors.black12 : null,
child: Text('The letter A'),
),
),
DropdownMenuItem<String>(
value: 'B',
child: Container(
color: letter == 'B' ? Colors.black12 : null,
child: Text('The letter B'),
),
),
],
),
);
}
Note that in a real case scenario, you would have a method with a paremeter to build each dropdown item, so the verification wouldn't have to be hardcoded like letter == 'A'.
This would be the output:
This approach allows you to style a bit, but it has an ugly result in some cases. Although it is customizable, there will always be a white margin around the item, and it also shows the same styles when the dropdown list is closed, so it gets a bit ugly on the main page.
Instead of changing the background, you can also change text colors, underline, icons on the side, something like that make it much better, like:
DropdownMenuItem<String>(
value: 'A',
child: Text('The letter A',
style: TextStyle(
color: letter == 'A' ? Colors.red : Colors.black87,
),
),
)
Well, as far as I know this grey overlay is a so called 'Ripple effect' in the material design library. It seems that Flutter does not adapt the full design in all widgets yet.
However you can try to use the InkWell widget to add this kind of animations/colors to current widgets:
https://flutter.io/docs/cookbook/gestures/ripples
E.g:
PopupMenuItem<String>(
value: 'B',
child: InkWell(child: Text('The letter B'))
),
I am not sure if the width will be correct, but at least it should show the grey overlay when you press on the entry.
You can also check the Flutter source:
https://github.com/flutter/flutter/blob/237fc2fb45639312001e947bf7465ef9f23bb699/packages/flutter/lib/src/material/popup_menu.dart#L933
Here you can see that a Inkwell is standard being used for the PopupMenuButton.
Responding to your original issue which was: "I'm interested in the darker background from the currently selected value when all of the values are shown."
Your PopupMenuButton will look at its initialValue: parameter each time it is opened--the item corresponding to this value will be highlighted. You will need to update the initialValue parameter each time using the onSelected function.
Make sure the parent widget is a StatefulWidget widget and create a reference to whatever your initialValue is. The PopupMenuButton has an onSelected parameter that takes in a function with parameter String.
Whenever you select an option from the PopupMenuButton, call
setState(() {
...
this.initialValue = value;
});
The full class will look something like this.
Class YourClass extends StatefulWidget {
#override
createState() => _YourClassState();
}
class _YourClassState extends State<YourClass> {
...
String initialValue = 'foo';
#override
Widget build(BuildContext context) {
final items = [
PopupMenuItem(
value: 'foo',
child: Text('foo'),
),
PopupMenuItem(
value: 'nice',
child: Text('nice'),
),
}
return Scaffold(
appBar: ...,
drawer: ...,
body: PopupMenuButton(
icon: ...,
itemBuilder: (_) => items,
initialValue: this.initialValue,
onSelected: (value) => bar(value),
),
);
}
void bar(String value) {
setState(() {
...
this.initialValue = value;
});
}
}
You can wrap the widget with Theme to set a highlight color.
return Theme(
data: ThemeData(highlightColor: Colors.grey[300]),
child: DropdownButton()
You may try it:
class CustomDropdownMenuItem<T> extends DropdownMenuItem<T> {
const CustomDropdownMenuItem({
super.key,
super.onTap,
super.value,
super.enabled = true,
super.alignment,
required this.current,
required super.child,
});
final T current;
#override
Widget build(BuildContext context) {
return Container(
color: current == value ? Theme.of(context).highlightColor : null,
child: super.build(context),
);
}
}
However, the element will not be completely covered in color. You can also add a check on the current device to exclude those that work correctly (web and desktop).
Basically, we have to wait for this issue to be solved.
Update:
Alternatively, you can use color selection if you use Text:
final theme = Theme.of(context);
...
return DropdownMenuItem<AppLocale>(
value: value,
onTap: () => {},
child: Text(
value.name,
style: theme.textTheme.titleMedium?.copyWith(
color: value == current ? theme.colorScheme.secondary : null),
),
);
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.